@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -2,8 +2,13 @@ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
2
  import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
3
3
  export const CLI_DOMAIN_CONTROL_MONITOR_SCHEMA = 'whoisleuth.cli.domain-control-monitor';
4
4
  export const PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION = 1;
5
- export const CLI_DOMAIN_CONTROL_MONITOR_VERSION = 2;
6
- export const SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, CLI_DOMAIN_CONTROL_MONITOR_VERSION];
5
+ export const CLI_DOMAIN_CONTROL_MONITOR_VERSION = 3;
6
+ export const SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, 2, CLI_DOMAIN_CONTROL_MONITOR_VERSION];
7
+ export const DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS = Object.freeze({
8
+ 1: Object.freeze({ review: 1, flightRecorder: 1 }),
9
+ 2: Object.freeze({ review: 2, flightRecorder: 2 }),
10
+ 3: Object.freeze({ review: 3, flightRecorder: 3 }),
11
+ });
7
12
  export const MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES = 16 * 1024 * 1024;
8
13
  export const MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH = 48;
9
14
  export const MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS = 50_000;
@@ -100,6 +105,12 @@ export const DOMAIN_CONTROL_MONITOR_SCHEMA_LIFECYCLE = defineSchemaLifecycleFami
100
105
  {
101
106
  id: 'domain-control-monitor-v2', path: 'test/fixtures/domain-control-monitor-v2.json',
102
107
  bytes: 18_023, sha256: 'f4017f3dc84b3adb6b6114ea49c37ff05939779daabbbb56c868010145cac55c',
108
+ contentDigestSha256: null, schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, version: 2,
109
+ role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
110
+ },
111
+ {
112
+ id: 'domain-control-monitor-v3', path: 'test/fixtures/domain-control-monitor-v3.json',
113
+ bytes: 19_215, sha256: 'a7233a0b52b282e3ad8097b9bd71bf15125b743e60a421cfec765cb8b9456563',
103
114
  contentDigestSha256: null, schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, version: CLI_DOMAIN_CONTROL_MONITOR_VERSION,
104
115
  role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
105
116
  },
@@ -10,10 +10,10 @@ export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA = 'whoisleuth.cli.domain-con
10
10
  export const CLI_DOMAIN_CONTROL_REVIEW_SCHEMA = 'whoisleuth.cli.domain-control-review';
11
11
  export const PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION = 1;
12
12
  export const PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION = 1;
13
- export const DOMAIN_CONTROL_REVIEW_VERSION = 2;
14
- export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 2;
15
- export const SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION, DOMAIN_CONTROL_REVIEW_VERSION];
16
- export const SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, CLI_DOMAIN_CONTROL_REVIEW_VERSION];
13
+ export const DOMAIN_CONTROL_REVIEW_VERSION = 3;
14
+ export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 3;
15
+ export const SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION, 2, DOMAIN_CONTROL_REVIEW_VERSION];
16
+ export const SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, 2, CLI_DOMAIN_CONTROL_REVIEW_VERSION];
17
17
  export const DOMAIN_CONTROL_REVIEW_OBSERVATION_FACTOR = 2;
18
18
  export const MAX_DOMAIN_CONTROL_REVIEW_OBSERVATIONS = MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES
19
19
  * DOMAIN_CONTROL_REVIEW_OBSERVATION_FACTOR;
@@ -87,7 +87,8 @@ export const PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([
87
87
  'not_configured',
88
88
  'due',
89
89
  ]);
90
- export const DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([...PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS, 'observed']);
90
+ export const DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS = Object.freeze([...PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS, 'observed']);
91
+ export const DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([...DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS, 'not_due']);
91
92
  export const DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS = Object.freeze([
92
93
  'domain',
93
94
  'state',
@@ -108,11 +109,21 @@ export const PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
108
109
  'Only complete observations can produce drift. Partial, unavailable, unsupported, or missing evidence remains inconclusive.',
109
110
  'A valid manifest digest or signature establishes file integrity, not the correctness of the desired state or supplied observations.',
110
111
  ]);
111
- export const DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
112
+ export const DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS = Object.freeze([
112
113
  PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS[0],
113
114
  'Only complete observations with valid source times within the current review-age window can establish alignment, drift or expected absence. Observation-only fields do not establish alignment.',
114
115
  PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS[2],
115
116
  ]);
