@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,14 +1,18 @@
1
1
  import { ACQUISITION_DECISIONS, ACQUISITION_MANUAL_CHECKS, ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../../packages/investigation/acquisition-decision-packet.mjs";
2
2
  import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, } from "../../packages/investigation/bulk-domain-comparison.mjs";
3
3
  import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../../packages/investigation/bulk-mail-exposure.mjs";
4
- import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../../packages/investigation/bulk-review-export.mjs";
4
+ import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS, } from "../../packages/contracts/investigation-portability.mjs";
5
+ import { MAX_BULK_SESSION_ROWS, MAX_BULK_SESSION_SOURCES } from "../../packages/contracts/workspace-portability.mjs";
6
+ import { BULK_SORT_KEYS } from "../../packages/workspace/bulk-sort.mjs";
7
+ import { normalizeExplicitIsoTimestamp } from "../../packages/evidence/observation.mjs";
5
8
  import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../../packages/investigation/lookup-claim-passport.mjs";
6
9
  import { LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "../../packages/investigation/lookup-claim-readiness.mjs";
7
10
  import { normalizeDomainControlManifestDocument } from "../../packages/evidence/domain-control-runtime.mjs";
8
- import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION } from "../../lib/domain-change-packet.mjs";
11
+ import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_REVIEW_VERSIONS, } from "../../lib/domain-change-packet.mjs";
12
+ import { DOMAIN_CHANGE_REVIEW_SCHEMA } from "../../lib/domain-change-review.mjs";
9
13
  import { DOMAIN_CONTROL_MANIFEST_SCHEMA } from "../../packages/contracts/domain-control-manifest.mjs";
10
- import { INVESTIGATION_MANIFEST_SCHEMA, INVESTIGATION_MANIFEST_VERSION, MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, } from "../investigation-manifest.mjs";
11
- import { array, boolean, domain, digest, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, validateIntegrity, } from "./structure-primitives.mjs";
14
+ import { INVESTIGATION_MANIFEST_SCHEMA, validateInvestigationManifest, } from "../investigation-manifest.mjs";
15
+ import { array, boolean, domain, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, validateIntegrity, } from "./structure-primitives.mjs";
12
16
  const CLAIM_PASSPORT_SOURCE_STATES = ['complete', 'not_found', 'partial', 'skipped', 'unavailable', 'unknown', 'unsupported'];
13
17
  const COMPARISON_STATES = ['conflicting', 'different', 'equal', 'missing', 'not_recorded', 'unavailable'];
14
18
  const SOURCE_STATES = ['complete', 'error', 'not_found', 'not_recorded', 'partial', 'skipped', 'unavailable', 'unsupported'];
@@ -209,11 +213,17 @@ function validateDomainComparison(value) {
209
213
  strings(comparison.limitations, 'Domain comparison limitations', 12, 600);
210
214
  validateIntegrity(root.integrity, 'Domain comparison integrity', root.version, BULK_DOMAIN_COMPARISON_EXPORT_VERSION);
211
215
  }
