@slicedearth/whoisleuth-cli 1.47.4 → 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 (235) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,17 +1,28 @@
1
1
  import { classifyQuery } from "../lib/classify.mjs";
2
2
  import { scanBoundedJson } from "../lib/bounded-json.mjs";
3
- import { buildDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
4
- import { verifyDomainControlManifest } from "../lib/domain-control-manifest.mjs";
3
+ import { buildDomainControlFlightRecorder, validateDomainControlFlightRecorderDocument, } 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, } from "../packages/contracts/domain-control-flight-recorder.mjs";
5
+ import { DOMAIN_CONTROL_REVIEW_SCHEMA, 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, 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";
7
+ import { validateDomainControlReviewDocument, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
8
+ import { requireDomainName, requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
5
9
  import { buildCliLookupDocument } from "./formatters/json.mjs";
6
10
  import { buildCliDomainControlReview, } from "./domain-control-observations.mjs";
7
- import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
8
- export const CLI_DOMAIN_CONTROL_MONITOR_SCHEMA = 'whoisleuth.cli.domain-control-monitor';
9
- export const CLI_DOMAIN_CONTROL_MONITOR_VERSION = 1;
10
- export const MAX_DOMAIN_CONTROL_MONITOR_DOMAINS = 20;
11
+ import { CliUsageError } from "./errors.mjs";
12
+ export { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION };
13
+ export const MAX_DOMAIN_CONTROL_MONITOR_DOMAINS = CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS;
11
14
  function parseMonitorJson(input, label) {
12
15
  const normalized = input.replace(/^\uFEFF/u, '');
16
+ if (Buffer.byteLength(input, 'utf8') > MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES) {
17
+ throw new CliUsageError(`${label} exceeds the ${MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES}-byte limit.`);
18
+ }
13
19
  try {
14
- scanBoundedJson(normalized);
20
+ scanBoundedJson(normalized, {
21
+ maximumDepth: MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH,
22
+ maximumKeys: MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS,
23
+ maximumValues: MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES,
24
+ maximumContainerItems: MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS,
25
+ });
15
26
  return JSON.parse(normalized);
16
27
  }
17
28
  catch (cause) {
@@ -25,32 +36,187 @@ function parseMonitorJson(input, label) {
25
36
  throw new CliUsageError(`${label} must satisfy the bounded JSON structure contract.`);
26
37
  }
27
38
  }
28
- function previousSnapshot(input) {
29
- if (!input)
39
+ function exactMonitorRecord(value, keys, label) {
40
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
41
+ throw new CliUsageError(`${label} must use its exact object fields.`);
42
+ }
43
+ const record = value;
44
+ const actualKeys = Object.keys(record);
45
+ if (actualKeys.length !== keys.length || actualKeys.some((key) => !keys.includes(key))) {
46
+ throw new CliUsageError(`${label} must use its exact object fields.`);
47
+ }
48
+ return record;
49
+ }
50
+ function monitorInteger(value, minimum, maximum, label) {
51
+ if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
52
+ throw new CliUsageError(`${label} must be an integer from ${minimum} to ${maximum}.`);
53
+ }
54
+ return Number(value);
55
+ }
56
+ function previousSnapshot(input, currentGeneratedAt) {
57
+ if (input === null)
30
58
  return { observations: [] };
31
59
  const parsed = parseMonitorJson(input, 'Previous monitor snapshot');
32
- const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
33
- if (root.schema !== CLI_DOMAIN_CONTROL_MONITOR_SCHEMA || root.version !== CLI_DOMAIN_CONTROL_MONITOR_VERSION || !Array.isArray(root.observations)) {
60
+ const root = exactMonitorRecord(parsed, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, 'Previous monitor snapshot');
61
+ if (root.schema !== CLI_DOMAIN_CONTROL_MONITOR_SCHEMA || root.version !== CLI_DOMAIN_CONTROL_MONITOR_VERSION) {
34
62
  throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} version ${CLI_DOMAIN_CONTROL_MONITOR_VERSION}.`);
35
63
  }
36
- const validated = buildDomainControlFlightRecorder({
37
- schema: 'whoisleuth.domain-control-flight-recorder.input', version: 1, observations: root.observations, approvedWindows: [],
38
- });
39
- void validated;
40
- return { observations: root.observations };
64
+ let previousGeneratedAt;
65
+ try {
66
+ previousGeneratedAt = requireIsoTimestamp(root.generatedAt, 'Previous monitor snapshot generatedAt');
67
+ }
68
+ catch {
69
+ throw new CliUsageError('Previous monitor snapshot generatedAt must be a valid ISO 8601 timestamp.');
70
+ }
71
+ if (Date.parse(previousGeneratedAt) >= Date.parse(currentGeneratedAt)) {
72
+ throw new CliUsageError('Previous monitor snapshot must precede the current monitor run.');
73
+ }
74
+ const manifest = exactMonitorRecord(root.manifest, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, 'Previous monitor snapshot manifest');
75
+ if (typeof manifest.digestSha256 !== 'string'
76
+ || !/^sha256:[a-f0-9]{64}$/u.test(manifest.digestSha256)
77
+ || typeof manifest.expiresAt !== 'string') {
78
+ throw new CliUsageError('Previous monitor snapshot manifest is invalid.');
79
+ }
80
+ try {
81
+ requireIsoTimestamp(manifest.expiresAt, 'Previous monitor snapshot manifest expiresAt');
82
+ }
83
+ catch {
84
+ throw new CliUsageError('Previous monitor snapshot manifest expiresAt must be a valid ISO 8601 timestamp.');
85
+ }
86
+ if (Date.parse(manifest.expiresAt) <= Date.parse(previousGeneratedAt)) {
87
+ throw new CliUsageError('Previous monitor snapshot manifest must have been unexpired when the checkpoint was generated.');
88
+ }
89
+ const collection = exactMonitorRecord(root.collection, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, 'Previous monitor snapshot collection');
90
+ const requested = monitorInteger(collection.requested, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, 'Previous monitor snapshot requested count');
91
+ const succeeded = monitorInteger(collection.succeeded, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, 'Previous monitor snapshot succeeded count');
92
+ const failed = monitorInteger(collection.failed, 0, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, 'Previous monitor snapshot failed count');
93
+ if (requested !== succeeded + failed || !Array.isArray(collection.failures) || collection.failures.length !== failed) {
94
+ throw new CliUsageError('Previous monitor snapshot collection counts are inconsistent.');
95
+ }
96
+ const failureDomains = new Set();
97
+ const failureDomainOrder = [];
98
+ for (const [index, failureValue] of collection.failures.entries()) {
99
+ const failure = exactMonitorRecord(failureValue, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, `Previous monitor snapshot failure ${index + 1}`);
100
+ let failureDomain;
101
+ try {
102
+ failureDomain = requireDomainName(failure.domain, `Previous monitor snapshot failure ${index + 1} domain`);
103
+ }
104
+ catch {
105
+ throw new CliUsageError(`Previous monitor snapshot failure ${index + 1} domain is invalid.`);
106
+ }
107
+ if (failureDomain !== failure.domain
108
+ || failureDomains.has(failureDomain)
109
+ || typeof failure.error !== 'string'
110
+ || !DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES.includes(failure.error)
111
+ || failure.error.length > MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH) {
112
+ throw new CliUsageError(`Previous monitor snapshot failure ${index + 1} is invalid.`);
113
+ }
114
+ failureDomains.add(failureDomain);
115
+ failureDomainOrder.push(failureDomain);
116
+ }
117
+ if (!Array.isArray(root.observations)
118
+ || root.observations.length !== succeeded
119
+ || root.observations.length < MIN_DOMAIN_CONTROL_MONITOR_DOMAINS
120
+ || root.observations.length > MAX_DOMAIN_CONTROL_MONITOR_DOMAINS) {
121
+ throw new CliUsageError('Previous monitor snapshot observations are inconsistent with its collection summary.');
122
+ }
123
+ let review;
124
+ let flightRecorder;
125
+ try {
126
+ review = validateDomainControlReviewDocument(root.review);
127
+ }
128
+ catch {
129
+ throw new CliUsageError(`Previous monitor snapshot review must use the exact ${DOMAIN_CONTROL_REVIEW_SCHEMA} version ${DOMAIN_CONTROL_REVIEW_VERSION} contract.`);
130
+ }
131
+ try {
132
+ flightRecorder = validateDomainControlFlightRecorderDocument(root.flightRecorder);
133
+ }
134
+ catch {
135
+ throw new CliUsageError(`Previous monitor snapshot flight recorder must use the exact ${DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA} version ${DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION} contract.`);
136
+ }
137
+ if (!Array.isArray(root.limitations)
138
+ || root.limitations.length !== DOMAIN_CONTROL_MONITOR_LIMITATIONS.length
139
+ || root.limitations.some((value, index) => value !== DOMAIN_CONTROL_MONITOR_LIMITATIONS[index])) {
140
+ throw new CliUsageError('Previous monitor snapshot limitations are invalid.');
141
+ }
142
+ try {
143
+ buildDomainControlFlightRecorder({
144
+ schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
145
+ version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
146
+ observations: root.observations,
147
+ approvedWindows: [],
148
+ }, previousGeneratedAt);
149
+ }
150
+ catch {
151
+ throw new CliUsageError('Previous monitor snapshot observations must use the exact bounded flight-recorder input contract.');
152
+ }
153
+ const observations = root.observations;
154
+ const observationDomainOrder = observations.map((observation) => observation.domain);
155
+ const observationDomains = new Set(observationDomainOrder);
156
+ const requestedDomains = review.domains.slice(0, requested).map((item) => item.domain);
157
+ const expectedObservationDomains = requestedDomains.filter((domain) => !failureDomains.has(domain));
158
+ const expectedFailureDomains = requestedDomains.filter((domain) => !observationDomains.has(domain));
159
+ if (review.domains.length < requested
160
+ || observationDomains.size !== observations.length
161
+ || observationDomainOrder.some((domain, index) => domain !== expectedObservationDomains[index])
162
+ || observationDomainOrder.length !== expectedObservationDomains.length
163
+ || failureDomainOrder.some((domain, index) => domain !== expectedFailureDomains[index])
164
+ || failureDomainOrder.length !== expectedFailureDomains.length
165
+ || observations.some((observation) => observation.observedAt !== previousGeneratedAt)
166
+ || review.domains.some((item) => item.comparisons.some((comparison) => comparison.observedAt !== null && comparison.observedAt !== previousGeneratedAt))
167
+ || review.generatedAt !== previousGeneratedAt
168
+ || review.manifest.digestSha256 !== manifest.digestSha256
169
+ || review.manifest.expiresAt !== manifest.expiresAt
170
+ || [...observationDomains].some((domain) => !review.domains.some((item) => item.domain === domain))
171
+ || flightRecorder.generatedAt !== previousGeneratedAt
172
+ || flightRecorder.observationCount < observations.length
173
+ || [...observationDomains].some((domain) => !flightRecorder.domains.includes(domain))) {
174
+ throw new CliUsageError('Previous monitor snapshot embedded documents are inconsistent with its observation projection.');
175
+ }
176
+ return { observations };
177
+ }
178
+ function monitorFailureMessage() {
179
+ return DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES[0];
41
180
  }
42
181
  export async function runDomainControlMonitor(manifestInput, previousInput, options) {
43
- const generatedAt = options.now();
182
+ if (!Number.isSafeInteger(options.limit)
183
+ || options.limit < MIN_DOMAIN_CONTROL_MONITOR_DOMAINS
184
+ || options.limit > MAX_DOMAIN_CONTROL_MONITOR_DOMAINS) {
185
+ throw new CliUsageError(`Domain-control monitor limit must be from ${MIN_DOMAIN_CONTROL_MONITOR_DOMAINS} to ${MAX_DOMAIN_CONTROL_MONITOR_DOMAINS}.`);
186
+ }
187
+ if (!Number.isSafeInteger(options.concurrency)
188
+ || options.concurrency < MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY
189
+ || options.concurrency > MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY) {
190
+ throw new CliUsageError(`Domain-control monitor concurrency must be from ${MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY} to ${MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY}.`);
191
+ }
192
+ let generatedAt;
193
+ try {
194
+ generatedAt = requireIsoTimestamp(options.now(), 'Domain-control monitor time');
195
+ }
196
+ catch {
197
+ throw new CliUsageError('Domain-control monitor time must be a valid ISO 8601 timestamp.');
198
+ }
44
199
  const rawManifest = parseMonitorJson(manifestInput, 'Domain-control manifest');
45
- const manifest = verifyDomainControlManifest(rawManifest);
46
- const previous = previousSnapshot(previousInput);
200
+ let manifest;
201
+ try {
202
+ manifest = verifyDomainControlManifest(rawManifest);
203
+ }
204
+ catch {
205
+ throw new CliUsageError('Domain-control manifest must satisfy its supported bounded integrity contract.');
206
+ }
207
+ const checkedAt = Date.parse(generatedAt);
208
+ if (Date.parse(manifest.expiresAt) <= checkedAt) {
209
+ throw new CliUsageError('Domain-control monitor requires an unexpired manifest.');
210
+ }
211
+ const previous = previousSnapshot(previousInput, generatedAt);
47
212
  const entries = manifest.entries.slice(0, Math.min(options.limit, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS));
48
213
  const lookups = new Array(entries.length);
49
- const failures = [];
214
+ const failuresByIndex = new Array(entries.length);
50
215
  let cursor = 0;
51
216
  let completed = 0;
52
217
  const worker = async () => {
53
218
  while (cursor < entries.length) {
219
+ options.signal?.throwIfAborted();
54
220
  const index = cursor++;
55
221
  const entry = entries[index];
56
222
  if (!entry)
@@ -58,10 +224,14 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
58
224
  try {
59
225
  const classified = classifyQuery(entry.domain);
60
226
  const result = await options.executeLookup(classified, { fast: false, compact: false, ...(options.signal ? { signal: options.signal } : {}) });
227
+ options.signal?.throwIfAborted();
61
228
  lookups[index] = buildCliLookupDocument(entry.domain, classified, result, generatedAt, 'deep');
62
229
  }
63
- catch (cause) {
64
- failures.push({ domain: entry.domain, error: boundedCliErrorMessage(cause, 'Lookup failed') });
230
+ catch {
231
+ if (options.signal?.aborted) {
232
+ throw options.signal.reason || new DOMException('Aborted', 'AbortError');
233
+ }
234
+ failuresByIndex[index] = Object.freeze({ domain: entry.domain, error: monitorFailureMessage() });
65
235
  }
66
236
  finally {
67
237
  completed += 1;
@@ -70,16 +240,23 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
70
240
  }
71
241
  };
72
242
  await Promise.all(Array.from({ length: Math.min(options.concurrency, Math.max(entries.length, 1)) }, worker));
243
+ options.signal?.throwIfAborted();
244
+ const failures = failuresByIndex.filter((failure) => Boolean(failure));
73
245
  const successful = lookups.filter(Boolean);
74
246
  if (!successful.length)
75
247
  throw new Error('No domain-control monitor lookup completed successfully.');
76
248
  const review = buildCliDomainControlReview(JSON.stringify({
77
- schema: 'whoisleuth.cli.domain-control-review-input', version: 1, manifest, lookups: successful,
249
+ schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
250
+ version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
251
+ manifest,
252
+ lookups: successful,
78
253
  }), generatedAt);
79
254
  const observations = review.observations;
80
255
  const flightRecorder = buildDomainControlFlightRecorder({
81
- schema: 'whoisleuth.domain-control-flight-recorder.input', version: 1,
82
- observations: [...previous.observations, ...observations], approvedWindows: [],
256
+ schema: DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA,
257
+ version: DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
258
+ observations: [...previous.observations, ...observations],
259
+ approvedWindows: [],
83
260
  }, generatedAt);
84
261
  return Object.freeze({
85
262
  schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA,
@@ -90,11 +267,7 @@ export async function runDomainControlMonitor(manifestInput, previousInput, opti
90
267
  observations,
91
268
  review: review.review,
92
269
  flightRecorder,
93
- limitations: Object.freeze([
94
- 'This is one bounded run, not a daemon. Scheduling and secure checkpoint retention remain operator-controlled.',
95
- 'The output retains compact normalised observations and errors, not raw RDAP, WHOIS, DNS, HTTP, TLS or page payloads.',
96
- 'A failed source or lookup remains incomplete and does not establish that a previously observed value disappeared.',
97
- ]),
270
+ limitations: DOMAIN_CONTROL_MONITOR_LIMITATIONS,
98
271
  });
99
272
  }
100
273
  export function formatDomainControlMonitor(document) {
@@ -1,17 +1,12 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, scanBoundedJson, } from "../lib/bounded-json.mjs";
2
+ import { scanBoundedJson, } from "../lib/bounded-json.mjs";
3
+ 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, 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";
3
4
  import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, } from "../lib/domain-control-flight-recorder.mjs";
4
5
  import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
5
6
  import { CliUsageError } from "./errors.mjs";
6
7
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
7
- export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA = 'whoisleuth.cli.domain-control-review-input';
8
- export const CLI_DOMAIN_CONTROL_REVIEW_SCHEMA = 'whoisleuth.cli.domain-control-review';
9
- export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 1;
10
- export const MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES = 32 * 1024 * 1024;
11
- export const MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS = 100;
12
- export const MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH = MAX_BOUNDED_JSON_DEPTH + 2;
13
- export const MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS = MAX_BOUNDED_JSON_KEYS * (MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS + 1);
14
- export const MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES = MAX_BOUNDED_JSON_VALUES * (MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS + 1);
8
+ export { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_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, };
9
+ const CLI_REVIEW_INPUT_KEY_SET = new Set(CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS);
15
10
  function record(value) {
16
11
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
17
12
  }
@@ -165,14 +160,17 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
165
160
  throw new CliUsageError('Domain-control review input must be valid JSON.');
166
161
  }
167
162
  const input = record(parsed);
168
- if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA || input.version !== 1 || !Array.isArray(input.lookups)) {
169
- throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version 1.`);
163
+ if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
164
+ || input.version !== CLI_DOMAIN_CONTROL_REVIEW_VERSION
165
+ || !Array.isArray(input.lookups)) {
166
+ throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version ${CLI_DOMAIN_CONTROL_REVIEW_VERSION}.`);
170
167
  }
171
- if (Object.keys(input).some((key) => !new Set(['schema', 'version', 'manifest', 'lookups']).has(key))) {
168
+ if (Object.keys(input).some((key) => !CLI_REVIEW_INPUT_KEY_SET.has(key))) {
172
169
  throw new CliUsageError('Domain-control review input contains an unsupported field.');
173
170
  }
174
- if (input.lookups.length < 1 || input.lookups.length > MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS) {
175
- throw new CliUsageError('Domain-control review requires from 1 to 100 saved Lookup documents.');
171
+ if (input.lookups.length < MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS
172
+ || input.lookups.length > MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS) {
173
+ throw new CliUsageError(`Domain-control review requires from ${MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS} to ${MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS} saved Lookup documents.`);
176
174
  }
177
175
  const manifest = verifyDomainControlManifest(input.manifest);
178
176
  const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
@@ -185,7 +183,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
185
183
  const observations = [...latest.values()].map(domainControlObservationFromSavedLookup);
186
184
  const review = reviewDomainControlManifest({
187
185
  schema: DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
188
- version: 1,
186
+ version: DOMAIN_CONTROL_REVIEW_VERSION,
189
187
  manifest,
190
188
  observations: observations.map((item) => ({
191
189
  domain: item.domain,
@@ -225,17 +223,13 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
225
223
  latestDomainObservations: observations.length,
226
224
  ignoredHistoricalLookups: lookups.length - observations.length,
227
225
  }),
228
- limitations: Object.freeze([
229
- 'The review uses only supplied saved Lookup documents and performs no request.',
230
- 'The newest supplied observation per domain is used for desired-state comparison; all supplied observations remain available to a separate flight-recorder review.',
231
- 'Raw RDAP, WHOIS, HTTP, TLS, and page payloads are not copied into this output.',
232
- ]),
226
+ limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
233
227
  });
234
228
  }
235
229
  export function buildControlReviewInput(manifest, lookupDocuments) {
236
230
  return Object.freeze({
237
231
  schema: CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
238
- version: 1,
232
+ version: CLI_DOMAIN_CONTROL_REVIEW_VERSION,
239
233
  manifest,
240
234
  lookups: Object.freeze(lookupDocuments),
241
235
  });
package/cli/errors.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
2
+ const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
3
+ const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
2
4
  class CliUsageError extends Error {
3
5
  constructor(message) {
4
6
  super(message);
@@ -12,16 +14,19 @@ function errorMessage(error) {
12
14
  }
13
15
  function boundedCliErrorMessage(error, fallback = 'Unexpected command failure') {
14
16
  return String(errorMessage(error) || error || fallback)
15
- .replace(/[\x00-\x1f\x7f]+/g, ' ')
16
- .replace(/[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/g, '')
17
+ .replace(/[\x00-\x1f\x7f-\x9f]+/g, ' ')
18
+ .replace(CLI_DEFAULT_IGNORABLE_GLOBAL_RE, '')
17
19
  .replace(/\s+/g, ' ')
18
20
  .trim()
19
21
  .slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
20
22
  }
23
+ function hasUnsafeCliText(value) {
24
+ return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
25
+ }
21
26
  function boundedCliInputError(error, label) {
22
27
  if (error instanceof CliUsageError)
23
28
  return error;
24
29
  const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
25
30
  return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`);
26
31
  }
27
- export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, };
32
+ export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, hasUnsafeCliText, };
@@ -1,11 +1,11 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash, createPrivateKey, createPublicKey, sign, verify, } from 'node:crypto';
3
- import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, SORTED_JSON_V1, SORTED_JSON_V2, } from "../frontend/src/lib/analysis/artifact-integrity.js";
3
+ import { canonicalArtifactJsonFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
4
4
  import { hasVerifiedApplicableIntegrity, hasVerifiedArtifactStructure, MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
5
5
  import { parseBoundedJsonObject } from "./bounded-json.mjs";
6
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
6
7
  export const SIGNED_EVIDENCE_PACKAGE_SCHEMA = 'whoisleuth.signed-evidence-package';
7
8
  export const SIGNED_EVIDENCE_PACKAGE_VERSION = 2;
8
- export const LEGACY_SIGNED_EVIDENCE_PACKAGE_VERSION = 1;
9
9
  export const EVIDENCE_SIGNATURE_VERIFICATION_SCHEMA = 'whoisleuth.evidence-signature-verification';
10
10
  export const EVIDENCE_SIGNATURE_VERIFICATION_VERSION = 2;
11
11
  export const EVIDENCE_SIGNATURE_ALGORITHM = 'Ed25519';
@@ -34,13 +34,10 @@ function hasExactKeys(value, expected) {
34
34
  return keys.length === expected.size && keys.every((key) => expected.has(key));
35
35
  }
36
36
  function timestamp(value) {
37
- if (typeof value !== 'string' || value.length > 64) {
38
- throw new TypeError('Signature time must be a valid timestamp.');
39
- }
40
- const parsed = Date.parse(value);
41
- if (!Number.isFinite(parsed))
42
- throw new TypeError('Signature time must be a valid timestamp.');
43
- return new Date(parsed).toISOString();
37
+ const normalized = normalizeExplicitIsoTimestamp(value);
38
+ if (!normalized)
39
+ throw new TypeError('Signature time must be a valid timestamp with an explicit timezone.');
40
+ return normalized;
44
41
  }
45
42
  function canonicalTimestamp(value) {
46
43
  const normalized = timestamp(value);
@@ -131,7 +128,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
131
128
  const artifact = record(value.artifact);
132
129
  if (!hasExactKeys(value, PACKAGE_KEYS)
133
130
  || value.schema !== SIGNED_EVIDENCE_PACKAGE_SCHEMA
134
- || (value.version !== LEGACY_SIGNED_EVIDENCE_PACKAGE_VERSION && value.version !== SIGNED_EVIDENCE_PACKAGE_VERSION)
131
+ || value.version !== SIGNED_EVIDENCE_PACKAGE_VERSION
135
132
  || !artifact
136
133
  || !signature
137
134
  || !hasExactKeys(signature, SIGNATURE_KEYS)
@@ -146,14 +143,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
146
143
  || !SHA256_RE.test(signature.keyIdSha256)) {
147
144
  throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
148
145
  }
149
- let canonicalization;
150
- try {
151
- canonicalization = resolveArtifactCanonicalization(value.version, signature.canonicalization, [
152
- { version: LEGACY_SIGNED_EVIDENCE_PACKAGE_VERSION, canonicalization: SORTED_JSON_V1, explicit: true },
153
- { version: SIGNED_EVIDENCE_PACKAGE_VERSION, canonicalization: SORTED_JSON_V2, explicit: true },
154
- ], 'Signed evidence package');
155
- }
156
- catch {
146
+ if (signature.canonicalization !== EVIDENCE_SIGNATURE_CANONICALIZATION) {
157
147
  throw new TypeError('Signed evidence package has an unsupported or malformed envelope.');
158
148
  }
159
149
  const signedAt = canonicalTimestamp(value.signedAt);
@@ -164,13 +154,13 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
164
154
  const embeddedPublicKey = importPublicKey(publicDer, 'Embedded evidence-signing public key');
165
155
  const payload = {
166
156
  schema: SIGNED_EVIDENCE_PACKAGE_SCHEMA,
167
- version: value.version,
157
+ version: SIGNED_EVIDENCE_PACKAGE_VERSION,
168
158
  signedAt,
169
159
  artifact,
170
160
  };
171
161
  const signatureBytes = Buffer.from(signature.valueBase64, 'base64');
172
162
  if (signatureBytes.length !== 64
173
- || !verify(null, payloadBytes(payload, canonicalization), embeddedPublicKey, signatureBytes)) {
163
+ || !verify(null, payloadBytes(payload, EVIDENCE_SIGNATURE_CANONICALIZATION), embeddedPublicKey, signatureBytes)) {
174
164
  throw new TypeError('Signed evidence package failed Ed25519 verification.');
175
165
  }
176
166
  let artifactVerification = null;
@@ -1,6 +1,6 @@
1
1
  import { domainToASCII } from 'node:url';
2
2
  import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
3
- import { EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../frontend/src/lib/analysis/external-findings-import.js";
3
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } 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;
6
6
  export const MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES = 4 * 1024 * 1024;
@@ -135,8 +135,8 @@ export function mapExternalObservations(inputRaw) {
135
135
  }
136
136
  const truncated = selected.length < seen.size;
137
137
  const document = {
138
- schema: 'whoisleuth.external-findings',
139
- schemaVersion: 3,
138
+ schema: EXTERNAL_FINDINGS_SCHEMA,
139
+ schemaVersion: EXTERNAL_FINDINGS_VERSION,
140
140
  source,
141
141
  findings: selected.map((finding) => ({
142
142
  ...finding,
@@ -1,5 +1,6 @@
1
1
  import { bulkDnsSummary } from "../bulk-output.mjs";
2
- const CLI_LOOKUP_SCHEMA = 'whoisleuth.cli.lookup';
2
+ import { SAVED_LOOKUP_SCHEMA_VERSION, buildCliLookupDocument, } from "../saved-lookup.mjs";
3
+ import { CLI_LOOKUP_SCHEMA } from "../../packages/contracts/cli-lookup.mjs";
3
4
  const CLI_BULK_SCHEMA = 'whoisleuth.cli.bulk';
4
5
  const CLI_BULK_ITEM_SCHEMA = 'whoisleuth.cli.bulk.item';
5
6
  const CLI_CT_SEARCH_SCHEMA = 'whoisleuth.cli.ct-search';
@@ -9,42 +10,29 @@ const CLI_POSTURE_SCHEMA = 'whoisleuth.cli.posture';
9
10
  const CLI_HTTP_SCHEMA = 'whoisleuth.cli.http';
10
11
  const CLI_TLS_SCHEMA = 'whoisleuth.cli.tls';
11
12
  const CLI_COMPARE_SCHEMA = 'whoisleuth.cli.compare';
12
- const CLI_LOOKUP_SCHEMA_VERSION = 1;
13
+ const CLI_LOOKUP_SCHEMA_VERSION = SAVED_LOOKUP_SCHEMA_VERSION;
13
14
  const CLI_BULK_SCHEMA_VERSION = 3;
14
15
  const CLI_CT_SEARCH_SCHEMA_VERSION = 1;
15
16
  const CLI_DISCOVER_SCHEMA_VERSION = 2;
16
17
  const CLI_POSTURE_SCHEMA_VERSION = 1;
17
- const CLI_HTTP_SCHEMA_VERSION = 2;
18
+ const CLI_HTTP_SCHEMA_VERSION = 3;
18
19
  const CLI_TLS_SCHEMA_VERSION = 1;
19
20
  const CLI_COMPARE_SCHEMA_VERSION = 3;
20
- function buildCliLookupDocument(query, classified, result, generatedAt = new Date().toISOString(), mode = 'fast', collectionContext = {}) {
21
- const observerLabel = typeof collectionContext.observerLabel === 'string'
22
- ? collectionContext.observerLabel
23
- : null;
24
- const vantageLabel = typeof collectionContext.vantageLabel === 'string'
25
- ? collectionContext.vantageLabel
26
- : null;
27
- return {
28
- ...result,
29
- schema: CLI_LOOKUP_SCHEMA,
30
- version: CLI_LOOKUP_SCHEMA_VERSION,
31
- generatedAt,
32
- mode: mode === 'deep' ? 'deep' : 'fast',
33
- query,
34
- type: classified.type,
35
- inputHostname: classified.inputHostname,
36
- registrableDomain: classified.registrableDomain,
37
- isSubdomain: classified.isSubdomain,
38
- ...((observerLabel || vantageLabel) ? {
39
- collectionContext: {
40
- ...(observerLabel ? { observerLabel } : {}),
41
- ...(vantageLabel ? { vantageLabel } : {}),
42
- },
43
- } : {}),
44
- };
21
+ const TERMINAL_UNSAFE_JSON_RE = /[\u007f-\u009f\u2028\u2029]|\p{Default_Ignorable_Code_Point}/gu;
22
+ function jsonUnicodeEscape(value) {
23
+ const codePoint = value.codePointAt(0);
24
+ if (codePoint <= 0xffff)
25
+ return `\\u${codePoint.toString(16).padStart(4, '0')}`;
26
+ const offset = codePoint - 0x10000;
27
+ const high = 0xd800 + (offset >> 10);
28
+ const low = 0xdc00 + (offset & 0x3ff);
29
+ return `\\u${high.toString(16).padStart(4, '0')}\\u${low.toString(16).padStart(4, '0')}`;
30
+ }
31
+ function terminalSafeJson(value, space) {
32
+ return JSON.stringify(value, null, space).replace(TERMINAL_UNSAFE_JSON_RE, jsonUnicodeEscape);
45
33
  }
46
34
  function formatJsonDocument(document) {
47
- return `${JSON.stringify(document, null, 2)}\n`;
35
+ return `${terminalSafeJson(document, 2)}\n`;
48
36
  }
49
37
  function buildCliCtSearchDocument(keyword, result, generatedAt = new Date().toISOString()) {
50
38
  return { ...result, schema: CLI_CT_SEARCH_SCHEMA, version: CLI_CT_SEARCH_SCHEMA_VERSION, generatedAt, keyword };
@@ -83,7 +71,7 @@ function discoverJsonItem(candidate, metadata) {
83
71
  function formatDiscoverJsonLines(candidates, metadata) {
84
72
  if (!candidates.length)
85
73
  return '';
86
- return `${candidates.map((candidate) => JSON.stringify(discoverJsonItem(candidate, metadata))).join('\n')}\n`;
74
+ return `${candidates.map((candidate) => terminalSafeJson(discoverJsonItem(candidate, metadata))).join('\n')}\n`;
87
75
  }
88
76
  function formatDiscoverDomainList(candidates) {
89
77
  const domains = [...new Set(candidates.flatMap((candidate) => (typeof candidate.domain === 'string' && candidate.domain ? [candidate.domain] : [])))];
@@ -149,7 +137,7 @@ function buildCliBulkDocument(items, metadata) {
149
137
  }
150
138
  function formatJsonLines(items, metadata) {
151
139
  return items.length
152
- ? `${items.map((item) => JSON.stringify(bulkJsonItem(item, metadata))).join('\n')}\n`
140
+ ? `${items.map((item) => terminalSafeJson(bulkJsonItem(item, metadata))).join('\n')}\n`
153
141
  : '';
154
142
  }
155
- export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, };
143
+ export { CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA, CLI_BULK_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_DISCOVER_ITEM_SCHEMA, CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_HTTP_SCHEMA, CLI_HTTP_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_TLS_SCHEMA, CLI_TLS_SCHEMA_VERSION, CLI_LOOKUP_SCHEMA, CLI_LOOKUP_SCHEMA_VERSION, buildCliBulkDocument, buildCliCompareDocument, buildCliCtSearchDocument, buildCliDiscoverDocument, buildCliHttpDocument, buildCliLookupDocument, buildCliPostureDocument, buildCliTlsDocument, bulkJsonItem, discoverJsonItem, formatDiscoverDomainList, formatDiscoverJsonLines, formatJsonDocument, formatJsonLines, terminalSafeJson, };