117
+ export const DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
118
+ ...DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS,
119
+ 'Renewal dates are analyst-authored reminders, not observations of configuration.',
120
+ ]);
121
+ export function domainControlReviewCountKeys(version) {
122
+ return version === 1 ? PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS : version === 2 ? DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS : DOMAIN_CONTROL_REVIEW_COUNT_KEYS;
123
+ }
124
+ export function domainControlReviewLimitations(version) {
125
+ return version === 1 ? PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS : version === 2 ? DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS : DOMAIN_CONTROL_REVIEW_LIMITATIONS;
126
+ }
116
127
  export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS = Object.freeze([
117
128
  'schema',
118
129
  'version',
@@ -233,11 +244,11 @@ const CORE_REVIEW_SHAPES = SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS.flatMap((ver
233
244
  objects: [
234
245
  { path: '$', requiredKeys: DOMAIN_CONTROL_REVIEW_ROOT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
235
246
  { path: '$.manifest', requiredKeys: DOMAIN_CONTROL_REVIEW_MANIFEST_SUMMARY_KEYS, optionalKeys: [], unknownKeys: 'reject' },
236
- { path: '$.counts', requiredKeys: version === PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION ? PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS : DOMAIN_CONTROL_REVIEW_COUNT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
247
+ { path: '$.counts', requiredKeys: domainControlReviewCountKeys(version), optionalKeys: [], unknownKeys: 'reject' },
237
248
  { path: '$.domains[]', requiredKeys: DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS, optionalKeys: [], unknownKeys: 'reject' },
238
249
  { path: '$.domains[].comparisons[]', requiredKeys: version === PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION ? PUBLIC_DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS : DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS, optionalKeys: [], unknownKeys: 'reject' },
239
250
  ],
240
- fixedArrays: [{ path: '$.limitations', values: version === PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION ? PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS : DOMAIN_CONTROL_REVIEW_LIMITATIONS }],
251
+ fixedArrays: [{ path: '$.limitations', values: domainControlReviewLimitations(version) }],
241
252
  normalisation: 'preserve_document',
242
253
  target: null,
243
254
  },
@@ -253,6 +264,10 @@ function reviewFixtureShapeId(schema, version) {
253
264
  return `domain-control-review.${cli ? 'cli-' : ''}${schema.endsWith('-input') ? 'input' : 'document'}.v${version}`;
254
265
  }
255
266
  const CURRENT_REVIEW_FIXTURE_CONTENT = {
267
+ "domain-control-review-input-v3": { "bytes": 1879, "sha256": "747cccb9a0610dd5b77ebd2a01f44dc7bd6a929b09b6e52037596427d0c2275b" },
268
+ "domain-control-review-v3": { "bytes": 4496, "sha256": "736926cf402ca868ddc832c229b88d3c91b44a4c73563c356633a69c1ec8d06c" },
269
+ "cli-domain-control-review-input-v3": { "bytes": 4123, "sha256": "0283efe6f78551a8aa5a9449a608fb4f3b46b39a9f11f6ffe93244979b16d30b" },
270
+ "cli-domain-control-review-v3": { "bytes": 9194, "sha256": "31c263f17cd663be74ab9801970a8800894da4023842137e41f602fde633452e" },
256
271
  "domain-control-review-input-v2": {
257
272
  "bytes": 1879,
258
273
  "sha256": "ac3b5913a44c47bd179cd69e4a8bf4268f10e9f01dd7f59c7d722a8c0d241ad1"
@@ -408,13 +423,19 @@ export const DOMAIN_CONTROL_REVIEW_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamil
408
423
  scope: 'repository',
409
424
  },
410
425
  ].flatMap((fixture) => {
411
- const version = fixture.schema.startsWith('whoisleuth.cli.') ? CLI_DOMAIN_CONTROL_REVIEW_VERSION : DOMAIN_CONTROL_REVIEW_VERSION;
412
- const id = fixture.id.replace(/v\d+$/u, `v${version}`);
413
- const content = CURRENT_REVIEW_FIXTURE_CONTENT[id];
414
- return [
415
- { ...fixture, shapeId: reviewFixtureShapeId(fixture.schema, fixture.version), role: fixture.role === 'input' ? 'input' : 'historical', expectation: fixture.schema === CLI_DOMAIN_CONTROL_REVIEW_SCHEMA ? 'historical_output_exact' : 'accepted_exact', expectedOutputFixtureId: null },
416
- { ...fixture, ...content, id, path: `test/fixtures/${id}.json`, version, shapeId: reviewFixtureShapeId(fixture.schema, version), role: fixture.role, expectation: fixture.expectation, expectedOutputFixtureId: fixture.expectedOutputFixtureId?.replace(/v\d+$/u, `v${version}`) ?? null },
417
- ];
426
+ const currentVersion = fixture.schema.startsWith('whoisleuth.cli.') ? CLI_DOMAIN_CONTROL_REVIEW_VERSION : DOMAIN_CONTROL_REVIEW_VERSION;
427
+ const versions = fixture.schema.startsWith('whoisleuth.cli.') ? SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS : SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS;
428
+ return versions.map((version) => {
429
+ const id = fixture.id.replace(/v\d+$/u, `v${version}`);
430
+ const current = version === currentVersion;
431
+ return {
432
+ ...fixture, ...(version === 1 ? {} : CURRENT_REVIEW_FIXTURE_CONTENT[id]), id,
433
+ path: `test/fixtures/${id}.json`, version, shapeId: reviewFixtureShapeId(fixture.schema, version),
434
+ role: fixture.role === 'input' ? 'input' : current ? 'current' : 'historical',
435
+ expectation: current ? fixture.expectation : fixture.schema === CLI_DOMAIN_CONTROL_REVIEW_SCHEMA ? 'historical_output_exact' : 'accepted_exact',
436
+ expectedOutputFixtureId: current ? fixture.expectedOutputFixtureId?.replace(/v\d+$/u, `v${version}`) ?? null : null,
437
+ };
438
+ });
418
439
  }),
419
440
  metadata: {
420
441
  enforcement: 'declarative_only',
@@ -0,0 +1,2 @@
1
+ export const MAX_HAR_ENTRIES = 10_000;
2
+ export const HAR_TIMING_PHASES = ['blocked', 'dns', 'connect', 'ssl', 'send', 'wait', 'receive'];
@@ -0,0 +1,4 @@
1
+ /** Selected account telemetry, never a provider connection or an identity verdict. */
2
+ export const IDENTITY_EVENTS_INPUT_SCHEMA = 'whoisleuth.identity-events.input';
3
+ export const IDENTITY_EVENTS_INPUT_VERSION = 1;
4
+ export const MAX_IDENTITY_EVENTS = 10_000;
@@ -1,5 +1,7 @@
1
1
  export const CLI_INVESTIGATION_RUN_SCHEMA = 'whoisleuth.cli.investigation-run';
2
2
  export const CLI_INVESTIGATION_RUN_VERSION = 3;
3
+ export const CLI_INVESTIGATION_PREVIEW_SCHEMA = 'whoisleuth.cli.investigation-preview';
4
+ export const CLI_INVESTIGATION_PREVIEW_VERSION = 1;
3
5
  export const SUPPORTED_CLI_INVESTIGATION_RUN_VERSIONS = Object.freeze([1, 2, 3]);
4
6
  export const MAX_INVESTIGATION_RUN_BYTES = 24 * 1024 * 1024;
5
7
  export const MAX_INVESTIGATION_RUN_SELECTIONS = 16;
@@ -1,3 +1,13 @@
1
+ /** Syntactic identity only: private and reserved addresses remain valid evidence. */
2
+ export function canonicalIpAddress(value) {
3
+ const candidate = typeof value === 'string' ? value.trim() : value;
4
+ const parsed = addressValue(candidate);
5
+ if (!parsed)
6
+ return null;
7
+ if (parsed.family === 4)
8
+ return [24, 16, 8, 0].map(shift => (parsed.value >>> shift) & 255).join('.');
9
+ return new URL(`https://[${candidate}]/`).hostname.slice(1, -1);
10
+ }
1
11
  export function ipv6Groups(value) {
2
12
  const address = addressValue(value);
3
13
  if (address?.family !== 6)
@@ -0,0 +1,5 @@
1
+ export const MAX_AUTHENTICATION_HEADERS = 512;
2
+ export const MAX_AUTHENTICATION_CLAUSES = 64;
3
+ export const MAX_AUTHENTICATION_VALUE_LENGTH = 16 * 1024;
4
+ export const MAIL_AUTHENTICATION_METHODS = Object.freeze(['spf', 'dkim', 'dmarc', 'arc']);
5
+ export const MAIL_AUTHENTICATION_PROPERTIES = ['smtp.mailfrom', 'smtp.helo', 'header.d', 'header.i', 'header.from'];
@@ -0,0 +1,31 @@
1
+ export const MESSAGE_INTAKE_SCHEMA = 'whoisleuth.message-intake';
2
+ export const MESSAGE_INTAKE_VERSION = 1;
3
+ export const MESSAGE_INTAKE_KINDS = ['text', 'email', 'calendar', 'qr', 'pdf', 'docx', 'har', 'identity'];
4
+ export const MESSAGE_INTAKE_INPUTS = {
5
+ text: { label: 'Pasted text or links', accept: '.txt,text/plain', binary: false },
6
+ email: { label: 'Email (.eml, including nested messages)', accept: '.eml,message/rfc822', binary: false },
7
+ calendar: { label: 'Calendar invitation (.ics)', accept: '.ics,text/calendar', binary: false },
8
+ qr: { label: 'QR image (still PNG)', accept: '.png,image/png', binary: true },
9
+ pdf: { label: 'PDF document', accept: '.pdf,application/pdf', binary: true },
10
+ docx: { label: 'Word document (.docx)', accept: '.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document', binary: true },
11
+ har: { label: 'HTTP archive (.har)', accept: '.har,application/json', binary: false },
12
+ identity: { label: 'Identity events (selected JSON)', accept: '.json,application/json', binary: false },
13
+ };
14
+ export const IDENTITY_ACTIONS = [
15
+ { id: 'opened_link', label: 'Opened the link' },
16
+ { id: 'entered_password', label: 'Entered a password' },
17
+ { id: 'approved_signin', label: 'Approved a sign-in or MFA request' },
18
+ { id: 'granted_consent', label: 'Granted application consent' },
19
+ { id: 'entered_device_code', label: 'Entered a device code' },
20
+ { id: 'executed_command', label: 'Ran a command or installed software' },
21
+ ];
22
+ // Byte admission, extraction work and decoded pixels are independent bounds.
23
+ export const MAX_MESSAGE_INTAKE_BYTES = 16 * 1024 * 1024;
24
+ export const MAX_MESSAGE_PARTS = 256;
25
+ export const MAX_MESSAGE_DEPTH = 12;
26
+ export const MAX_MESSAGE_HTML_NODES = 50_000;
27
+ export const MAX_INTAKE_LINKS = 512;
28
+ export const MAX_INTAKE_URL_LENGTH = 8_192;
29
+ export const MAX_EMBEDDED_LINK_DEPTH = 8;
30
+ export const MAX_AUTH_SCOPES = 64;
31
+ export const MESSAGE_ACTION_HINTS = ['clipboard_instruction', 'shell_instruction', 'verification_prompt', 'device_code_instruction', 'consent_instruction'];
@@ -10,7 +10,10 @@ export const CLI_COMPARISON_LEDGER_SCHEMA = 'whoisleuth.cli.comparison-ledger';
10
10
  export const CLI_COMPARISON_LEDGER_VERSION = 1;
11
11
  export const MAX_RETAINED_ARTIFACT_DIFF_BYTES = 8 * 1024 * 1024;
12
12
  export const RELATIONSHIP_EVIDENCE_SCHEMA = 'whoisleuth.relationship-evidence';
13
- export const RELATIONSHIP_EVIDENCE_VERSION = 3;
13
+ export const RELATIONSHIP_EVIDENCE_VERSION = 4;
14
+ export const SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS = Object.freeze([2, 3, RELATIONSHIP_EVIDENCE_VERSION]);
15
+ export const RELATIONSHIP_SOURCE_PROVENANCE_VERSION = 3;
16
+ export const RELATIONSHIP_PUBLIC_ADDRESS_VERSION = 4;
14
17
  export const TLS_RELATIONSHIP_PROFILE_VERSION = 2;
15
18
  export const SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS = Object.freeze([TLS_RELATIONSHIP_PROFILE_VERSION]);
16
19
  export const OFFLINE_COMPARISON_COMPATIBILITY_FACADES = Object.freeze([
@@ -0,0 +1,10 @@
1
+ /** Stable interchange identities describe DNS publication, not message authentication. */
2
+ export const PASSIVE_MAIL_LABELS = Object.freeze({
3
+ authenticated_mail: 'MX, SPF and DMARC observed',
4
+ evidence_incomplete: 'Mail DNS evidence incomplete',
5
+ mail_auth_gap: 'MX observed; SPF or DMARC absent',
6
+ mail_auth_incomplete: 'MX observed; policy evidence incomplete',
7
+ no_explicit_mx: 'No explicit MX observed',
8
+ null_mx: 'Null MX observed',
9
+ });
10
+ export const PASSIVE_MAIL_INTERPRETATION = 'The retained authenticated_mail state means MX, SPF and DMARC publications were observed; it does not establish policy validity, alignment, message authentication or delivery.';
@@ -1,8 +1,8 @@
1
- export { MAX_WEB_CAPTURE_MANIFEST_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "./analyst-interchange.mjs";
1
+ export { MAX_WEB_CAPTURE_MANIFEST_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA, WEB_CAPTURE_MANIFEST_VERSION, WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS, WEB_CAPTURE_SUMMARY_SCHEMA, WEB_CAPTURE_SUMMARY_VERSION, } from "./analyst-interchange.mjs";
2
2
  export const WEB_CAPTURE_DOM_DIGEST_SCHEMA = 'whoisleuth.dom-digest';
3
3
  export const WEB_CAPTURE_DOM_DIGEST_VERSION = 1;
4
4
  export const WEB_CAPTURE_COMPARISON_SCHEMA = 'whoisleuth.web-capture-comparison';
5
- export const WEB_CAPTURE_COMPARISON_VERSION = 3;
5
+ export const WEB_CAPTURE_COMPARISON_VERSION = 4;
6
6
  export const MAX_WEB_CAPTURE_DOM_DIGEST_BYTES = 1024 * 1024;
7
7
  export const MAX_WEB_CAPTURE_SCREENSHOT_BYTES = 10 * 1024 * 1024;
8
8
  export const MAX_WEB_CAPTURE_DOM_ELEMENTS = 20_000;
@@ -47,9 +47,9 @@ export const MAX_CAMPAIGN_IMPORT_BYTES = 2 * 1024 * 1024;
47
47
  export const MAX_CAMPAIGN_INPUT_RECORDS = 500;
48
48
  export const MAX_CAMPAIGN_STORE_BYTES = 512 * 1024;
49
49
  export const WATCHLIST_SCHEMA = 'whoisleuth.watchlists';
50
- export const WATCHLIST_SCHEMA_VERSION = 2;
51
- export const WATCHLIST_BROWSER_SUPPORTED_VERSIONS = Object.freeze([WATCHLIST_SCHEMA_VERSION]);
52
- export const WATCHLIST_EXPORT_SUPPORTED_VERSIONS = Object.freeze([WATCHLIST_SCHEMA_VERSION]);
50
+ export const WATCHLIST_SCHEMA_VERSION = 4;
51
+ export const WATCHLIST_BROWSER_SUPPORTED_VERSIONS = Object.freeze([2, 3, WATCHLIST_SCHEMA_VERSION]);
52
+ export const WATCHLIST_EXPORT_SUPPORTED_VERSIONS = WATCHLIST_BROWSER_SUPPORTED_VERSIONS;
53
53
  export const MAX_WATCHLISTS = 100;
54
54
  export const MAX_WATCHLIST_INPUTS = MAX_WATCHLISTS * 4;
55
55
  export const MAX_WATCHLIST_NAME_LENGTH = 100;
@@ -114,8 +114,8 @@ export const MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN = 12;
114
114
  export const MAX_WEBSITE_SNAPSHOT_STORE_BYTES = 512 * 1024;
115
115
  export const MAX_WEBSITE_SNAPSHOT_IMPORT_BYTES = 768 * 1024;
116
116
  export const BULK_SESSION_SCHEMA = 'whoisleuth.bulk-sessions';
117
- export const BULK_SESSION_SCHEMA_VERSION = 5;
118
- export const BULK_SESSION_BROWSER_SUPPORTED_VERSIONS = Object.freeze([4, BULK_SESSION_SCHEMA_VERSION]);
117
+ export const BULK_SESSION_SCHEMA_VERSION = 6;
118
+ export const BULK_SESSION_BROWSER_SUPPORTED_VERSIONS = Object.freeze([4, 5, BULK_SESSION_SCHEMA_VERSION]);
119
119
  export const SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS = BULK_SESSION_BROWSER_SUPPORTED_VERSIONS;
120
120
  export const MAX_BULK_SESSIONS = 10;
121
121
  export const MAX_BULK_SESSION_ROWS = 2_000;
@@ -126,8 +126,8 @@ export const MAX_BULK_SESSION_ARRAY_VALUES = 100;
126
126
  export const MAX_BULK_SESSION_SOURCES = 12;
127
127
  export const MAX_BULK_PROFILE_CONTEXT_LIMITATION_LENGTH = 300;
128
128
  export const INVESTIGATION_TEMPLATE_SCHEMA = 'whoisleuth.investigation-templates';
129
- export const INVESTIGATION_TEMPLATE_VERSION = 2;
130
- export const INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS = Object.freeze([INVESTIGATION_TEMPLATE_VERSION]);
129
+ export const INVESTIGATION_TEMPLATE_VERSION = 3;
130
+ export const INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS = Object.freeze([2, INVESTIGATION_TEMPLATE_VERSION]);
131
131
  export const MAX_INVESTIGATION_TEMPLATES = 20;
132
132
  export const MAX_INVESTIGATION_TEMPLATE_STORE_BYTES = 256 * 1024;
133
133
  export const MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES = 384 * 1024;
@@ -241,9 +241,9 @@ export const CAMPAIGN_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
241
241
  export const WATCHLIST_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
242
242
  id: 'browser.watchlists', kind: 'browser_store', schema: null, currentVersion: WATCHLIST_SCHEMA_VERSION,
243
243
  supportedVersions: WATCHLIST_BROWSER_SUPPORTED_VERSIONS, acceptsUnversionedLegacy: false,
244
- futureVersionBehavior: 'preserve_without_write', migration: 'exact_current_only', writeSemantics: 'normalized_rewrite',
244
+ futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current', writeSemantics: 'normalized_rewrite',
245
245
  byteBudget: MAX_WATCHLIST_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
246
- note: 'The unchanged public version 2 envelope is the browser baseline; unversioned maps are outside the compatibility boundary.',
246
+ note: 'Public version 2 remains readable without inferred collection outcomes. Version 3 retains explicit page and favicon collection quality; unversioned maps are outside the compatibility boundary.',
247
247
  });
248
248
  export const SHORTLIST_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
249
249
  id: 'browser.shortlist', kind: 'browser_store', schema: null, currentVersion: SHORTLIST_SCHEMA_VERSION,
@@ -285,14 +285,14 @@ export const BULK_SESSION_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
285
285
  supportedVersions: BULK_SESSION_BROWSER_SUPPORTED_VERSIONS, acceptsUnversionedLegacy: false,
286
286
  futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current', writeSemantics: 'normalized_rewrite',
287
287
  byteBudget: MAX_BULK_SESSION_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
288
- note: 'Public schema 4 sessions migrate with unknown row and per-source observation times and contributing-source provenance. Schema 5 retains those bounded fields without retaining raw lookup payloads; future versions are preserved without rewrite.',
288
+ note: 'Public schemas 4 and 5 remain readable with their original observation provenance. Schema 6 retains explicit page and favicon collection quality for saved evidence handoffs; future versions are preserved without rewrite.',
289
289
  });
290
290
  export const INVESTIGATION_TEMPLATE_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
291
291
  id: 'browser.investigation-templates', kind: 'browser_store', schema: null,
292
292
  currentVersion: INVESTIGATION_TEMPLATE_VERSION, supportedVersions: INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS,
293
- acceptsUnversionedLegacy: false, futureVersionBehavior: 'preserve_without_write', migration: 'exact_current_only',
293
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'preserve_without_write', migration: 'normalize_to_current',
294
294
  writeSemantics: 'normalized_rewrite', byteBudget: MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
295
- note: 'Version 2 adds the fixed response-playbook recipe identifiers. Analyst-authored guidance remains bound to allowlisted built-in stages and cannot run code, start collection, submit evidence, or remove mandatory request gates.',
295
+ note: 'Public version 2 remains readable. Version 3 retains optional lesson-revision provenance without Case identities or note text. Guidance remains bound to built-in stages and cannot execute actions or remove mandatory request gates.',
296
296
  });
297
297
  export const BULK_REVIEW_BROWSER_COMPATIBILITY = defineSchemaCompatibility({
298
298
  id: 'browser.bulk-review', kind: 'browser_store', schema: null, currentVersion: BULK_REVIEW_SCHEMA_VERSION,
@@ -318,9 +318,9 @@ export const CAMPAIGN_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
318
318
  export const WATCHLIST_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
319
319
  id: 'export.watchlists', kind: 'export', schema: WATCHLIST_SCHEMA, currentVersion: WATCHLIST_SCHEMA_VERSION,
320
320
  supportedVersions: WATCHLIST_EXPORT_SUPPORTED_VERSIONS, acceptsUnversionedLegacy: false,
321
- futureVersionBehavior: 'reject', migration: 'exact_current_only', writeSemantics: 'non_destructive_merge',
321
+ futureVersionBehavior: 'reject', migration: 'normalize_to_current', writeSemantics: 'non_destructive_merge',
322
322
  byteBudget: MAX_WATCHLIST_IMPORT_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
323
- note: 'Non-destructive collection merge with current schema required.',
323
+ note: 'Public version 2 and current version 3 merge non-destructively. Historical records have unknown collection quality; incomplete current checks cannot erase a usable baseline.',
324
324
  });
325
325
  export const SHORTLIST_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
326
326
  id: 'export.shortlist', kind: 'export', schema: SHORTLIST_SCHEMA, currentVersion: SHORTLIST_SCHEMA_VERSION,
@@ -355,14 +355,14 @@ export const BULK_SESSION_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
355
355
  supportedVersions: SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, acceptsUnversionedLegacy: false,
356
356
  futureVersionBehavior: 'reject', migration: 'normalize_to_current', writeSemantics: 'non_destructive_merge',
357
357
  byteBudget: MAX_BULK_SESSION_STORE_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
358
- note: 'Public schema 4 sessions migrate directly to schema 5 with unknown historical row and per-source observation times and source provenance. Current exports retain source-qualified relationship evidence and bounded profile context.',
358
+ note: 'Public schemas 4 and 5 migrate without invented observation times or collection outcomes. Current exports retain source-qualified relationship evidence, bounded profile context and collection quality.',
359
359
  });
360
360
  export const INVESTIGATION_TEMPLATE_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
361
361
  id: 'export.investigation-templates', kind: 'export', schema: INVESTIGATION_TEMPLATE_SCHEMA,
362
362
  currentVersion: INVESTIGATION_TEMPLATE_VERSION, supportedVersions: INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS,
363
- acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only',
363
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'normalize_to_current',
364
364
  writeSemantics: 'non_destructive_merge', byteBudget: MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES, owner: WORKSPACE_CONTRACT_OWNER,
365
- note: 'The unchanged public version 2 writer remains the exact bounded analyst-authored guide baseline with allowlisted stage identities and request gates.',
365
+ note: 'Version 3 exports authored guidance and optional content-hash lesson provenance. Public version 2 remains readable; future versions fail closed.',
366
366
  });