212
- function validateSourceCoverage(value, label) {
213
- const source = exact(value, ['source', 'state'], label);
216
+ function validateNullableObservationTime(value, label) {
217
+ if (value !== null && normalizeExplicitIsoTimestamp(value) !== value)
218
+ fail(label);
219
+ }
220
+ function validateSourceCoverage(value, label, observedTime = false) {
221
+ const source = exact(value, ['source', 'state', ...(observedTime ? ['observedAt'] : [])], label);
214
222
  if (typeof source.source !== 'string' || !/^[a-z][a-z0-9_-]{0,39}$/u.test(source.source))
215
223
  fail(label);
216
224
  enumeration(source.state, SOURCE_STATES.filter((state) => state !== 'not_recorded'), label);
225
+ if (observedTime)
226
+ validateNullableObservationTime(source.observedAt, `${label} observedAt`);
217
227
  }
218
228
  function validateProfileContext(value, label) {
219
229
  const context = exact(value, ['sourceState', 'activeProfileId', 'profileUpdatedAt', 'limitation'], label);
@@ -281,29 +291,40 @@ function validateBulkView(value, label) {
281
291
  text(view.caseDispositionFilter, label, 60, true);
282
292
  enumeration(view.reviewStateFilter, ['', 'unreviewed', 'reviewing', 'reviewed', 'deferred'], label);
283
293
  text(view.groupBy, label, 60, true);
284
- enumeration(view.sortKey, ['domain', 'availability', 'risk', 'opportunity', 'activity', 'registrar', 'mutation'], label);
294
+ enumeration(view.sortKey, BULK_SORT_KEYS, label);
285
295
  if (view.sortDirection !== 1 && view.sortDirection !== -1)
286
296
  fail(label);
287
297
  }
288
298
  function validateBulkReviewManifest(value) {
289
299
  const root = exact(value, ['schema', 'version', 'generatedAt', 'observedAt', 'lookupProfile', 'selection', 'view', 'rows', 'limitations', 'integrity'], 'Bulk review manifest');
300
+ const version = SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS.find((candidate) => candidate === root.version);
301
+ if (version === undefined)
302
+ fail('Bulk review manifest version');
303
+ const sourceTimes = version === BULK_REVIEW_MANIFEST_VERSION;
304
+ const maximumRows = sourceTimes ? MAX_BULK_SESSION_ROWS : 2_000;
305
+ const maximumSources = sourceTimes ? MAX_BULK_SESSION_SOURCES : 12;
290
306
  iso(root.generatedAt, 'Bulk review manifest generatedAt');
291
- iso(root.observedAt, 'Bulk review manifest observedAt');
307
+ if (sourceTimes)
308
+ validateNullableObservationTime(root.observedAt, 'Bulk review manifest observedAt');
309
+ else
310
+ iso(root.observedAt, 'Bulk review manifest observedAt');
292
311
  enumeration(root.lookupProfile, ['deep', 'fast'], 'Bulk review manifest lookup profile');
293
312
  const selection = exact(root.selection, ['count', 'domains'], 'Bulk review manifest selection');
294
- const domains = strings(selection.domains, 'Bulk review manifest selected domains', 2_000, 253);
313
+ const domains = strings(selection.domains, 'Bulk review manifest selected domains', maximumRows, 253);
295
314
  domains.forEach((item) => domain(item, 'Bulk review manifest selected domain'));
296
- if (integer(selection.count, 'Bulk review manifest selection count', 0, 2_000) !== domains.length)
315
+ if (integer(selection.count, 'Bulk review manifest selection count', 0, maximumRows) !== domains.length)
297
316
  fail('Bulk review manifest selection');
298
317
  validateBulkView(root.view, 'Bulk review manifest view');
299
- const rows = array(root.rows, 'Bulk review manifest rows', 2_000);
318
+ const rows = array(root.rows, 'Bulk review manifest rows', maximumRows);
300
319
  for (const [index, candidate] of rows.entries()) {
301
- const row = exact(candidate, ['domain', 'reviewState', 'resultState', 'scanDepth', 'sourceCoverage', 'profileContext'], `Bulk review manifest row ${index + 1}`);
320
+ const row = exact(candidate, ['domain', 'reviewState', 'resultState', 'scanDepth', ...(sourceTimes ? ['observedAt'] : []), 'sourceCoverage', 'profileContext'], `Bulk review manifest row ${index + 1}`);
302
321
  domain(row.domain, 'Bulk review manifest row domain');
303
322
  enumeration(row.reviewState, ['unreviewed', 'reviewing', 'reviewed', 'deferred'], 'Bulk review manifest review state');
304
323
  enumeration(row.resultState, ['complete', 'error'], 'Bulk review manifest result state');
305
324
  enumeration(row.scanDepth, ['deep', 'fast'], 'Bulk review manifest scan depth');
306
- array(row.sourceCoverage, 'Bulk review manifest source coverage', 12).forEach((item, sourceIndex) => validateSourceCoverage(item, `Bulk review manifest source ${sourceIndex + 1}`));
325
+ if (sourceTimes)
326
+ validateNullableObservationTime(row.observedAt, 'Bulk review manifest row observedAt');
327
+ array(row.sourceCoverage, 'Bulk review manifest source coverage', maximumSources).forEach((item, sourceIndex) => validateSourceCoverage(item, `Bulk review manifest source ${sourceIndex + 1}`, sourceTimes));
307
328
  validateProfileContext(row.profileContext, 'Bulk review manifest profile context');
308
329
  if (domains[index] !== row.domain)
309
330
  fail('Bulk review manifest selection');
@@ -311,59 +332,49 @@ function validateBulkReviewManifest(value) {
311
332
  if (rows.length !== domains.length)
312
333
  fail('Bulk review manifest selection');
313
334
  strings(root.limitations, 'Bulk review manifest limitations', 8, 600);
314
- validateIntegrity(root.integrity, 'Bulk review manifest integrity', root.version, BULK_REVIEW_MANIFEST_VERSION);
335
+ validateIntegrity(root.integrity, 'Bulk review manifest integrity', root.version, version);
315
336
  }
316
- function validateInvestigationManifest(value) {
317
- const root = exact(value, ['schema', 'version', 'generatedAt', 'application', 'workflow', 'configuration', 'artifacts', 'steps', 'summary', 'limitations', 'integrity'], 'Investigation manifest');
318
- iso(root.generatedAt, 'Investigation manifest generatedAt');
319
- const application = exact(root.application, ['name', 'version'], 'Investigation manifest application');
320
- if (application.name !== 'WHOISleuth CLI' || typeof application.version !== 'string'
321
- || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(application.version))
322
- fail('Investigation manifest application');
323
- text(root.workflow, 'Investigation manifest workflow', 160);
324
- const configuration = exact(root.configuration, ['digestSha256'], 'Investigation manifest configuration');
325
- if (configuration.digestSha256 !== null)
326
- digest(configuration.digestSha256, 'Investigation manifest configuration');
327
- const artifacts = array(root.artifacts, 'Investigation manifest artifacts', MAX_INVESTIGATION_MANIFEST_ARTIFACTS, 1);
328
- const steps = array(root.steps, 'Investigation manifest steps', MAX_INVESTIGATION_MANIFEST_ARTIFACTS, 1);
329
- let totalBytes = 0;
330
- for (const [index, candidate] of artifacts.entries()) {
331
- const item = exact(candidate, ['sequence', 'id', 'schema', 'version', 'byteLength', 'contentDigestSha256', 'canonicalDigestSha256'], `Investigation manifest artifact ${index + 1}`);
332
- if (integer(item.sequence, 'Investigation manifest artifact sequence', 1, artifacts.length) !== index + 1
333
- || item.id !== `artifact-${index + 1}`)
334
- fail('Investigation manifest artifact order');
335
- if (item.schema !== null)
336
- text(item.schema, 'Investigation manifest artifact schema', 160);
337
- if (item.version !== null)
338
- integer(item.version, 'Investigation manifest artifact version', 1, 1_000);
339
- totalBytes += integer(item.byteLength, 'Investigation manifest artifact bytes', 1, 15 * 1024 * 1024);
340
- digest(item.contentDigestSha256, 'Investigation manifest content digest');
341
- digest(item.canonicalDigestSha256, 'Investigation manifest canonical digest');
342
- const step = exact(steps[index], ['sequence', 'artifactId', 'contentDigestSha256'], `Investigation manifest step ${index + 1}`);
343
- if (step.sequence !== item.sequence || step.artifactId !== item.id || step.contentDigestSha256 !== item.contentDigestSha256)
344
- fail('Investigation manifest step linkage');
345
- }
346
- const summary = exact(root.summary, ['artifactCount', 'totalBytes'], 'Investigation manifest summary');
347
- if (integer(summary.artifactCount, 'Investigation manifest artifact count', 1, MAX_INVESTIGATION_MANIFEST_ARTIFACTS) !== artifacts.length
348
- || integer(summary.totalBytes, 'Investigation manifest total bytes', 1, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) !== totalBytes)
349
- fail('Investigation manifest summary');
350
- strings(root.limitations, 'Investigation manifest limitations', 8, 600);
351
- validateIntegrity(root.integrity, 'Investigation manifest integrity', root.version, INVESTIGATION_MANIFEST_VERSION);
337
+ function validateSourceObservations(value, label) {
338
+ const names = new Set();
339
+ return array(value, label, 16).map((candidate) => {
340
+ const source = exact(candidate, ['label', 'source', 'state', 'observedAt'], label);
341
+ const name = text(source.label, `${label} label`, 120).toLowerCase();
342
+ if (names.has(name))
343
+ fail(`${label} duplicate label`);
344
+ names.add(name);
345
+ text(source.source, `${label} source`, 240);
346
+ enumeration(source.state, ['observed', 'partial', 'unavailable'], `${label} state`);
347
+ iso(source.observedAt, `${label} observedAt`);
348
+ return source;
349
+ });
352
350
  }
353
- function validateReviewMatrix(value, label) {
351
+ function validateReviewMatrix(value, label, sources) {
354
352
  for (const [rowIndex, candidate] of array(value, label, 500).entries()) {
355
353
  const row = exact(candidate, ['owner', 'type', 'state', 'observations'], `${label} row ${rowIndex + 1}`);
356
354
  text(row.owner, `${label} owner`, 253);
357
355
  enumeration(row.type, DNS_TYPES, `${label} type`);
358
356
  const observations = array(row.observations, `${label} observations`, 16);
357
+ if (sources && observations.length !== sources.length)
358
+ fail(`${label} source coverage`);
359
359
  let complete = 0;
360
360
  const signatures = new Set();
361
+ const names = new Set();
361
362
  for (const observationCandidate of observations) {
362
- const observation = exact(observationCandidate, ['label', 'source', 'state', 'values', 'ttlRange'], `${label} observation`);
363
+ const observation = exact(observationCandidate, ['label', 'source', 'state', 'values', 'ttlRange', ...(sources ? ['observedAt'] : [])], `${label} observation`);
363
364
  text(observation.label, `${label} observation label`, 120);
364
365
  text(observation.source, `${label} observation source`, 240);
365
366
  const state = enumeration(observation.state, ['observed', 'partial', 'unavailable'], `${label} observation state`);
366
367
  const values = strings(observation.values, `${label} observation values`, 500, 16_384);
368
+ if (sources) {
369
+ const name = String(observation.label).toLowerCase();
370
+ if (names.has(name))
371
+ fail(`${label} duplicate observation`);
372
+ names.add(name);
373
+ const source = sources.find((item) => item.label === observation.label);
374
+ if (!source || ['label', 'source', 'state', 'observedAt'].some((key) => observation[key] !== source[key]))
375
+ fail(`${label} source provenance`);
376
+ iso(observation.observedAt, `${label} observation time`);
377
+ }
367
378
  if (state === 'observed') {
368
379
  complete += 1;
369
380
  signatures.add(JSON.stringify(values));
@@ -381,15 +392,17 @@ function validateReviewMatrix(value, label) {
381
392
  fail(`${label} state`);
382
393
  }
383
394
  }
384
- function validateDomainChangeReview(value, label) {
385
- const review = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'state', 'authoritativeRecordMatrix', 'resolverDivergenceMatrix', 'dnssecAutomation', 'acmeDependencies', 'certificate', 'services', 'hsts', 'gate', 'limitations'], label);
386
- if (review.schema !== 'whoisleuth.domain-change.review' || review.version !== 1)
395
+ function validateDomainChangeReview(value, label, version) {
396
+ const retainedTimes = version >= 2;
397
+ const review = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'state', 'authoritativeRecordMatrix', 'resolverDivergenceMatrix', 'dnssecAutomation', 'acmeDependencies', 'certificate', 'services', 'hsts', 'gate', 'limitations', ...(retainedTimes ? ['sourceObservations'] : [])], label);
398
+ if (review.schema !== DOMAIN_CHANGE_REVIEW_SCHEMA || review.version !== version)
387
399
  fail(label);
388
400
  iso(review.generatedAt, `${label} generatedAt`);
389
401
  domain(review.domain, `${label} domain`);
390
402
  enumeration(review.state, ['ready', 'review'], `${label} state`);
391
- validateReviewMatrix(review.authoritativeRecordMatrix, `${label} authority matrix`);
392
- validateReviewMatrix(review.resolverDivergenceMatrix, `${label} resolver matrix`);
403
+ const sources = retainedTimes ? exact(review.sourceObservations, ['authorities', 'resolvers'], `${label} source observations`) : null;
404
+ validateReviewMatrix(review.authoritativeRecordMatrix, `${label} authority matrix`, sources ? validateSourceObservations(sources.authorities, `${label} authority sources`) : undefined);
405
+ validateReviewMatrix(review.resolverDivergenceMatrix, `${label} resolver matrix`, sources ? validateSourceObservations(sources.resolvers, `${label} resolver sources`) : undefined);
393
406
  const automation = exact(review.dnssecAutomation, ['state', 'cdsObserved', 'cdnskeyObserved', 'csyncObserved', 'conflictingTypes', 'detail'], `${label} DNSSEC automation`);
394
407
  enumeration(automation.state, ['not_observed', 'conflict', 'partial', 'review_ready'], `${label} DNSSEC automation`);
395
408
  boolean(automation.cdsObserved, `${label} CDS observed`);
@@ -405,8 +418,13 @@ function validateDomainChangeReview(value, label) {
405
418
  optionalText(dependency.provider, `${label} ACME provider`, 120);
406
419
  enumeration(dependency.state, ['confirmed', 'partial', 'unknown'], `${label} ACME state`);
407
420
  }
408
- const certificate = exact(review.certificate, ['state', 'continuity', 'findings'], `${label} certificate`);
421
+ const certificate = exact(review.certificate, ['state', 'continuity', 'findings', ...(retainedTimes ? ['observedAt'] : [])], `${label} certificate`);
409
422
  enumeration(certificate.state, ['not_supplied', 'observed', 'partial', 'unavailable'], `${label} certificate state`);
423
+ if (retainedTimes) {
424
+ iso(certificate.observedAt, `${label} certificate observedAt`, true);
425
+ if ((certificate.state === 'not_supplied') !== (certificate.observedAt === null))
426
+ fail(`${label} certificate observation time`);
427
+ }
410
428
  enumeration(certificate.continuity, ['unknown', 'retained', 'changes'], `${label} certificate continuity`);
411
429
  strings(certificate.findings, `${label} certificate findings`, 8, 600);
412
430
  for (const candidate of array(review.services, `${label} services`, 1_000)) {
@@ -501,21 +519,43 @@ function expectedDomainChangeSummary(before, after) {
501
519
  const beforeRows = rows(before);
502
520
  const afterRows = rows(after);
503
521
  const keys = [...new Set([...beforeRows.keys(), ...afterRows.keys()])].sort();
504
- return keys.slice(0, 500).flatMap((key) => {
505
- const left = beforeRows.get(key);
506
- const right = afterRows.get(key);
507
- const values = (row) => [...new Set((row?.observations ?? [])
508
- .flatMap((observation) => observation.values))].sort();
509
- const beforeValues = values(left);
510
- const afterValues = values(right);
511
- if (sameValues(beforeValues, afterValues))
512
- return [];
522
+ const collectionComplete = (review) => {
523
+ const row = (review.authoritativeRecordMatrix[0]);
524
+ const observations = row?.observations ?? [];
525
+ return observations.length >= 2 && observations.every((item) => item.state === 'observed');
526
+ };
527
+ const rowEvidence = (review, row) => {
528
+ if (!row)
529
+ return { state: collectionComplete(review) ? 'complete' : 'unavailable', values: [] };
530
+ const observations = row.observations;
531
+ if (observations.some((item) => item.state !== 'observed'))
532
+ return { state: 'partial', values: [] };
533
+ if (row.state === 'different')
534
+ return { state: 'inconsistent', values: [] };
535
+ if (row.state !== 'aligned')
536
+ return { state: 'insufficient', values: [] };
537
+ return { state: 'complete', values: [...new Set(observations.flatMap((item) => item.values))].sort() };
538
+ };
539
+ const changed = [];
540
+ for (const key of keys.slice(0, 500)) {
541
+ const left = rowEvidence(before, beforeRows.get(key));
542
+ const right = rowEvidence(after, afterRows.get(key));
513
543
  const [owner, type] = key.split('\u0000');
514
- return [{ owner: owner ?? '', type: type ?? '', beforeValues, afterValues }];
515
- });
544
+ if (left.state !== 'complete' || right.state !== 'complete') {
545
+ continue;
546
+ }
547
+ else if (!sameValues(left.values, right.values)) {
548
+ changed.push({ owner: owner ?? '', type: type ?? '', beforeValues: left.values, afterValues: right.values });
549
+ }
550
+ }
551
+ return changed;
516
552
  }
517
553
  function validateDomainChangePacket(value) {
518
554
  const root = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'reference', 'state', 'gate', 'summary', 'evidence', 'limitations', 'integrity'], 'Domain change packet');
555
+ const version = integer(root.version, 'Domain change packet version', 1, 1_000);
556
+ const reviewVersion = DOMAIN_CHANGE_PACKET_REVIEW_VERSIONS[version];
557
+ if (reviewVersion === undefined)
558
+ fail('Domain change packet version');
519
559
  iso(root.generatedAt, 'Domain change packet generatedAt');
520
560
  domain(root.domain, 'Domain change packet domain');
521
561
  text(root.reference, 'Domain change packet reference', 200);
@@ -538,8 +578,8 @@ function validateDomainChangePacket(value) {
538
578
  enumeration(summary.postChangeState, ['ready', 'review'], 'Domain change packet post-change state');
539
579
  enumeration(summary.assuranceState, ['incomplete', 'needs_review', 'ready'], 'Domain change packet assurance state');
540
580
  const evidence = exact(root.evidence, ['preChange', 'postChange', 'assurance'], 'Domain change packet evidence');
541
- const pre = validateDomainChangeReview(evidence.preChange, 'Domain change packet pre-change review');
542
- const post = validateDomainChangeReview(evidence.postChange, 'Domain change packet post-change review');
581
+ const pre = validateDomainChangeReview(evidence.preChange, 'Domain change packet pre-change review', reviewVersion);
582
+ const post = validateDomainChangeReview(evidence.postChange, 'Domain change packet post-change review', reviewVersion);
543
583
  const assurance = validatePlannedAssurance(evidence.assurance, 'Domain change packet assurance');
544
584
  const assuranceResult = assurance.result;
545
585
  if (pre.generatedAt !== root.generatedAt || post.generatedAt !== root.generatedAt || assurance.generatedAt !== root.generatedAt
@@ -566,7 +606,7 @@ function validateDomainChangePacket(value) {
566
606
  }))
567
607
  fail('Domain change packet summary');
568
608
  strings(root.limitations, 'Domain change packet limitations', 8, 600);
569
- validateIntegrity(root.integrity, 'Domain change packet integrity', root.version, DOMAIN_CHANGE_PACKET_VERSION);
609
+ validateIntegrity(root.integrity, 'Domain change packet integrity', root.version, version);
570
610
  }
571
611
  export function validateSignedDigestArtifactStructure(schema, value) {
572
612
  if (value.schema !== schema)
@@ -1,105 +1 @@
1
- const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
2
- const DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
3
- const DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
4
- export const HEX_DIGEST_RE = /^[a-f0-9]{64}$/u;
5
- export const SEMANTIC_VERSION_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
6
- export function fail(label) {
7
- throw new TypeError(`${label} has an unsupported or malformed structure.`);
8
- }
9
- export function record(value, label) {
10
- if (!value || typeof value !== 'object' || Array.isArray(value))
11
- fail(label);
12
- return value;
13
- }
14
- export function exact(value, keys, label) {
15
- const source = record(value, label);
16
- const actual = Object.keys(source);
17
- if (actual.length !== keys.length || actual.some((key) => !keys.includes(key)))
18
- fail(label);
19
- return source;
20
- }
21
- export function exactOptional(value, required, optional, label) {
22
- const source = record(value, label);
23
- const actual = Object.keys(source);
24
- if (required.some((key) => !actual.includes(key))
25
- || actual.some((key) => !required.includes(key) && !optional.includes(key)))
26
- fail(label);
27
- return source;
28
- }
29
- export function text(value, label, maximum = 2_000, allowEmpty = false) {
30
- if (typeof value !== 'string' || value.length > maximum || CONTROL_RE.test(value) || (!allowEmpty && !value))
31
- fail(label);
32
- return value;
33
- }
34
- export function optionalText(value, label, maximum = 2_000) {
35
- if (value !== null)
36
- text(value, label, maximum);
37
- }
38
- export function iso(value, label, nullable = false) {
39
- if (nullable && value === null)
40
- return;
41
- const candidate = text(value, label, 64);
42
- const parsed = Date.parse(candidate);
43
- if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== candidate)
44
- fail(label);
45
- }
46
- export function integer(value, label, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) {
47
- if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum)
48
- fail(label);
49
- return Number(value);
50
- }
51
- export function boolean(value, label) {
52
- if (typeof value !== 'boolean')
53
- fail(label);
54
- }
55
- export function enumeration(value, allowed, label) {
56
- if (typeof value !== 'string' || !allowed.includes(value))
57
- fail(label);
58
- return value;
59
- }
60
- export function array(value, label, maximum, minimum = 0) {
61
- if (!Array.isArray(value) || value.length < minimum || value.length > maximum)
62
- fail(label);
63
- return value;
64
- }
65
- export function strings(value, label, maximum, textMaximum = 2_000) {
66
- const values = array(value, label, maximum);
67
- for (const item of values)
68
- text(item, label, textMaximum);
69
- return values;
70
- }
71
- export function digest(value, label) {
72
- if (typeof value !== 'string' || !DIGEST_RE.test(value))
73
- fail(label);
74
- }
75
- export function sameValues(left, right) {
76
- return left.length === right.length && left.every((value, index) => value === right[index]);
77
- }
78
- export function domain(value, label) {
79
- if (typeof value !== 'string' || !DOMAIN_RE.test(value))
80
- fail(label);
81
- }
82
- export function absoluteUrl(value, label, protocols) {
83
- const candidate = text(value, label, 2_048);
84
- try {
85
- const parsed = new URL(candidate);
86
- if (!protocols.includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash)
87
- fail(label);
88
- return parsed;
89
- }
90
- catch {
91
- return fail(label);
92
- }
93
- }
94
- export function nullableRecord(value, label) {
95
- if (value !== null)
96
- record(value, label);
97
- }
98
- export function validateIntegrity(value, label, version, currentVersion) {
99
- if (version !== currentVersion)
100
- fail(label);
101
- const integrity = exact(value, ['algorithm', 'canonicalization', 'digestSha256'], label);
102
- if (integrity.algorithm !== 'SHA-256' || integrity.canonicalization !== 'sorted-json-v2')
103
- fail(label);
104
- digest(integrity.digestSha256, label);
105
- }
1
+ export * from "../../packages/evidence/artifact-structure.mjs";