@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,232 @@
1
+ import { LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "./lookup-claim-readiness.mjs";
2
+ import { SORTED_JSON_V2, sha256ArtifactDigestV2, } from "../evidence/artifact-integrity.mjs";
3
+ import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_BYTES, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../contracts/investigation-portability.mjs";
4
+ export { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_BYTES, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, };
5
+ const SOURCE_STATES = new Set([
6
+ 'complete',
7
+ 'not_found',
8
+ 'partial',
9
+ 'skipped',
10
+ 'unavailable',
11
+ 'unknown',
12
+ 'unsupported',
13
+ ]);
14
+ const CLAIM_STATES = new Set(['ready', 'limited', 'not_ready']);
15
+ const REQUIREMENT_IDS = new Set(LOOKUP_CLAIM_REQUIREMENT_IDS);
16
+ const CLAIM_IDS = new Set(LOOKUP_CLAIM_IDS);
17
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
18
+ const HAS_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
19
+ const DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
20
+ const SEMVER_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
21
+ function text(value, maximum) {
22
+ return typeof value === 'string'
23
+ ? value.replace(CONTROL_CHARACTERS, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
24
+ : '';
25
+ }
26
+ function exactText(value, maximum) {
27
+ if (typeof value !== 'string' || value.length > maximum || HAS_CONTROL_CHARACTERS.test(value))
28
+ return '';
29
+ return value.trim();
30
+ }
31
+ function timestamp(value) {
32
+ if (typeof value !== 'string')
33
+ return null;
34
+ const parsed = Date.parse(value);
35
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
36
+ }
37
+ function uniqueText(values, maximum = MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS) {
38
+ const output = [];
39
+ const seen = new Set();
40
+ for (const value of values.slice(0, maximum * 2)) {
41
+ const normalized = text(value, 400);
42
+ if (!normalized || seen.has(normalized))
43
+ continue;
44
+ output.push(normalized);
45
+ seen.add(normalized);
46
+ if (output.length >= maximum)
47
+ break;
48
+ }
49
+ return output;
50
+ }
51
+ function targetType(value) {
52
+ if (typeof value !== 'string' || !LOOKUP_CLAIM_PASSPORT_TARGET_TYPES.includes(value)) {
53
+ throw new TypeError('A supported Lookup target type is required for a claim passport.');
54
+ }
55
+ return value;
56
+ }
57
+ function domainTarget(value) {
58
+ const raw = exactText(value, 253).toLowerCase().replace(/\.$/u, '');
59
+ if (!raw || /[/\\?#@]/u.test(raw))
60
+ throw new TypeError('A canonical domain is required for a claim passport.');
61
+ let normalized = '';
62
+ try {
63
+ normalized = new URL(`http://${raw}`).hostname.toLowerCase().replace(/\.$/u, '');
64
+ }
65
+ catch { /* fail below */ }
66
+ if (!DOMAIN_RE.test(normalized))
67
+ throw new TypeError('A canonical domain is required for a claim passport.');
68
+ return normalized;
69
+ }
70
+ function ipv4Target(value) {
71
+ const raw = exactText(value, 64);
72
+ const parts = raw.split('.');
73
+ if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/u.test(part) || Number(part) > 255 || String(Number(part)) !== part)) {
74
+ throw new TypeError('A canonical IPv4 address is required for a claim passport.');
75
+ }
76
+ return raw;
77
+ }
78
+ function ipv6Target(value) {
79
+ const raw = exactText(value, 64).toLowerCase();
80
+ if (!raw || /[\[\]%/?#@]/u.test(raw))
81
+ throw new TypeError('A canonical IPv6 address is required for a claim passport.');
82
+ try {
83
+ const hostname = new URL(`http://[${raw}]/`).hostname;
84
+ const normalized = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
85
+ if (!normalized.includes(':'))
86
+ throw new TypeError();
87
+ return normalized;
88
+ }
89
+ catch {
90
+ throw new TypeError('A canonical IPv6 address is required for a claim passport.');
91
+ }
92
+ }
93
+ function asnTarget(value) {
94
+ const raw = exactText(value, 32).toUpperCase().replace(/^AS/u, '');
95
+ if (!/^\d{1,10}$/u.test(raw) || Number(raw) < 1 || Number(raw) > 4_294_967_295) {
96
+ throw new TypeError('A canonical ASN is required for a claim passport.');
97
+ }
98
+ return `AS${Number(raw)}`;
99
+ }
100
+ function targetValue(type, value) {
101
+ if (type === 'domain')
102
+ return domainTarget(value);
103
+ if (type === 'ipv4')
104
+ return ipv4Target(value);
105
+ if (type === 'ipv6')
106
+ return ipv6Target(value);
107
+ return asnTarget(value);
108
+ }
109
+ function requirementId(value) {
110
+ if (typeof value !== 'string' || !REQUIREMENT_IDS.has(value)) {
111
+ throw new TypeError('The selected claim contains an unsupported evidence requirement.');
112
+ }
113
+ return value;
114
+ }
115
+ export async function buildLookupClaimPassport(input) {
116
+ if (input.readiness.version !== LOOKUP_CLAIM_READINESS_VERSION) {
117
+ throw new TypeError('The Evidence Readiness contract is not supported for passport export.');
118
+ }
119
+ if (input.readiness.entries.length > LOOKUP_CLAIM_IDS.length) {
120
+ throw new TypeError('The Evidence Readiness contract contains too many entries for passport export.');
121
+ }
122
+ const rawClaimId = exactText(input.claimId, 80);
123
+ if (!CLAIM_IDS.has(rawClaimId))
124
+ throw new TypeError('Select a current Lookup claim to export.');
125
+ const claimId = rawClaimId;
126
+ const selected = input.readiness.entries.find((entry) => entry.id === claimId);
127
+ if (!selected || !CLAIM_STATES.has(selected.state))
128
+ throw new TypeError('Select a current Lookup claim to export.');
129
+ if (selected.requirements.length < 1 || selected.requirements.length > MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS) {
130
+ throw new TypeError('The selected claim has an unsupported evidence-requirement count.');
131
+ }
132
+ const type = targetType(input.targetType);
133
+ const value = targetValue(type, input.target);
134
+ const generatedAt = timestamp(input.generatedAt) ?? new Date().toISOString();
135
+ const applicationVersion = exactText(input.applicationVersion, 80);
136
+ if (!SEMVER_RE.test(applicationVersion))
137
+ throw new TypeError('A valid application version is required for a claim passport.');
138
+ const lookupDepth = input.lookupDepth === 'deep' ? 'deep' : input.lookupDepth === 'fast' ? 'fast' : null;
139
+ if (!lookupDepth)
140
+ throw new TypeError('A completed Lookup depth is required for a claim passport.');
141
+ const requirementIds = new Set();
142
+ const requirements = selected.requirements.map((source) => {
143
+ const id = requirementId(source.id);
144
+ if (requirementIds.has(id) || !SOURCE_STATES.has(source.state)) {
145
+ throw new TypeError('The selected claim contains duplicate or invalid evidence requirements.');
146
+ }
147
+ requirementIds.add(id);
148
+ let evidenceId = null;
149
+ if (source.evidenceId !== null) {
150
+ const normalizedEvidenceId = exactText(source.evidenceId, 64);
151
+ if (!/^[a-z][a-z0-9_-]{0,63}$/u.test(normalizedEvidenceId))
152
+ throw new TypeError('The selected claim contains an invalid evidence source identifier.');
153
+ evidenceId = normalizedEvidenceId;
154
+ }
155
+ if (source.mode !== 'network_collection' && source.mode !== 'local_review') {
156
+ throw new TypeError('The selected claim contains an invalid evidence requirement mode.');
157
+ }
158
+ return Object.freeze({
159
+ id,
160
+ label: exactText(source.label, 160),
161
+ evidenceId,
162
+ mode: source.mode,
163
+ state: source.state,
164
+ observedAt: evidenceId ? timestamp(input.evidenceObservedAtById?.[evidenceId]) : null,
165
+ limitations: Object.freeze(uniqueText(source.limitations, 8)),
166
+ });
167
+ });
168
+ if (requirements.some((item) => !item.label))
169
+ throw new TypeError('The selected claim contains an invalid evidence label.');
170
+ const requiredEvidenceIds = requirements.map((item) => item.id);
171
+ const missingEvidenceIds = requirements.filter((item) => item.state !== 'complete').map((item) => item.id);
172
+ const riskModelVersion = Number.isSafeInteger(input.riskModelVersion)
173
+ && Number(input.riskModelVersion) >= 1
174
+ && Number(input.riskModelVersion) <= 1_000
175
+ ? Number(input.riskModelVersion)
176
+ : null;
177
+ const claimLabel = exactText(selected.label, 160);
178
+ const claimConclusion = exactText(selected.conclusion, 600);
179
+ const unsigned = {
180
+ schema: LOOKUP_CLAIM_PASSPORT_SCHEMA,
181
+ version: LOOKUP_CLAIM_PASSPORT_VERSION,
182
+ generatedAt,
183
+ application: { name: 'WHOISleuth', version: applicationVersion },
184
+ target: { type, value },
185
+ observation: { observedAt: timestamp(input.observedAt), lookupDepth },
186
+ claim: {
187
+ id: claimId,
188
+ label: claimLabel,
189
+ state: selected.state,
190
+ conclusion: claimConclusion,
191
+ requiredEvidenceIds,
192
+ missingEvidenceIds,
193
+ requirements,
194
+ limitations: uniqueText(selected.limitations),
195
+ },
196
+ models: {
197
+ claimReadiness: LOOKUP_CLAIM_READINESS_VERSION,
198
+ risk: riskModelVersion,
199
+ },
200
+ limitations: uniqueText([
201
+ input.readiness.limitation,
202
+ 'This portable passport records evidence sufficiency for one point-in-time statement. It does not establish that the statement is true, current, safe, authorized, or attributable to an owner or actor.',
203
+ 'The SHA-256 digest detects changes to this file. It does not authenticate the analyst, collection source, signer, or truth of the retained states.',
204
+ 'Raw registry responses, contacts, page values, request paths, credentials, and browser-local records are excluded.',
205
+ ]),
206
+ };
207
+ if (!unsigned.claim.label || !unsigned.claim.conclusion)
208
+ throw new TypeError('The selected claim is incomplete.');
209
+ const digestSha256 = await sha256ArtifactDigestV2(unsigned);
210
+ const document = Object.freeze({
211
+ ...unsigned,
212
+ claim: Object.freeze({
213
+ ...unsigned.claim,
214
+ requiredEvidenceIds: Object.freeze(requiredEvidenceIds),
215
+ missingEvidenceIds: Object.freeze(missingEvidenceIds),
216
+ requirements: Object.freeze(requirements),
217
+ limitations: Object.freeze(unsigned.claim.limitations),
218
+ }),
219
+ limitations: Object.freeze(unsigned.limitations),
220
+ integrity: Object.freeze({ algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2, digestSha256 }),
221
+ });
222
+ const content = `${JSON.stringify(document, null, 2)}\n`;
223
+ if (new TextEncoder().encode(content).byteLength > MAX_LOOKUP_CLAIM_PASSPORT_BYTES) {
224
+ throw new TypeError('The claim passport exceeds its export byte limit.');
225
+ }
226
+ const safeTarget = value.toLowerCase().replace(/[^a-z0-9.-]+/gu, '-').replace(/^-|-$/gu, '').slice(0, 100) || 'target';
227
+ return Object.freeze({
228
+ document,
229
+ content,
230
+ filename: `whoisleuth-claim-${safeTarget}-${claimId}-${generatedAt.slice(0, 10)}.json`,
231
+ });
232
+ }
@@ -0,0 +1,287 @@
1
+ import { LOOKUP_CLAIM_READINESS_VERSION } from "../contracts/investigation-portability.mjs";
2
+ export { LOOKUP_CLAIM_READINESS_VERSION };
3
+ export const LOOKUP_CLAIM_IDS = [
4
+ 'registration-state',
5
+ 'current-web-observation',
6
+ 'brand-resemblance',
7
+ 'controlled-change',
8
+ 'incident-response',
9
+ 'network-context',
10
+ ];
11
+ export const LOOKUP_CLAIM_REQUIREMENT_IDS = [
12
+ 'authority-aware-availability',
13
+ 'registry-rdap',
14
+ 'registry-whois',
15
+ 'registry-control-selection',
16
+ 'dns-observation',
17
+ 'http-observation',
18
+ 'tls-observation',
19
+ 'page-identity-observation',
20
+ 'reviewed-brand-profile',
21
+ 'reviewed-case-recipient',
22
+ 'authoritative-rdap',
23
+ ];
24
+ const MAX_TEXT = 320;
25
+ const MAX_DIAGNOSTICS = 12;
26
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
27
+ const LIMITED_STATES = new Set(['partial', 'unavailable', 'unknown']);
28
+ const ABSENT_STATES = new Set(['skipped', 'unsupported']);
29
+ export const LOOKUP_CLAIM_REQUIREMENTS = Object.freeze({
30
+ 'authority-aware-availability': Object.freeze({ id: 'authority-aware-availability', label: 'Authority-aware availability decision', evidenceId: 'availability', mode: 'network_collection', href: '#registry' }),
31
+ 'registry-rdap': Object.freeze({ id: 'registry-rdap', label: 'Registry RDAP evidence', evidenceId: 'rdap', mode: 'network_collection', href: '#registry' }),
32
+ 'registry-whois': Object.freeze({ id: 'registry-whois', label: 'Registry WHOIS evidence', evidenceId: 'whois', mode: 'network_collection', href: '#registry' }),
33
+ 'registry-control-selection': Object.freeze({ id: 'registry-control-selection', label: 'Registry control evidence selected by the availability authority', evidenceId: null, mode: 'network_collection', href: '#registry' }),
34
+ 'dns-observation': Object.freeze({ id: 'dns-observation', label: 'DNS observation', evidenceId: 'dns', mode: 'network_collection', href: '#evidence-dns' }),
35
+ 'http-observation': Object.freeze({ id: 'http-observation', label: 'HTTP observation', evidenceId: 'http', mode: 'network_collection', href: '#evidence-http' }),
36
+ 'tls-observation': Object.freeze({ id: 'tls-observation', label: 'TLS observation', evidenceId: 'tls', mode: 'network_collection', href: '#evidence-tls' }),
37
+ 'page-identity-observation': Object.freeze({ id: 'page-identity-observation', label: 'Page identity observation', evidenceId: 'page-identity', mode: 'network_collection', href: '#evidence-page' }),
38
+ 'reviewed-brand-profile': Object.freeze({ id: 'reviewed-brand-profile', label: 'Reviewed Brand Profile', evidenceId: null, mode: 'local_review', href: '#case-response' }),
39
+ 'reviewed-case-recipient': Object.freeze({ id: 'reviewed-case-recipient', label: 'Reviewed case and recipient route', evidenceId: null, mode: 'local_review', href: '#case-response' }),
40
+ 'authoritative-rdap': Object.freeze({ id: 'authoritative-rdap', label: 'Authoritative RDAP evidence', evidenceId: 'rdap', mode: 'network_collection', href: '#registry' }),
41
+ });
42
+ function record(value) {
43
+ return value && typeof value === 'object' && !Array.isArray(value)
44
+ ? value
45
+ : {};
46
+ }
47
+ function text(value, maximum = MAX_TEXT) {
48
+ return String(value ?? '')
49
+ .replace(CONTROL_CHARACTERS, ' ')
50
+ .replace(/\s+/gu, ' ')
51
+ .trim()
52
+ .slice(0, maximum);
53
+ }
54
+ function timestamp(value) {
55
+ if (typeof value !== 'string')
56
+ return null;
57
+ const parsed = Date.parse(value);
58
+ return Number.isFinite(parsed) ? parsed : null;
59
+ }
60
+ function evaluateRequirements(requirements) {
61
+ const missing = requirements.filter((requirement) => requirement.state !== 'complete');
62
+ const limitations = missing.flatMap((requirement) => {
63
+ const detail = requirement.limitations[0] || requirement.state.replaceAll('_', ' ');
64
+ return [`${requirement.label}: ${detail}`];
65
+ }).slice(0, 8);
66
+ if (!missing.length) {
67
+ return {
68
+ state: 'ready',
69
+ requiredEvidence: requirements.map((item) => item.label),
70
+ missingEvidence: [],
71
+ requiredEvidenceIds: requirements.map((item) => item.id),
72
+ missingEvidenceIds: [],
73
+ limitations,
74
+ requirements,
75
+ };
76
+ }
77
+ const observedCount = requirements.filter((requirement) => !ABSENT_STATES.has(requirement.state) && requirement.state !== 'unknown').length;
78
+ const hasLimited = requirements.some((requirement) => LIMITED_STATES.has(requirement.state));
79
+ return {
80
+ state: observedCount > 0 && hasLimited ? 'limited' : 'not_ready',
81
+ requiredEvidence: requirements.map((item) => item.label),
82
+ missingEvidence: missing.map((item) => item.label),
83
+ requiredEvidenceIds: requirements.map((item) => item.id),
84
+ missingEvidenceIds: missing.map((item) => item.id),
85
+ limitations,
86
+ requirements,
87
+ };
88
+ }
89
+ function requirement(id, coverage, override) {
90
+ const definition = LOOKUP_CLAIM_REQUIREMENTS[id];
91
+ const evidence = definition.evidenceId ? coverage.get(definition.evidenceId) : undefined;
92
+ const state = override?.state ?? evidence?.state ?? 'unknown';
93
+ const coverageState = definition.evidenceId ? evidence?.state ?? 'unknown' : null;
94
+ const limitations = override?.limitation
95
+ ? [override.limitation]
96
+ : evidence?.limitations.length
97
+ ? evidence.limitations.slice(0, 8)
98
+ : state === 'complete'
99
+ ? []
100
+ : [evidence?.statusLabel || 'Not supplied'];
101
+ return Object.freeze({
102
+ ...definition,
103
+ coverageState,
104
+ state,
105
+ limitations: Object.freeze(limitations),
106
+ });
107
+ }
108
+ function readinessEntry(input) {
109
+ const evaluated = evaluateRequirements(input.requirements);
110
+ return { id: input.id, label: input.label, conclusion: input.conclusion, href: input.href, ...evaluated };
111
+ }
112
+ function comparisonDiagnostics(comparison, sourceLabel, observedAt) {
113
+ const source = record(comparison);
114
+ const fields = Array.isArray(source.fields) ? source.fields.slice(0, 32) : [];
115
+ const registryTime = timestamp(observedAt.registry);
116
+ const comparedTime = timestamp(observedAt.compared);
117
+ const timeGapHours = registryTime !== null && comparedTime !== null
118
+ ? Math.round(Math.abs(registryTime - comparedTime) / 3_600_000)
119
+ : null;
120
+ const output = [];
121
+ for (const item of fields) {
122
+ const field = record(item);
123
+ if (text(field.status, 64) !== 'conflict')
124
+ continue;
125
+ const label = text(field.label, 100) || 'Registration field';
126
+ const normalizedLabel = label.toLowerCase();
127
+ let hypothesis = 'Different source publication scope or update timing';
128
+ let detail = `Registry RDAP and ${sourceLabel} published different normalized values. Review the exact values and source timestamps.`;
129
+ const basis = ['Material normalized-value difference'];
130
+ if (timeGapHours !== null && timeGapHours >= 1) {
131
+ hypothesis = 'Collection-time or publication-lag difference';
132
+ detail = `The sources were observed about ${timeGapHours} hour${timeGapHours === 1 ? '' : 's'} apart. A change or publication delay could explain the difference, but this is not established.`;
133
+ basis.push('Different observation times');
134
+ }
135
+ else if (/contact|registrant|administrative|technical|abuse/u.test(normalizedLabel)) {
136
+ hypothesis = 'Disclosure or source-role difference';
137
+ detail = `Registration contact publications can differ by source role, disclosure policy, privacy service, or update timing. The available evidence does not identify which explanation applies.`;
138
+ basis.push('Contact-related field');
139
+ }
140
+ else if (/status|nameserver|name server|dnssec/u.test(normalizedLabel)) {
141
+ hypothesis = 'Registry and delegated-service publication difference';
142
+ detail = `Operational and registry publications can update at different times or represent different layers. Directly review the current authoritative evidence before treating this as a configuration change.`;
143
+ basis.push('Operational registration field');
144
+ }
145
+ else if (/registrar|sponsor/u.test(normalizedLabel)) {
146
+ hypothesis = 'Sponsorship publication or transfer timing difference';
147
+ detail = `A transfer, reseller relationship, source normalisation, or publication delay could explain the difference. None is proven by this comparison alone.`;
148
+ basis.push('Registrar-related field');
149
+ }
150
+ output.push({
151
+ id: `${sourceLabel === 'WHOIS' ? 'whois' : 'registrar-rdap'}-${normalizedLabel.replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '') || output.length}`,
152
+ field: label,
153
+ hypothesis,
154
+ detail,
155
+ basis,
156
+ });
157
+ }
158
+ return output;
159
+ }
160
+ export function buildLookupClaimReadiness(input) {
161
+ const coverage = new Map(input.coverage.entries.map((entry) => [entry.id, entry]));
162
+ const targetType = text(input.targetType, 20);
163
+ const settledAvailability = !['', 'unknown', 'inconclusive', 'error'].includes(text(input.availabilityState, 40));
164
+ const observedAvailability = requirement('authority-aware-availability', coverage);
165
+ const registrationRequirements = [settledAvailability
166
+ ? observedAvailability
167
+ : Object.freeze({
168
+ ...observedAvailability,
169
+ state: observedAvailability.state === 'complete' ? 'partial' : observedAvailability.state,
170
+ limitations: Object.freeze([
171
+ ...observedAvailability.limitations,
172
+ 'The authority-aware availability state is not settled.',
173
+ ].slice(0, 8)),
174
+ })];
175
+ const availabilitySource = text(input.availabilitySource, 24).toLowerCase();
176
+ const controlledRegistrationRequirements = availabilitySource === 'rdap'
177
+ ? [requirement('registry-rdap', coverage)]
178
+ : availabilitySource === 'whois'
179
+ ? [requirement('registry-whois', coverage)]
180
+ : [requirement('registry-control-selection', coverage, {
181
+ state: 'unknown',
182
+ limitation: 'The availability authority did not select a registry control source.',
183
+ })];
184
+ const webRequirements = [requirement('http-observation', coverage), requirement('tls-observation', coverage)];
185
+ const entries = [];
186
+ if (targetType === 'domain') {
187
+ entries.push(readinessEntry({
188
+ id: 'registration-state',
189
+ label: 'Registration-state statement',
190
+ conclusion: 'Whether an authority-aware registered, available, or inconclusive statement can be made.',
191
+ requirements: registrationRequirements,
192
+ href: '#registry',
193
+ }));
194
+ entries.push(readinessEntry({
195
+ id: 'current-web-observation',
196
+ label: 'Current website statement',
197
+ conclusion: 'Whether the current HTTP and TLS observation is complete enough to describe what responded.',
198
+ requirements: webRequirements,
199
+ href: '#web-evidence',
200
+ }));
201
+ if (input.task === 'brand' || input.hasActiveProfile) {
202
+ entries.push(readinessEntry({
203
+ id: 'brand-resemblance',
204
+ label: 'Brand-resemblance assessment',
205
+ conclusion: 'Whether reviewed brand context and page identity evidence support a bounded resemblance assessment.',
206
+ requirements: [
207
+ requirement('page-identity-observation', coverage),
208
+ requirement('reviewed-brand-profile', coverage, {
209
+ state: input.profileSourceState === 'unavailable'
210
+ ? 'unavailable'
211
+ : input.hasActiveProfile === true
212
+ ? 'complete'
213
+ : 'unknown',
214
+ ...(input.profileSourceState === 'unavailable'
215
+ ? { limitation: 'The browser-local Brand Profile source is unavailable.' }
216
+ : input.hasActiveProfile === true
217
+ ? {}
218
+ : { limitation: 'No reviewed Brand Profile is active.' }),
219
+ }),
220
+ ],
221
+ href: '#evidence-page',
222
+ }));
223
+ }
224
+ if (input.task === 'owned' || input.task === 'acquisition') {
225
+ entries.push(readinessEntry({
226
+ id: 'controlled-change',
227
+ label: 'Controlled-change planning',
228
+ conclusion: 'Whether registration and DNS evidence are complete enough to prepare a reviewed change plan.',
229
+ requirements: [...registrationRequirements, ...controlledRegistrationRequirements, requirement('dns-observation', coverage)],
230
+ href: '#evidence-dns',
231
+ }));
232
+ }
233
+ if (input.task === 'incident') {
234
+ entries.push(readinessEntry({
235
+ id: 'incident-response',
236
+ label: 'Incident response handoff',
237
+ conclusion: 'Whether current web evidence and a reviewed recipient route are present for a response packet.',
238
+ requirements: [
239
+ requirement('http-observation', coverage),
240
+ requirement('page-identity-observation', coverage),
241
+ requirement('reviewed-case-recipient', coverage, {
242
+ state: input.hasCaseSection === true && (input.responseRecipientCount ?? 0) > 0 ? 'complete' : 'unknown',
243
+ ...(input.hasCaseSection === true && (input.responseRecipientCount ?? 0) > 0
244
+ ? {}
245
+ : { limitation: 'A reviewed case and recipient route are not both available.' }),
246
+ }),
247
+ ],
248
+ href: '#case-response',
249
+ }));
250
+ }
251
+ }
252
+ else if (targetType === 'ip' || targetType === 'ipv4' || targetType === 'ipv6' || targetType === 'asn') {
253
+ entries.push(readinessEntry({
254
+ id: 'network-context',
255
+ label: 'Network registration statement',
256
+ conclusion: 'Whether the observed network registration context is complete enough to describe the resource.',
257
+ requirements: [requirement('authoritative-rdap', coverage)],
258
+ href: '#registry',
259
+ }));
260
+ }
261
+ const disagreements = [
262
+ ...comparisonDiagnostics(input.registryComparison, 'WHOIS', {
263
+ registry: input.observedAt?.registry,
264
+ compared: input.observedAt?.whois,
265
+ }),
266
+ ...comparisonDiagnostics(input.registrarPublicationComparison, 'Registrar RDAP', {
267
+ registry: input.observedAt?.registry,
268
+ compared: input.observedAt?.registrar,
269
+ }),
270
+ ].slice(0, MAX_DIAGNOSTICS);
271
+ const counts = {
272
+ ready: 0,
273
+ limited: 0,
274
+ not_ready: 0,
275
+ };
276
+ for (const entry of entries)
277
+ counts[entry.state] += 1;
278
+ return {
279
+ version: LOOKUP_CLAIM_READINESS_VERSION,
280
+ entries,
281
+ disagreements,
282
+ counts,
283
+ limitation: input.decisionSupport.counts.conflicts
284
+ ? 'Readiness describes evidence sufficiency, not truth. Registration-source explanations are bounded hypotheses and require review of the original observations.'
285
+ : 'Readiness describes evidence sufficiency, not truth, ownership, intent, safety, or eligibility.',
286
+ };
287
+ }