367
367
  export const BULK_REVIEW_EXPORT_COMPATIBILITY = defineSchemaCompatibility({
368
368
  id: 'export.bulk-review', kind: 'export', schema: BULK_REVIEW_SCHEMA, currentVersion: BULK_REVIEW_SCHEMA_VERSION,
@@ -649,6 +649,36 @@ const WORKSPACE_LIFECYCLE_DEFINITIONS = Object.freeze([
649
649
  },
650
650
  ]);
651
651
  const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
652
+ {
653
+ id: 'workspace.browser.watchlist.v4', path: 'test/fixtures/workspace-lifecycle/browser-watchlist-v4.json',
654
+ bytes: 76, sha256: '059930ee54d1bc65ac68ef6d3a8ac8cd8e53f682fe9a857512e2b6c57e93d5fe',
655
+ schema: 'whoisleuth.browser.watchlist-store', version: 4,
656
+ },
657
+ {
658
+ id: 'workspace.portable.watchlist.v4', path: 'test/fixtures/workspace-lifecycle/portable-watchlist-v4.json',
659
+ bytes: 120, sha256: '496f1ec39add1d3fe080ad8f9f991639cfbbe659ddb1c58932ac6ff563ce32aa',
660
+ schema: 'whoisleuth.watchlists', version: 4,
661
+ },
662
+ {
663
+ id: 'workspace.browser.watchlist.v3', path: 'test/fixtures/workspace-lifecycle/browser-watchlist-v3.json',
664
+ bytes: 76, sha256: '740246c1666c332ae907a5b40364e03a4f008484fce7b1c6dce8082a6ac7b654',
665
+ schema: 'whoisleuth.browser.watchlist-store', version: 3,
666
+ },
667
+ {
668
+ id: 'workspace.portable.watchlist.v3', path: 'test/fixtures/workspace-lifecycle/portable-watchlist-v3.json',
669
+ bytes: 120, sha256: 'ad07c8a530c369b49192bd3d8dde1b17c58b737da4e3d00a834a64be6aa429bf',
670
+ schema: 'whoisleuth.watchlists', version: 3,
671
+ },
672
+ {
673
+ id: 'workspace.browser.bulk.v6', path: 'test/fixtures/workspace-lifecycle/browser-bulk-v6.json',
674
+ bytes: 77, sha256: '811ef0b05415f19dc7bd940a4786d53a6398121cbb7cd060fcd6d21ed562487b',
675
+ schema: 'whoisleuth.browser.bulk-session-store', version: 6,
676
+ },
677
+ {
678
+ id: 'workspace.portable.bulk.v6', path: 'test/fixtures/workspace-lifecycle/portable-bulk-v6.json',
679
+ bytes: 338, sha256: 'ec7f6445cf42c013163ea835d5f919213b69978124d6883c1f10d62a0868235f',
680
+ schema: 'whoisleuth.bulk-sessions', version: 6,
681
+ },
652
682
  {
653
683
  "id": "workspace.browser.brand.v6",
654
684
  "path": "test/fixtures/workspace-lifecycle/browser-brand-v6.json",
@@ -761,6 +791,14 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
761
791
  "schema": "whoisleuth.browser.investigation-template-store",
762
792
  "version": 2
763
793
  },
794
+ {
795
+ "id": "workspace.browser.template.v3",
796
+ "path": "test/fixtures/workspace-lifecycle/browser-template-v3.json",
797
+ "bytes": 88,
798
+ "sha256": "70a6dfe2c27c75d86c74a9dfe05148c3186be68191002cbea8c18816a64792c9",
799
+ "schema": "whoisleuth.browser.investigation-template-store",
800
+ "version": 3
801
+ },
764
802
  {
765
803
  "id": "workspace.browser.watchlist.v2",
766
804
  "path": "test/fixtures/workspace-lifecycle/browser-watchlist-v2.json",
@@ -889,6 +927,14 @@ const WORKSPACE_LIFECYCLE_FIXTURE_SOURCE = Object.freeze([
889
927
  "schema": "whoisleuth.investigation-templates",
890
928
  "version": 2
891
929
  },
930
+ {
931
+ "id": "workspace.portable.template.v3",
932
+ "path": "test/fixtures/workspace-lifecycle/portable-template-v3.json",
933
+ "bytes": 471,
934
+ "sha256": "6b211af44661594337731767cd212beae305d81e03e090a783633eebaa62d03e",
935
+ "schema": "whoisleuth.investigation-templates",
936
+ "version": 3
937
+ },
892
938
  {
893
939
  "id": "workspace.portable.watchlist.v2",
894
940
  "path": "test/fixtures/workspace-lifecycle/portable-watchlist-v2.json",
@@ -0,0 +1,67 @@
1
+ const STATES = new Set(['complete', 'partial', 'unavailable', 'not_collected', 'unknown']);
2
+ export function isWebCollectionQuality(value) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value))
4
+ return false;
5
+ const prototype = Object.getPrototypeOf(value);
6
+ if (prototype !== Object.prototype && prototype !== null)
7
+ return false;
8
+ const record = value;
9
+ const keys = Object.keys(record);
10
+ if (keys.length !== 4 || keys.some(key => !['version', 'page', 'favicon', 'combined'].includes(key)
11
+ || !Object.hasOwn(Object.getOwnPropertyDescriptor(record, key) ?? {}, 'value')))
12
+ return false;
13
+ return record.version === 1
14
+ && typeof record.page === 'string' && STATES.has(record.page)
15
+ && typeof record.favicon === 'string' && STATES.has(record.favicon)
16
+ && typeof record.combined === 'string' && STATES.has(record.combined)
17
+ && (record.combined !== 'complete' || (record.page === 'complete' && record.favicon === 'complete'));
18
+ }
19
+ export function normalizeWebCollectionQuality(value, depth) {
20
+ if (value === undefined)
21
+ return undefined;
22
+ if (!isWebCollectionQuality(value))
23
+ throw new TypeError('Unsupported or invalid web collection quality.');
24
+ if (depth === 'fast')
25
+ return capturedWebCollectionQuality('not_collected', 'not_collected');
26
+ return { version: 1, page: value.page, favicon: value.favicon, combined: value.combined };
27
+ }
28
+ export function capturedWebCollectionQuality(page, favicon) {
29
+ const combined = page === 'complete' && favicon === 'complete' ? 'complete'
30
+ : page === favicon ? page : 'partial';
31
+ return { version: 1, page, favicon, combined };
32
+ }
33
+ export function webCollectionQualityForCapture(value, depth) {
34
+ if (depth !== 'deep')
35
+ return capturedWebCollectionQuality('not_collected', 'not_collected');
36
+ return normalizeWebCollectionQuality(value) ?? capturedWebCollectionQuality('unknown', 'unknown');
37
+ }
38
+ const PAGE_FIELDS = new Set(['activityStatus', 'pageTitle', 'hasPasswordField', 'hasExternalFormAction', 'phishingLanguageMatch', 'pageBaselineMatch']);
39
+ const FAVICON_FIELDS = new Set(['faviconHash', 'faviconPHash', 'faviconMatch', 'faviconNearMatch']);
40
+ const COMBINED_FIELDS = new Set(['riskScore', 'riskFactors', 'riskModelVersion', 'opportunityScore', 'opportunityFactors', 'reusesOfficialAssets']);
41
+ /** Missing historical quality never proves that a false/null value was observed. */
42
+ export function webCollectionAllowsComparison(field, quality, depth) {
43
+ if (PAGE_FIELDS.has(field))
44
+ return quality?.page === 'complete';
45
+ if (FAVICON_FIELDS.has(field))
46
+ return quality?.favicon === 'complete';
47
+ if (COMBINED_FIELDS.has(field) && depth === 'deep')
48
+ return quality?.combined === 'complete';
49
+ return true;
50
+ }
51
+ /** Retain the measured score while qualifying incomplete inputs at presentation. */
52
+ export function webCollectionScoreLimitation(quality, depth) {
53
+ if (depth === 'unknown')
54
+ return 'Collection depth is unknown; score is not comparable.';
55
+ return webCollectionAllowsComparison('riskScore', quality, depth)
56
+ ? null : 'Incomplete or unknown web collection; score is not comparable.';
57
+ }
58
+ /** An incomplete current attempt cannot erase the last comparable baseline. */
59
+ export function mergeWebCollectionQuality(previous, current) {
60
+ if (!previous && !current)
61
+ return undefined;
62
+ return { version: 1,
63
+ page: current?.page === 'complete' ? 'complete' : previous?.page ?? current?.page ?? 'unknown',
64
+ favicon: current?.favicon === 'complete' ? 'complete' : previous?.favicon ?? current?.favicon ?? 'unknown',
65
+ combined: current?.combined === 'complete' ? 'complete' : previous?.combined ?? current?.combined ?? 'unknown',
66
+ };
67
+ }
@@ -0,0 +1,72 @@
1
+ import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE } from "../analysis/lookup-network-evidence-bounds.mjs";
2
+ import { addressValue, canonicalIpAddress } from "../contracts/ip-address.mjs";
3
+ /** Fixed values retained in Observation v1's existing diagnostic detail slot. */
4
+ export const DNS_QUERY_OUTCOMES = Object.freeze({
5
+ records: { status: 'success', label: 'Records returned' },
6
+ empty_answer: { status: 'not_found', label: 'No records returned' },
7
+ no_data: { status: 'not_found', label: 'No data for this record type (NODATA)' },
8
+ name_not_found: { status: 'not_found', label: 'Name not found by resolver' },
9
+ timeout: { status: 'error', label: 'DNS query timed out' },
10
+ server_failure: { status: 'error', label: 'DNS server failed to answer (SERVFAIL)' },
11
+ refused: { status: 'error', label: 'DNS query refused' },
12
+ invalid_response: { status: 'error', label: 'DNS answer could not be read completely' },
13
+ error: { status: 'error', label: 'DNS query failed' },
14
+ skipped: { status: 'skipped', label: 'Not queried' },
15
+ });
16
+ function record(value) {
17
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
18
+ ? value : {};
19
+ }
20
+ /** Legacy not_found diagnostics do not identify which negative answer occurred. */
21
+ export function readDnsQueryOutcome(value) {
22
+ const diagnostic = record(value);
23
+ const detail = diagnostic.detail;
24
+ return typeof detail === 'string' && Object.hasOwn(DNS_QUERY_OUTCOMES, detail)
25
+ && DNS_QUERY_OUTCOMES[detail].status === diagnostic.status
26
+ ? detail : null;
27
+ }
28
+ export function dnsQueryFailureOutcome(code) {
29
+ switch (code) {
30
+ case 'ENODATA': return 'no_data';
31
+ case 'ENOTFOUND': return 'name_not_found';
32
+ case 'ETIMEOUT': return 'timeout';
33
+ case 'ESERVFAIL': return 'server_failure';
34
+ case 'EREFUSED': return 'refused';
35
+ case 'EBADRESP': return 'invalid_response';
36
+ default: return 'error';
37
+ }
38
+ }
39
+ /** One family, one source state. Unrelated mail/delegation failures cannot erase it. */
40
+ export function dnsAddressFamilyEvidence(dnsValue, family) {
41
+ const dns = record(dnsValue);
42
+ const diagnostic = record(record(dns.diagnostics)[family]);
43
+ const raw = record(dns.records)[family];
44
+ const candidates = Array.isArray(raw) ? raw.slice(0, MAX_LOOKUP_DNS_RECORDS_PER_TYPE) : [];
45
+ const addresses = candidates.flatMap(value => {
46
+ const address = canonicalIpAddress(value);
47
+ return address && addressValue(address)?.family === (family === 'a' ? 4 : 6) ? [address] : [];
48
+ });
49
+ const malformed = raw !== undefined && !Array.isArray(raw)
50
+ || candidates.length !== addresses.length;
51
+ const truncated = diagnostic.truncated === true
52
+ || typeof diagnostic.discarded === 'number' && diagnostic.discarded > 0
53
+ || Array.isArray(raw) && raw.length > candidates.length;
54
+ const outcome = readDnsQueryOutcome(diagnostic);
55
+ const inconsistent = outcome === 'records' && !addresses.length
56
+ || outcome !== null && outcome !== 'records' && addresses.length > 0;
57
+ const complete = !malformed && !truncated && !inconsistent && Array.isArray(raw)
58
+ && outcome !== null && DNS_QUERY_OUTCOMES[outcome].status !== 'error' && outcome !== 'skipped';
59
+ const state = malformed || inconsistent ? 'error'
60
+ : truncated ? 'partial'
61
+ : outcome ? DNS_QUERY_OUTCOMES[outcome].status
62
+ : diagnostic.status === 'error' ? 'error'
63
+ : diagnostic.status === 'skipped' || dns.status === 'skipped' ? 'skipped' : 'unknown';
64
+ const value = addresses.length ? [...new Set(addresses)].sort().join(' · ')
65
+ : malformed || inconsistent ? DNS_QUERY_OUTCOMES.invalid_response.label
66
+ : truncated ? 'DNS answer incomplete'
67
+ : outcome ? DNS_QUERY_OUTCOMES[outcome].label
68
+ : state === 'error' ? DNS_QUERY_OUTCOMES.error.label
69
+ : state === 'skipped' ? DNS_QUERY_OUTCOMES.skipped.label
70
+ : diagnostic.status === 'not_found' ? 'Negative DNS outcome not recorded' : null;
71
+ return { outcome, value, state, complete, truncated };
72
+ }
@@ -0,0 +1,18 @@
1
+ // Single plain recipient admission for manually opened drafts, not delivery validation.
2
+ const SINGLE_ADDRESS = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/u;
3
+ export function emailRecipient(value) {
4
+ if (typeof value !== 'string' || value.length > 320 || /[\u0000-\u001f\u007f]/u.test(value))
5
+ return null;
6
+ const address = value.trim();
7
+ return SINGLE_ADDRESS.test(address) ? address : null;
8
+ }
9
+ /** Encode the recipient independently; upstream text never becomes a header. */
10
+ export function recipientMailto(value, headers) {
11
+ const address = emailRecipient(value);
12
+ if (!address)
13
+ return null;
14
+ const separator = address.indexOf('@');
15
+ const recipient = `${encodeURIComponent(address.slice(0, separator))}@${encodeURIComponent(address.slice(separator + 1))}`;
16
+ const query = headers ? `?subject=${encodeURIComponent(headers.subject)}&body=${encodeURIComponent(headers.body)}` : '';
17
+ return `mailto:${recipient}${query}`;
18
+ }
@@ -5,6 +5,10 @@ import { MAX_OUTBOUND_HTTP_URL_CHARACTERS } from "../contracts/http-url.mjs";
5
5
  export const MAX_LOOKUP_INPUT_CHARACTERS = 2 * 1024 * 1024;
