@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -0,0 +1,384 @@
1
+ // Compact provenance envelope for network-derived evidence. Type-specific
2
+ // payloads remain beside this object; the envelope only standardises source
3
+ // health, timing, completeness, truncation, and bounded limitations.
4
+ const OBSERVATION_VERSION = 1;
5
+ const STATUSES = new Set([
6
+ 'success',
7
+ 'partial',
8
+ 'not_found',
9
+ 'skipped',
10
+ 'error',
11
+ 'unsupported',
12
+ 'not_applicable',
13
+ ]);
14
+ const SCAN_MODES = new Set(['fast', 'deep']);
15
+ const MAX_OBSERVATION_LIMITATIONS = 10;
16
+ const MAX_OBSERVATION_LIMITATION_LENGTH = 300;
17
+ const MAX_OBSERVATION_DIAGNOSTICS = 20;
18
+ const MAX_DIAGNOSTICS = MAX_OBSERVATION_DIAGNOSTICS;
19
+ const MAX_DIAGNOSTIC_KEY = 40;
20
+ const MAX_DIAGNOSTIC_STRING = 240;
21
+ const MAX_DURATION_MS = 120_000;
22
+ const MAX_OBSERVATION_INPUT_LIMITATIONS = MAX_OBSERVATION_LIMITATIONS * 4;
23
+ const MAX_DIAGNOSTIC_DEPTH = 4;
24
+ const MAX_DIAGNOSTIC_VALUES_PER_ENTRY = 32;
25
+ const OBSERVATION_INPUT_FIELDS = [
26
+ 'version',
27
+ 'status',
28
+ 'observedAt',
29
+ 'scanMode',
30
+ 'source',
31
+ 'durationMs',
32
+ 'complete',
33
+ 'truncated',
34
+ 'limitations',
35
+ 'diagnostics',
36
+ ];
37
+ // Observation v1 diagnostics use a closed, source-owned vocabulary. Reading
38
+ // only these descriptors keeps normalisation independent of an untrusted
39
+ // object's total key count and preserves the deployed producer fields.
40
+ const REGISTERED_DIAGNOSTIC_KEYS = [
41
+ 'a',
42
+ 'aaaa',
43
+ 'addressSource',
44
+ 'advisoryMatches',
45
+ 'arrayItemsExamined',
46
+ 'attemptCount',
47
+ 'authorityCount',
48
+ 'caa',
49
+ 'caa_policy',
50
+ 'catalogComponents',
51
+ 'certificateGroups',
52
+ 'certificateGroupsTruncated',
53
+ 'certificateRows',
54
+ 'charactersExamined',
55
+ 'cidrCount',
56
+ 'classifiedInputs',
57
+ 'cname',
58
+ 'connectionAttempts',
59
+ 'delegation',
60
+ 'discarded',
61
+ 'discardedAuthorityRecordValueCount',
62
+ 'discardedFields',
63
+ 'discardedProperties',
64
+ 'discardedUrls',
65
+ 'discarded_attributes',
66
+ 'discarded_entities',
67
+ 'discarded_metadata',
68
+ 'discarded_relationships',
69
+ 'dmarc',
70
+ 'documentsParsed',
71
+ 'entities',
72
+ 'error',
73
+ 'externalScriptsSkipped',
74
+ 'findings',
75
+ 'formsObserved',
76
+ 'generatorEvaluated',
77
+ 'htmlEvaluated',
78
+ 'httpStatus',
79
+ 'https',
80
+ 'ignoredCount',
81
+ 'indicatorsObserved',
82
+ 'inlineCharactersExamined',
83
+ 'inlineScriptsExamined',
84
+ 'inlineSignatureCharactersExamined',
85
+ 'inlineSignatureTimedOut',
86
+ 'inlineSignatureUnavailable',
87
+ 'inputsObserved',
88
+ 'knownExploitedMatches',
89
+ 'lameAuthorityCount',
90
+ 'malformedCount',
91
+ 'malformedScripts',
92
+ 'matches',
93
+ 'mx',
94
+ 'namesExamined',
95
+ 'ns',
96
+ 'objectsExamined',
97
+ 'observed',
98
+ 'observedAbsence',
99
+ 'partialAuthorityCount',
100
+ 'passiveHeadersEvaluated',
101
+ 'potentialExposure',
102
+ 'ptr',
103
+ 'queriedAddressCount',
104
+ 'redirectCount',
105
+ 'referencesExamined',
106
+ 'rejectedRows',
107
+ 'relationshipTagsExamined',
108
+ 'relationshipUrlsDiscarded',
109
+ 'relationships',
110
+ 'requestCount',
111
+ 'resolvedAddressCount',
112
+ 'resourceOriginsEvaluated',
113
+ 'rolesObserved',
114
+ 'scriptElementsExamined',
115
+ 'scriptsExamined',
116
+ 'scriptsObserved',
117
+ 'serverEvaluated',
118
+ 'soa',
119
+ 'spf',
120
+ 'successfulAuthorityCount',
121
+ 'tagLimitReached',
122
+ 'tagsExamined',
123
+ 'trackingIdentifiersTruncated',
124
+ 'tree',
125
+ 'truncatedAuthorityRecordSetCount',
126
+ 'truncatedNames',
127
+ 'truncatedSameAsLists',
128
+ 'truncatedTypeLists',
129
+ 'unavailable',
130
+ 'unclassifiedActions',
131
+ 'unreachableAuthorityCount',
132
+ 'workTruncated',
133
+ ];
134
+ const ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})$/iu;
135
+ const LEGACY_ISO_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})?$/iu;
136
+ const CT_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})?$/iu;
137
+ function isObservationStatus(value) {
138
+ return typeof value === 'string' && STATUSES.has(value);
139
+ }
140
+ function isScanMode(value) {
141
+ return typeof value === 'string' && SCAN_MODES.has(value);
142
+ }
143
+ function safeString(value, maxLength) {
144
+ if (typeof value !== 'string' || value.length > maxLength * 4)
145
+ return null;
146
+ const normalised = value.trim().replace(/[\u0000-\u001f\u007f]/g, ' ');
147
+ return normalised ? normalised.slice(0, maxLength) : null;
148
+ }
149
+ function ownDataValue(value, key) {
150
+ try {
151
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
152
+ return descriptor && 'value' in descriptor ? descriptor.value : undefined;
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
158
+ function snapshotObservationInput(value) {
159
+ const snapshot = {};
160
+ for (const field of OBSERVATION_INPUT_FIELDS) {
161
+ const fieldValue = ownDataValue(value, field);
162
+ if (fieldValue === undefined)
163
+ continue;
164
+ Object.defineProperty(snapshot, field, {
165
+ configurable: true,
166
+ enumerable: true,
167
+ value: fieldValue,
168
+ writable: true,
169
+ });
170
+ }
171
+ return snapshot;
172
+ }
173
+ function safelyIsArray(value) {
174
+ try {
175
+ return Array.isArray(value);
176
+ }
177
+ catch {
178
+ return true;
179
+ }
180
+ }
181
+ function boundedArrayDataValues(value, maximum) {
182
+ if (!safelyIsArray(value))
183
+ return [];
184
+ const output = [];
185
+ const rawLength = ownDataValue(value, 'length');
186
+ const length = typeof rawLength === 'number' && Number.isSafeInteger(rawLength) && rawLength >= 0
187
+ ? Math.min(rawLength, maximum)
188
+ : 0;
189
+ for (let index = 0; index < length; index += 1) {
190
+ const item = ownDataValue(value, String(index));
191
+ if (item !== undefined)
192
+ output.push(item);
193
+ }
194
+ return output;
195
+ }
196
+ function validCalendarParts(parts) {
197
+ const year = Number(parts[1]);
198
+ const month = Number(parts[2]);
199
+ const day = Number(parts[3]);
200
+ const hour = Number(parts[4]);
201
+ const minute = Number(parts[5]);
202
+ const second = Number(parts[6]);
203
+ if (year < 1 || month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59)
204
+ return false;
205
+ const date = new Date(0);
206
+ date.setUTCFullYear(year, month - 1, day);
207
+ date.setUTCHours(hour, minute, second, 0);
208
+ return date.getUTCFullYear() === year
209
+ && date.getUTCMonth() === month - 1
210
+ && date.getUTCDate() === day
211
+ && date.getUTCHours() === hour
212
+ && date.getUTCMinutes() === minute
213
+ && date.getUTCSeconds() === second;
214
+ }
215
+ function canonicalTimestamp(value, expression, assignUtcWhenMissing) {
216
+ if (typeof value !== 'string' || value.length === 0 || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value))
217
+ return null;
218
+ const match = expression.exec(value);
219
+ if (!match || !validCalendarParts(match))
220
+ return null;
221
+ const zone = match[8] || (assignUtcWhenMissing ? 'Z' : '');
222
+ if (!zone)
223
+ return null;
224
+ if (zone !== 'Z' && zone.toUpperCase() !== 'Z') {
225
+ const hours = Number(zone.slice(1, 3));
226
+ const minutes = Number(zone.slice(4, 6));
227
+ if (hours > 23 || minutes > 59)
228
+ return null;
229
+ }
230
+ const normalised = `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}${match[7] || ''}${zone.toUpperCase() === 'Z' ? 'Z' : zone}`;
231
+ const timestamp = Date.parse(normalised);
232
+ if (!Number.isFinite(timestamp))
233
+ return null;
234
+ const date = new Date(timestamp);
235
+ const utcYear = date.getUTCFullYear();
236
+ return utcYear >= 1 && utcYear <= 9_999 ? date.toISOString() : null;
237
+ }
238
+ function normalizeExplicitIsoTimestamp(value) {
239
+ return canonicalTimestamp(value, ISO_DATE_TIME_RE, false);
240
+ }
241
+ // Frozen legacy schemas accepted zone-less ISO date-times. Preserve that
242
+ // migration path deterministically by assigning UTC instead of consulting the
243
+ // host timezone. Current schemas must use normalizeExplicitIsoTimestamp.
244
+ function normalizeLegacyIsoTimestamp(value) {
245
+ return canonicalTimestamp(value, LEGACY_ISO_DATE_TIME_RE, true);
246
+ }
247
+ function normalizeCtTimestamp(value) {
248
+ return canonicalTimestamp(value, CT_DATE_TIME_RE, true);
249
+ }
250
+ function isoTimestamp(value) {
251
+ return normalizeExplicitIsoTimestamp(value);
252
+ }
253
+ function normalizeDiagnosticValue(value, budget, depth = 0) {
254
+ budget.remaining -= 1;
255
+ if (budget.remaining < 0) {
256
+ budget.exhausted = true;
257
+ return null;
258
+ }
259
+ if (depth > MAX_DIAGNOSTIC_DEPTH)
260
+ return null;
261
+ if (typeof value === 'boolean')
262
+ return value;
263
+ if (typeof value === 'number')
264
+ return Number.isFinite(value) ? value : null;
265
+ if (typeof value === 'string')
266
+ return safeString(value, MAX_DIAGNOSTIC_STRING);
267
+ if (!value || typeof value !== 'object' || safelyIsArray(value))
268
+ return null;
269
+ const objectValue = value;
270
+ if (budget.active.has(objectValue))
271
+ return null;
272
+ const record = value;
273
+ const output = {};
274
+ budget.active.add(objectValue);
275
+ try {
276
+ for (const field of ['status', 'error', 'detail', 'truncated', 'discarded', 'count']) {
277
+ const fieldValue = ownDataValue(record, field);
278
+ if (fieldValue === undefined)
279
+ continue;
280
+ const normalised = normalizeDiagnosticValue(fieldValue, budget, depth + 1);
281
+ if (normalised !== null) {
282
+ Object.defineProperty(output, field, {
283
+ configurable: true,
284
+ enumerable: true,
285
+ value: normalised,
286
+ writable: true,
287
+ });
288
+ }
289
+ }
290
+ }
291
+ finally {
292
+ budget.active.delete(objectValue);
293
+ }
294
+ return Object.keys(output).length ? output : null;
295
+ }
296
+ function normalizeDiagnostics(value) {
297
+ if (!value || typeof value !== 'object' || safelyIsArray(value))
298
+ return {};
299
+ const record = value;
300
+ const output = {};
301
+ let retained = 0;
302
+ for (const key of REGISTERED_DIAGNOSTIC_KEYS) {
303
+ if (retained >= MAX_DIAGNOSTICS)
304
+ break;
305
+ if (key.length > MAX_DIAGNOSTIC_KEY)
306
+ continue;
307
+ const entryValue = ownDataValue(record, key);
308
+ if (entryValue === undefined)
309
+ continue;
310
+ const budget = {
311
+ remaining: MAX_DIAGNOSTIC_VALUES_PER_ENTRY,
312
+ active: new Set(),
313
+ exhausted: false,
314
+ };
315
+ const normalised = normalizeDiagnosticValue(entryValue, budget);
316
+ if (normalised !== null && !budget.exhausted) {
317
+ Object.defineProperty(output, key, {
318
+ configurable: true,
319
+ enumerable: true,
320
+ value: normalised,
321
+ writable: true,
322
+ });
323
+ retained += 1;
324
+ }
325
+ }
326
+ return output;
327
+ }
328
+ function createObservationFromSnapshot(record) {
329
+ const rawStatus = record.status;
330
+ const rawSource = record.source;
331
+ const rawObservedAt = record.observedAt;
332
+ const rawDuration = record.durationMs;
333
+ const rawScanMode = record.scanMode;
334
+ const status = isObservationStatus(rawStatus) ? rawStatus : 'error';
335
+ const source = safeString(rawSource, 40) || 'unknown';
336
+ const observedAt = isoTimestamp(rawObservedAt) || new Date().toISOString();
337
+ const duration = typeof rawDuration === 'number' ? rawDuration : Number.NaN;
338
+ const limitations = [...new Set(boundedArrayDataValues(record.limitations, MAX_OBSERVATION_INPUT_LIMITATIONS)
339
+ .map((item) => safeString(item, MAX_OBSERVATION_LIMITATION_LENGTH))
340
+ .filter((item) => item !== null))]
341
+ .slice(0, MAX_OBSERVATION_LIMITATIONS);
342
+ return {
343
+ version: OBSERVATION_VERSION,
344
+ status,
345
+ observedAt,
346
+ scanMode: isScanMode(rawScanMode) ? rawScanMode : null,
347
+ source,
348
+ durationMs: Number.isFinite(duration) ? Math.max(0, Math.min(MAX_DURATION_MS, Math.round(duration))) : null,
349
+ complete: record.complete === true,
350
+ truncated: record.truncated === true,
351
+ limitations,
352
+ diagnostics: normalizeDiagnostics(record.diagnostics),
353
+ };
354
+ }
355
+ function createObservation(input = {}) {
356
+ const record = input && typeof input === 'object' && !safelyIsArray(input)
357
+ ? snapshotObservationInput(input)
358
+ : {};
359
+ return createObservationFromSnapshot(record);
360
+ }
361
+ function readObservationEnvelope(value) {
362
+ if (value === undefined || value === null)
363
+ return { state: 'absent', observation: null };
364
+ if (!value || typeof value !== 'object' || safelyIsArray(value))
365
+ return { state: 'invalid', observation: null };
366
+ const record = snapshotObservationInput(value);
367
+ const version = record.version;
368
+ const status = record.status;
369
+ const source = record.source;
370
+ const observedAt = record.observedAt;
371
+ if (typeof version === 'number'
372
+ && Number.isInteger(version)
373
+ && version > OBSERVATION_VERSION) {
374
+ return { state: 'unsupported', observation: null };
375
+ }
376
+ if (version !== OBSERVATION_VERSION
377
+ || !isObservationStatus(status)
378
+ || !safeString(source, 40)
379
+ || !isoTimestamp(observedAt)) {
380
+ return { state: 'invalid', observation: null };
381
+ }
382
+ return { state: 'supported', observation: createObservationFromSnapshot(record) };
383
+ }
384
+ export { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, createObservation, normalizeCtTimestamp, normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp, readObservationEnvelope, };
@@ -1,10 +1,8 @@
1
- import { normalizeDomain, openOrCreateCase, updateCase, } from "./case-model.js";
2
- export const EXTERNAL_FINDINGS_SCHEMA = 'whoisleuth.external-findings';
3
- export const EXTERNAL_FINDINGS_VERSION = 3;
4
- export const MAX_EXTERNAL_FINDINGS_IMPORT_BYTES = 384 * 1024;
5
- export const MAX_EXTERNAL_FINDINGS = 100;
6
- export const MAX_EXTERNAL_FINDINGS_PER_DOMAIN = 20;
7
- export const MAX_EXTERNAL_FINDING_DOMAINS = 25;
1
+ import { normalizeDomain, openOrCreateCase, updateCase, } from "../cases/case-model.mjs";
2
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
3
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, } from "../contracts/external-observation-interchange.mjs";
4
+ import { hasUnsafeRetainedText } from "./retained-text.mjs";
5
+ export { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, };
8
6
  export const EXTERNAL_FINDING_CATEGORIES = [
9
7
  'certificate',
10
8
  'dns',
@@ -17,7 +15,6 @@ export const EXTERNAL_FINDING_CATEGORIES = [
17
15
  ];
18
16
  const CATEGORIES = new Set(EXTERNAL_FINDING_CATEGORIES);
19
17
  const COMPLETENESS = new Set(['complete', 'inconclusive', 'partial', 'unknown']);
20
- const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
21
18
  const ROOT_KEYS = new Set(['schema', 'schemaVersion', 'source', 'findings']);
22
19
  const SOURCE_KEYS = new Set(['name', 'reference', 'collectedAt']);
23
20
  const FINDING_KEYS = new Set([
@@ -38,12 +35,18 @@ const STRUCTURED_OBSERVATION_KEYS = new Set([
38
35
  'value',
39
36
  'issuer',
40
37
  'notAfter',
38
+ 'eventId',
39
+ 'logId',
40
+ 'certificateSha256',
41
+ 'dnsNameCount',
42
+ 'namesComplete',
41
43
  ]);
42
44
  const STRUCTURED_OBSERVATION_SCHEMAS = new Set([
43
45
  'whoisleuth.domain-observation-rows',
44
46
  'whoisleuth.dns-observation-rows',
45
47
  'whoisleuth.certificate-observation-rows',
46
48
  ]);
49
+ const SHA256_RE = /^[a-f0-9]{64}$/u;
47
50
  function record(value) {
48
51
  return value && typeof value === 'object' && !Array.isArray(value)
49
52
  ? value
@@ -56,8 +59,8 @@ function requiredText(value, maximum, label) {
56
59
  if (typeof value !== 'string'
57
60
  || !value.trim()
58
61
  || value.length > maximum
59
- || CONTROL_RE.test(value)) {
60
- throw new Error(`${label} must be non-empty, bounded text without control characters.`);
62
+ || hasUnsafeRetainedText(value)) {
63
+ throw new Error(`${label} must be non-empty, bounded text without unsafe control characters or formatting controls.`);
61
64
  }
62
65
  return value.trim();
63
66
  }
@@ -70,10 +73,10 @@ function iso(value, label, optional = false) {
70
73
  if (optional && (value === undefined || value === null || value === ''))
71
74
  return null;
72
75
  const text = requiredText(value, 64, label);
73
- const parsed = Date.parse(text);
74
- if (!Number.isFinite(parsed))
75
- throw new Error(`${label} must be a valid date and time.`);
76
- return new Date(parsed).toISOString();
76
+ const normalized = normalizeExplicitIsoTimestamp(text);
77
+ if (!normalized)
78
+ throw new Error(`${label} must be a valid date and time with an explicit timezone.`);
79
+ return normalized;
77
80
  }
78
81
  function limitations(value, index) {
79
82
  if (value === undefined)
@@ -104,9 +107,44 @@ function structuredObservation(value, index) {
104
107
  const issuer = optionalText(item.issuer, 160, `Finding ${index + 1} certificate issuer`);
105
108
  const notAfter = iso(item.notAfter, `Finding ${index + 1} certificate expiry`, true);
106
109
  const certificateSchema = item.sourceSchema === 'whoisleuth.certificate-observation-rows';
110
+ if (certificateSchema && (field === 'certificateSha256' || field === 'fingerprintSha256') && !SHA256_RE.test(observationValue.toLowerCase())) {
111
+ throw new Error(`Finding ${index + 1} certificate observation value must be a SHA-256 hexadecimal digest.`);
112
+ }
107
113
  if (!certificateSchema && (issuer !== null || notAfter !== null)) {
108
114
  throw new Error(`Finding ${index + 1} non-certificate observation cannot declare certificate metadata.`);
109
115
  }
116
+ const declaresEventMetadata = ['eventId', 'logId', 'certificateSha256', 'dnsNameCount', 'namesComplete']
117
+ .some((key) => item[key] !== undefined && item[key] !== null);
118
+ let eventId = null;
119
+ let logId = null;
120
+ let certificateSha256 = null;
121
+ let dnsNameCount = null;
122
+ let namesComplete = null;
123
+ if (declaresEventMetadata) {
124
+ if (!certificateSchema) {
125
+ throw new Error(`Finding ${index + 1} certificate-event metadata requires external-findings schema version 4.`);
126
+ }
127
+ eventId = requiredText(item.eventId, 64, `Finding ${index + 1} certificate event id`);
128
+ if (!/^[A-Za-z0-9_-]{1,64}$/u.test(eventId)) {
129
+ throw new Error(`Finding ${index + 1} certificate event id is invalid.`);
130
+ }
131
+ logId = requiredText(item.logId, 200, `Finding ${index + 1} certificate log id`);
132
+ certificateSha256 = requiredText(item.certificateSha256, 64, `Finding ${index + 1} certificate digest`).toLowerCase();
133
+ if (!SHA256_RE.test(certificateSha256)) {
134
+ throw new Error(`Finding ${index + 1} certificate event digest must be a SHA-256 hexadecimal digest.`);
135
+ }
136
+ if (certificateSha256 !== observationValue.toLowerCase()) {
137
+ throw new Error(`Finding ${index + 1} certificate event digest must match the structured observation value.`);
138
+ }
139
+ if (!Number.isSafeInteger(item.dnsNameCount) || Number(item.dnsNameCount) < 1 || Number(item.dnsNameCount) > 100) {
140
+ throw new Error(`Finding ${index + 1} certificate DNS name count is invalid.`);
141
+ }
142
+ dnsNameCount = Number(item.dnsNameCount);
143
+ if (typeof item.namesComplete !== 'boolean') {
144
+ throw new Error(`Finding ${index + 1} certificate name completeness is invalid.`);
145
+ }
146
+ namesComplete = item.namesComplete;
147
+ }
110
148
  return {
111
149
  sourceSchema: item.sourceSchema,
112
150
  sourceVersion: 1,
@@ -114,6 +152,11 @@ function structuredObservation(value, index) {
114
152
  value: observationValue,
115
153
  issuer,
116
154
  notAfter,
155
+ eventId,
156
+ logId,
157
+ certificateSha256,
158
+ dnsNameCount,
159
+ namesComplete,
117
160
  };
118
161
  }
119
162
  function findingKey(finding, sourceName) {
@@ -125,6 +168,7 @@ function findingKey(finding, sourceName) {
125
168
  finding.observedAt,
126
169
  finding.completeness,
127
170
  sourceName,
171
+ finding.structuredObservation?.eventId ?? '',
128
172
  ].join('\u0000');
129
173
  }
130
174
  export function parseExternalFindingsDocument(value) {
@@ -133,8 +177,8 @@ export function parseExternalFindingsDocument(value) {
133
177
  throw new Error('External findings must use the documented object shape without additional top-level fields.');
134
178
  }
135
179
  if (root.schema !== EXTERNAL_FINDINGS_SCHEMA
136
- || ![1, 2, EXTERNAL_FINDINGS_VERSION].includes(Number(root.schemaVersion))) {
137
- throw new Error(`External findings must use ${EXTERNAL_FINDINGS_SCHEMA} schema version 1, 2, or ${EXTERNAL_FINDINGS_VERSION}.`);
180
+ || root.schemaVersion !== EXTERNAL_FINDINGS_VERSION) {
181
+ throw new Error(`External findings must use ${EXTERNAL_FINDINGS_SCHEMA} schema version ${EXTERNAL_FINDINGS_VERSION}.`);
138
182
  }
139
183
  const sourceValue = record(root.source);
140
184
  if (!sourceValue || !hasOnlyKeys(sourceValue, SOURCE_KEYS)) {
@@ -166,9 +210,7 @@ export function parseExternalFindingsDocument(value) {
166
210
  if (typeof item.completeness !== 'string' || !COMPLETENESS.has(item.completeness)) {
167
211
  throw new Error(`Finding ${index + 1} completeness is unsupported.`);
168
212
  }
169
- const evidenceClass = item.evidenceClass === undefined && root.schemaVersion === 1
170
- ? 'provider_report'
171
- : item.evidenceClass;
213
+ const evidenceClass = item.evidenceClass;
172
214
  if (evidenceClass !== 'deployment_observation' && evidenceClass !== 'provider_report') {
173
215
  throw new Error(`Finding ${index + 1} evidence class is unsupported.`);
174
216
  }
@@ -206,6 +248,13 @@ export function parseExternalFindingsDocument(value) {
206
248
  findings: normalized,
207
249
  };
208
250
  }
251
+ export function serializeExternalFindingsDocument(value) {
252
+ const serialised = JSON.stringify(value, null, 2);
253
+ if (typeof serialised !== 'string') {
254
+ throw new TypeError('External findings must be JSON-serialisable.');
255
+ }
256
+ return `${serialised}\n`;
257
+ }
209
258
  function pinValue(finding) {
210
259
  return finding.reference
211
260
  ? `${finding.summary} Reference: ${finding.reference}`
@@ -229,12 +278,28 @@ function existingPinKey(recordValue, finding, sourceName) {
229
278
  && (!observation || (pin.field === observation.field
230
279
  && pin.sourceSchema?.collection === 'external_observations'
231
280
  && pin.sourceSchema.schema === observation.sourceSchema
232
- && pin.sourceSchema.version === observation.sourceVersion)))) ? findingKey(finding, sourceName) : '';
281
+ && pin.sourceSchema.version === observation.sourceVersion
282
+ && (observation.eventId === null || pin.certificateObservation?.eventId === observation.eventId))))) ? findingKey(finding, sourceName) : '';
233
283
  }
234
284
  function structuredPinFields(finding) {
235
285
  const observation = finding.structuredObservation;
236
286
  if (!observation)
237
287
  return {};
288
+ const certificateObservation = observation.eventId
289
+ && observation.logId
290
+ && observation.certificateSha256
291
+ && observation.dnsNameCount !== null
292
+ && observation.namesComplete !== null
293
+ ? {
294
+ eventId: observation.eventId,
295
+ logId: observation.logId,
296
+ certificateSha256: observation.certificateSha256,
297
+ issuer: observation.issuer,
298
+ notAfter: observation.notAfter,
299
+ dnsNameCount: observation.dnsNameCount,
300
+ namesComplete: observation.namesComplete,
301
+ }
302
+ : null;
238
303
  return {
239
304
  field: observation.field,
240
305
  category: finding.category,
@@ -243,6 +308,7 @@ function structuredPinFields(finding) {
243
308
  schema: observation.sourceSchema,
244
309
  version: observation.sourceVersion,
245
310
  },
311
+ certificateObservation,
246
312
  };
247
313
  }
248
314
  export function mergeExternalFindingsIntoCases(current, document, now = new Date().toISOString()) {
@@ -0,0 +1,8 @@
1
+ const UNSAFE_RETAINED_TEXT_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
2
+ const UNSAFE_RETAINED_TEXT_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
3
+ export function hasUnsafeRetainedText(value) {
4
+ return UNSAFE_RETAINED_TEXT_RE.test(value);
5
+ }
6
+ export function neutralizeUnsafeRetainedText(value) {
7
+ return value.replace(UNSAFE_RETAINED_TEXT_GLOBAL_RE, ' ');
8
+ }
@@ -1,6 +1,7 @@
1
- import { sha256ArtifactDigest } from "./artifact-integrity.js";
2
- export const ACQUISITION_DECISION_PACKET_SCHEMA = 'whoisleuth.acquisition-decision';
3
- export const ACQUISITION_DECISION_PACKET_VERSION = 1;
1
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
2
+ import { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../contracts/investigation-portability.mjs";
3
+ import { isValidAsciiDomainName } from "../../lib/hostname.mjs";
4
+ export { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION };
4
5
  export const ACQUISITION_DECISIONS = [
5
6
  'unresolved',
6
7
  'continue_manual_review',
@@ -28,11 +29,13 @@ function timestamp(value) {
28
29
  return new Date(value).toISOString();
29
30
  }
30
31
  function target(value) {
31
- const normalized = text(value, 253).toLowerCase().replace(/\.$/u, '');
32
- if (!normalized || !normalized.includes('.') || /[/\\\s]/u.test(normalized)) {
32
+ const submitted = typeof value === 'string'
33
+ ? value.trim().replace(/\.$/u, '')
34
+ : '';
35
+ if (!isValidAsciiDomainName(submitted, { requireDot: true })) {
33
36
  throw new Error('A canonical domain is required for an acquisition decision packet.');
34
37
  }
35
- return normalized;
38
+ return submitted.toLowerCase();
36
39
  }
37
40
  function decision(value) {
38
41
  return typeof value === 'string' && DECISION_SET.has(value)
@@ -80,10 +83,10 @@ export async function buildAcquisitionDecisionPacket(input) {
80
83
  : []),
81
84
  ],
82
85
  };
83
- const digestSha256 = await sha256ArtifactDigest(unsigned);
86
+ const digestSha256 = await sha256ArtifactDigestV2(unsigned);
84
87
  const document = {
85
88
  ...unsigned,
86
- integrity: { algorithm: 'SHA-256', digestSha256 },
89
+ integrity: { algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2, digestSha256 },
87
90
  };
88
91
  return {
89
92
  document,