6
6
  const URL_SCHEME = /^[a-z][a-z\d+.-]*:\/\//iu;
7
7
  const CONTROL = /[\u0000-\u001f\u007f]/u;
8
+ /** Lexical recognition only; protocol, credentials and target admission remain separate. */
9
+ export function hasLookupUrlScheme(value) {
10
+ return URL_SCHEME.test(value);
11
+ }
8
12
  export function validWebObservationMode(value) {
9
13
  return value === undefined || value === 'selected_url';
10
14
  }
@@ -76,7 +80,7 @@ export function prepareLookupCollectionTarget(value) {
76
80
  const raw = value.trim();
77
81
  if (!raw)
78
82
  throw new TypeError('Enter a domain, IP address or ASN.');
79
- const absolute = URL_SCHEME.test(raw);
83
+ const absolute = hasLookupUrlScheme(raw);
80
84
  // An unbracketed IPv6 literal is not a URL with a port. Use the same URL
81
85
  // parser to admit its syntax, then leave address safety and ASN bounds to
82
86
  // the classifier; malformed colon-separated text must not become a request.
@@ -106,6 +106,8 @@ const REGISTERED_DIAGNOSTIC_KEYS = [
106
106
  'queriedAuthorityCount',
107
107
  'redirectCount',
108
108
  'referencesExamined',
109
+ 'referenceSignatureTimedOut',
110
+ 'referenceSignatureUnavailable',
109
111
  'rejectedRows',
110
112
  'relationshipTagsExamined',
111
113
  'relationshipUrlsDiscarded',