@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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 (236) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,542 +0,0 @@
1
- const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
2
- const MAX_TEXT = 320;
3
- const MAX_ENTRIES = 24;
4
- const MAX_ACTIONS = 6;
5
- export const MAX_LOOKUP_PRESENTED_ACTIONS = 3;
6
- const LOOKUP_TASK_EVIDENCE_KINDS = new Set([
7
- 'delegation', 'dependency', 'dns', 'form', 'http', 'identity', 'mail', 'page',
8
- 'posture', 'ptr', 'redirect', 'tls',
9
- ]);
10
- const TASK_ACTION_ID = Object.freeze({
11
- acquisition: 'review-acquisition-dependencies',
12
- brand: 'review-page-identity',
13
- owned: 'review-owned-posture',
14
- });
15
- const ACTION_PRIORITY = Object.freeze({
16
- high: 0,
17
- medium: 1,
18
- low: 2,
19
- });
20
- const TASK_GUIDANCE = Object.freeze({
21
- general: Object.freeze({
22
- label: 'General investigation',
23
- summary: 'Establish what was observed, which sources agree, and what remains unknown before drawing a conclusion.',
24
- questions: Object.freeze([
25
- 'Which source observations are complete enough to rely on?',
26
- 'Do registration, infrastructure, certificate, and page identity evidence agree?',
27
- 'What manual pivot would reduce the most important uncertainty?',
28
- ]),
29
- prioritySections: Object.freeze(['overview', 'web-evidence', 'registry', 'case-response']),
30
- }),
31
- acquisition: Object.freeze({
32
- label: 'Acquisition review',
33
- summary: 'Prioritise registration lifecycle, authority, transfer dependencies, mail, DNS, and services that would need a controlled transition.',
34
- questions: Object.freeze([
35
- 'Is the registration conclusion authoritative and internally consistent?',
36
- 'Which DNS, mail, certificate, and website dependencies require transition planning?',
37
- 'Which lifecycle dates are observations rather than guaranteed transfer or deletion dates?',
38
- ]),
39
- prioritySections: Object.freeze(['overview', 'registry', 'web-evidence', 'case-response']),
40
- }),
41
- brand: Object.freeze({
42
- label: 'Brand review',
43
- summary: 'Prioritise declared identity, page similarity, credential surfaces, external destinations, and infrastructure relationships.',
44
- questions: Object.freeze([
45
- 'Does the page claim or resemble a reviewed identity?',
46
- 'Where do redirects, forms, resources, and trackers cross trust boundaries?',
47
- 'Which similarities are verified facts and which are only review leads?',
48
- ]),
49
- prioritySections: Object.freeze(['overview', 'web-evidence', 'external-intelligence', 'case-response']),
50
- }),
51
- incident: Object.freeze({
52
- label: 'Incident response',
53
- summary: 'Prioritise current reachability, redirects, certificate state, credential surfaces, warning data, and evidence that can support a reviewed response.',
54
- questions: Object.freeze([
55
- 'What behaviour was observed at the recorded time?',
56
- 'Which source limitations could change the response decision?',
57
- 'What evidence and recipient route are ready for a reviewed handoff?',
58
- ]),
59
- prioritySections: Object.freeze(['overview', 'external-intelligence', 'web-evidence', 'case-response']),
60
- }),
61
- owned: Object.freeze({
62
- label: 'Owned-domain posture',
63
- summary: 'Prioritise delegation, mail, certificate, security-policy, lifecycle, and change evidence for a domain under review.',
64
- questions: Object.freeze([
65
- 'Do registry publication and directly observed delegation agree?',
66
- 'Are mail, certificate, and website controls in the expected state?',
67
- 'Which incomplete or stale source should be reviewed before recording posture?',
68
- ]),
69
- prioritySections: Object.freeze(['overview', 'registry', 'web-evidence', 'case-response']),
70
- }),
71
- });
72
- const TIMING_TO_EVIDENCE = Object.freeze({
73
- rdap: Object.freeze(['rdap']),
74
- whois: Object.freeze(['whois']),
75
- domain_evidence: Object.freeze([
76
- 'availability',
77
- 'client-behavior',
78
- 'dns',
79
- 'http',
80
- 'page-identity',
81
- 'page-role',
82
- 'security-posture',
83
- 'sslbl',
84
- 'technology',
85
- 'tls',
86
- ]),
87
- reverse_dns: Object.freeze(['reverse-dns']),
88
- registrar_rdap: Object.freeze(['registrar-rdap']),
89
- network_context: Object.freeze(['network-context']),
90
- security_txt: Object.freeze(['security-txt']),
91
- external_intelligence: Object.freeze(['external-intelligence']),
92
- malware_host_intelligence: Object.freeze(['malware-host-intelligence']),
93
- malware_ioc_intelligence: Object.freeze(['malware-ioc-intelligence']),
94
- });
95
- const SUPPORTS = Object.freeze({
96
- rdap: Object.freeze(['Registration summary', 'Lifecycle', 'Availability']),
97
- whois: Object.freeze(['Registration comparison', 'Contacts', 'Lifecycle']),
98
- 'registrar-rdap': Object.freeze(['Registration publication comparison']),
99
- availability: Object.freeze(['Availability decision', 'Lookup assessment']),
100
- dns: Object.freeze(['Delegation', 'Mail posture', 'Service dependencies']),
101
- 'reverse-dns': Object.freeze(['Observed endpoint context']),
102
- 'network-context': Object.freeze(['Delivery and routing context']),
103
- http: Object.freeze(['Redirects', 'Page collection', 'Website state']),
104
- tls: Object.freeze(['Hostname authorisation', 'Certificate scope']),
105
- 'page-identity': Object.freeze(['Declared page identity', 'Form and resource review']),
106
- technology: Object.freeze(['Technology profile']),
107
- 'security-posture': Object.freeze(['Passive security posture']),
108
- 'security-txt': Object.freeze(['Security contact route']),
109
- sslbl: Object.freeze(['Certificate warning-data comparison']),
110
- 'external-intelligence': Object.freeze(['Optional external context']),
111
- });
112
- const ENDPOINT_CLASS = Object.freeze({
113
- rdap: 'Authoritative registry endpoint',
114
- whois: 'WHOIS transport',
115
- 'registrar-rdap': 'Registrar publication endpoint',
116
- availability: 'Authority-aware analysis',
117
- dns: 'DNS resolver and authorities',
118
- 'reverse-dns': 'Reverse DNS resolver',
119
- 'network-context': 'IP RDAP and routing',
120
- http: 'Bounded HTTP collection',
121
- tls: 'TLS endpoint',
122
- 'page-identity': 'Static page analysis',
123
- technology: 'Derived analysis',
124
- 'security-posture': 'Derived analysis',
125
- 'security-txt': 'Selected well-known resource',
126
- sslbl: 'Local warning-data snapshot',
127
- 'external-intelligence': 'Optional external provider',
128
- });
129
- const SOURCE_DESCRIPTION = Object.freeze({
130
- rdap: 'Structured registration data selected through the registry bootstrap.',
131
- whois: 'Referral-aware text registration data collected through the WHOIS service path.',
132
- 'registrar-rdap': 'A separately attributed registrar publication discovered from registry RDAP.',
133
- availability: 'An authority-aware conclusion derived from the enabled registration sources.',
134
- dns: 'Point-in-time delegation, address, mail, and service records from bounded DNS collection.',
135
- 'reverse-dns': 'Operator-published PTR context for an observed public address.',
136
- 'network-context': 'Point-in-time IP registration and routing context for an observed endpoint.',
137
- http: 'A bounded homepage request covering redirects, response metadata, and a capped body prefix.',
138
- tls: 'A point-in-time TLS handshake to one validated public address for the requested hostname.',
139
- 'page-identity': 'Static identity, form, resource, and page-component signals from retained HTML.',
140
- technology: 'Curated technology signatures evaluated against already observed page evidence.',
141
- 'security-posture': 'Passive security controls derived from collected DNS, HTTP, TLS, and page evidence.',
142
- 'security-txt': 'A bounded request for the selected well-known security contact resource.',
143
- sslbl: 'A local comparison against a versioned certificate warning-data snapshot.',
144
- 'external-intelligence': 'Optional provider context retained with its own source state and limitations.',
145
- });
146
- function record(value) {
147
- return value && typeof value === 'object' && !Array.isArray(value)
148
- ? value
149
- : {};
150
- }
151
- function text(value, maximum = MAX_TEXT) {
152
- return String(value ?? '')
153
- .replace(CONTROL_CHARACTERS, ' ')
154
- .replace(/\s+/gu, ' ')
155
- .trim()
156
- .slice(0, maximum);
157
- }
158
- function idPart(value) {
159
- return text(value, 80).toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '');
160
- }
161
- function display(value) {
162
- const normalized = text(value, 180);
163
- return normalized || 'not published';
164
- }
165
- function isoDate(value) {
166
- if (typeof value !== 'string')
167
- return null;
168
- const timestamp = Date.parse(value);
169
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
170
- }
171
- function ageDays(value, now) {
172
- const current = isoDate(now);
173
- if (!value || !current)
174
- return null;
175
- return Math.max(0, Math.floor((Date.parse(current) - Date.parse(value)) / 86_400_000));
176
- }
177
- function taskGuidance(task) {
178
- return { task, ...TASK_GUIDANCE[task] };
179
- }
180
- function comparisonEntries(comparison, kind) {
181
- const output = [];
182
- const source = record(comparison);
183
- const sourceHealth = record(source.sourceHealth);
184
- const fields = Array.isArray(source.fields) ? source.fields.slice(0, 24) : [];
185
- for (const item of fields) {
186
- const field = record(item);
187
- const status = text(field.status, 64);
188
- const label = text(field.label, 120) || 'Registration field';
189
- const suffix = idPart(label);
190
- if (status === 'conflict') {
191
- const left = kind === 'registry-whois' ? field.rdapDisplay : field.registryDisplay;
192
- const right = kind === 'registry-whois' ? field.whoisDisplay : field.registrarDisplay;
193
- output.push({
194
- id: `${kind}-${suffix || output.length}`,
195
- state: 'conflict',
196
- importance: ['Domain', 'Registrar', 'Name servers', 'Statuses'].includes(label) ? 'high' : 'medium',
197
- title: `${label} differs between registration sources`,
198
- detail: `${display(left)} compared with ${display(right)}.`,
199
- sources: kind === 'registry-whois'
200
- ? ['Registry RDAP', 'WHOIS']
201
- : ['Registry RDAP', 'Registrar RDAP'],
202
- href: '#registry',
203
- });
204
- continue;
205
- }
206
- const unavailableSource = status.startsWith('rdap_')
207
- ? 'rdap'
208
- : status.startsWith('whois_')
209
- ? 'whois'
210
- : status.startsWith('registry_')
211
- ? 'registry'
212
- : status.startsWith('registrar_')
213
- ? 'registrar'
214
- : null;
215
- const unavailableSourceStatus = unavailableSource
216
- ? text(record(sourceHealth[unavailableSource]).status, 64)
217
- : null;
218
- if ((status.includes('unavailable') || status.includes('incomplete'))
219
- && unavailableSourceStatus !== 'unsupported') {
220
- output.push({
221
- id: `${kind}-${suffix || output.length}-${status}`,
222
- state: 'uncertain',
223
- importance: 'low',
224
- title: `${label} comparison is incomplete`,
225
- detail: 'At least one registration source did not provide complete evidence, so no disagreement or equivalence conclusion is available.',
226
- sources: kind === 'registry-whois'
227
- ? ['Registry RDAP', 'WHOIS']
228
- : ['Registry RDAP', 'Registrar RDAP'],
229
- href: '#registry',
230
- });
231
- }
232
- }
233
- return output;
234
- }
235
- function hostname(value) {
236
- const candidate = typeof value === 'string' ? value : text(record(value).url, 2_048);
237
- if (!candidate)
238
- return null;
239
- try {
240
- const url = new URL(/^[a-z][a-z\d+.-]*:\/\//iu.test(candidate) ? candidate : `https://${candidate}`);
241
- return url.hostname.toLowerCase().replace(/\.$/u, '');
242
- }
243
- catch {
244
- return null;
245
- }
246
- }
247
- function sameRegistrableScope(left, right, registrableDomain) {
248
- if (left === right)
249
- return true;
250
- if (!registrableDomain)
251
- return false;
252
- const belongsToDomain = (value) => (value === registrableDomain || value.endsWith(`.${registrableDomain}`));
253
- return belongsToDomain(left) && belongsToDomain(right);
254
- }
255
- function identityEntries(input) {
256
- const output = [];
257
- const requestedHost = hostname(input.requestedHost);
258
- const registrableDomain = hostname(input.registrableDomain);
259
- const finalHost = hostname(input.finalUrl);
260
- const canonicalHost = hostname(input.canonicalUrl);
261
- const openGraphHost = hostname(input.openGraphUrl);
262
- const tlsAuthorization = record(input.tlsAuthorization);
263
- if (tlsAuthorization.authorized === false) {
264
- output.push({
265
- id: 'tls-hostname-authorization',
266
- state: 'conflict',
267
- importance: 'high',
268
- title: 'The observed certificate did not authorise the requested hostname',
269
- detail: text(tlsAuthorization.error, 240) || 'TLS hostname authorisation failed for the requested host.',
270
- sources: ['TLS'],
271
- href: '#evidence-tls',
272
- });
273
- }
274
- if (requestedHost && finalHost && !sameRegistrableScope(requestedHost, finalHost, registrableDomain)) {
275
- output.push({
276
- id: 'http-cross-site-final-origin',
277
- state: 'conflict',
278
- importance: 'medium',
279
- title: 'The final website origin is outside the requested domain',
280
- detail: `${requestedHost} redirected to ${finalHost}. Review the redirect chain before interpreting identity or control.`,
281
- sources: ['HTTP'],
282
- href: '#evidence-http',
283
- });
284
- }
285
- if (finalHost && canonicalHost && !sameRegistrableScope(finalHost, canonicalHost, registrableDomain)) {
286
- output.push({
287
- id: 'page-canonical-origin',
288
- state: 'conflict',
289
- importance: 'medium',
290
- title: 'The declared canonical origin differs from the observed final origin',
291
- detail: `${finalHost} served the page while the document declared ${canonicalHost} as canonical.`,
292
- sources: ['HTTP', 'HTML'],
293
- href: '#evidence-page',
294
- });
295
- }
296
- if (finalHost && openGraphHost && !sameRegistrableScope(finalHost, openGraphHost, registrableDomain)) {
297
- output.push({
298
- id: 'page-open-graph-origin',
299
- state: 'conflict',
300
- importance: 'low',
301
- title: 'The Open Graph URL points outside the observed final origin',
302
- detail: `${finalHost} served the page while Open Graph metadata declared ${openGraphHost}. Publisher metadata is a claim, not identity proof.`,
303
- sources: ['HTTP', 'HTML'],
304
- href: '#evidence-page',
305
- });
306
- }
307
- return output;
308
- }
309
- function certificatePolicyEntries(value) {
310
- const review = record(value);
311
- const findings = Array.isArray(review.findings) ? review.findings.slice(0, 8) : [];
312
- const output = [];
313
- for (const item of findings) {
314
- const finding = record(item);
315
- const id = text(finding.id, 80);
316
- const state = text(finding.state, 80);
317
- const label = text(finding.label, 120) || 'Certificate policy';
318
- if (state === 'apparently_outside_current_policy' || state === 'changed') {
319
- output.push({
320
- id: `certificate-policy-${id || state}`,
321
- state: 'conflict',
322
- importance: id === 'expected_spki' ? 'high' : 'medium',
323
- title: label,
324
- detail: text(finding.detail, 300) || 'Observed certificate context differs from the current reviewed policy context.',
325
- sources: Array.isArray(finding.sources) ? finding.sources.map((source) => text(source, 80)).filter(Boolean) : ['DNS', 'TLS'],
326
- href: '#evidence-certificate-policy',
327
- });
328
- continue;
329
- }
330
- if (state === 'indeterminate' || state === 'no_target_policy_observed') {
331
- output.push({
332
- id: `certificate-policy-${id || state}`,
333
- state: 'uncertain',
334
- importance: 'low',
335
- title: `${label} is indeterminate`,
336
- detail: text(finding.detail, 300) || 'Current evidence does not support a policy comparison.',
337
- sources: Array.isArray(finding.sources) ? finding.sources.map((source) => text(source, 80)).filter(Boolean) : ['DNS', 'TLS'],
338
- href: '#evidence-certificate-policy',
339
- });
340
- }
341
- }
342
- return output;
343
- }
344
- function prioritizeEntries(entries, task) {
345
- const taskBoost = {
346
- general: [],
347
- acquisition: ['registry'],
348
- brand: ['page', 'http', 'tls'],
349
- incident: ['http', 'tls', 'page'],
350
- owned: ['registry', 'tls'],
351
- };
352
- const importance = { high: 0, medium: 1, low: 2 };
353
- return [...entries].sort((left, right) => {
354
- const leftBoost = taskBoost[task].some((prefix) => left.id.startsWith(prefix)) ? -1 : 0;
355
- const rightBoost = taskBoost[task].some((prefix) => right.id.startsWith(prefix)) ? -1 : 0;
356
- return importance[left.importance] - importance[right.importance]
357
- || leftBoost - rightBoost
358
- || left.title.localeCompare(right.title);
359
- }).slice(0, 16);
360
- }
361
- function normalizedTaskEvidenceKinds(values) {
362
- return new Set((values ?? [])
363
- .slice(0, 24)
364
- .filter((value) => (typeof value === 'string' && LOOKUP_TASK_EVIDENCE_KINDS.has(value))));
365
- }
366
- function supportsDecisionDestination(href, evidence) {
367
- if (href === '#evidence-tls')
368
- return evidence.has('tls');
369
- if (href === '#evidence-http')
370
- return evidence.has('http') || evidence.has('redirect');
371
- if (href === '#evidence-page') {
372
- return evidence.has('page') || evidence.has('identity') || evidence.has('form');
373
- }
374
- if (href === '#evidence-certificate-policy')
375
- return evidence.has('tls') || evidence.has('dns');
376
- return true;
377
- }
378
- function supportsTaskAction(task, targetType, evidence) {
379
- if (targetType !== 'domain')
380
- return false;
381
- const requiredKinds = {
382
- acquisition: ['dns', 'mail', 'tls', 'http', 'page', 'dependency'],
383
- brand: ['page', 'identity', 'form', 'redirect'],
384
- owned: ['delegation', 'mail', 'tls', 'posture'],
385
- };
386
- return requiredKinds[task]?.some((kind) => evidence.has(kind)) ?? false;
387
- }
388
- export function buildLookupDecisionSupport(input) {
389
- const availableEvidence = normalizedTaskEvidenceKinds(input.availableEvidence);
390
- const entries = prioritizeEntries([
391
- ...comparisonEntries(input.registryComparison, 'registry-whois'),
392
- ...comparisonEntries(input.registrarPublicationComparison, 'registry-registrar'),
393
- ...identityEntries(input),
394
- ...certificatePolicyEntries(input.certificatePolicyReview),
395
- ], input.task);
396
- const actions = [];
397
- const firstConflict = entries.find((entry) => entry.state === 'conflict'
398
- && supportsDecisionDestination(entry.href, availableEvidence));
399
- if (firstConflict) {
400
- actions.push({
401
- id: 'review-priority-conflict',
402
- label: 'Review the highest-priority disagreement',
403
- reason: firstConflict.title,
404
- expectedOutcome: 'Establish which separately attributed observation is current, authoritative, or still unresolved.',
405
- href: firstConflict.href,
406
- priority: 'high',
407
- });
408
- }
409
- if (input.refreshPlan.items.length) {
410
- actions.push({
411
- id: 'review-refresh-options',
412
- label: 'Review limited or stale sources',
413
- reason: `${input.refreshPlan.items.length} deliberate source refresh option${input.refreshPlan.items.length === 1 ? ' is' : 's are'} available.`,
414
- expectedOutcome: 'Determine whether the limitation is transient, persistent, or an expected source constraint.',
415
- href: '#evidence-quality',
416
- priority: 'medium',
417
- });
418
- }
419
- const firstLimited = input.coverage.entries.find((entry) => entry.manualReviewSuggested);
420
- if (firstLimited && !input.refreshPlan.items.some((item) => item.evidenceIds.includes(firstLimited.id))) {
421
- actions.push({
422
- id: 'inspect-limited-source',
423
- label: `Inspect ${firstLimited.label}`,
424
- reason: `${firstLimited.statusLabel} evidence may limit downstream conclusions.`,
425
- expectedOutcome: `Clarify which conclusions ${firstLimited.label} can and cannot support in its current state.`,
426
- href: '#evidence-quality',
427
- priority: 'medium',
428
- });
429
- }
430
- if (supportsTaskAction(input.task, input.targetType, availableEvidence) && input.task === 'brand') {
431
- actions.push({
432
- id: 'review-page-identity',
433
- label: 'Review identity and credential evidence',
434
- reason: 'Compare declared identity, forms, redirects, and external destinations before making a brand assessment.',
435
- expectedOutcome: 'Separate observed identity signals from publisher claims, shared services, and unresolved resemblance leads.',
436
- href: '#web-evidence',
437
- priority: 'medium',
438
- });
439
- }
440
- else if (supportsTaskAction(input.task, input.targetType, availableEvidence) && input.task === 'acquisition') {
441
- actions.push({
442
- id: 'review-acquisition-dependencies',
443
- label: 'Review transfer dependencies',
444
- reason: 'Registration dates alone do not describe the DNS, mail, certificate, and website services that require transition.',
445
- expectedOutcome: 'Identify dependencies that must be retained, transferred, replaced, or deliberately retired.',
446
- href: '#web-evidence',
447
- priority: 'medium',
448
- });
449
- }
450
- else if (supportsTaskAction(input.task, input.targetType, availableEvidence) && input.task === 'owned') {
451
- actions.push({
452
- id: 'review-owned-posture',
453
- label: 'Review delegation and posture evidence',
454
- reason: 'Confirm registry publication, authoritative responses, mail, and certificate state together.',
455
- expectedOutcome: 'Identify whether observed control state aligns with the reviewed owned-domain baseline.',
456
- href: '#web-evidence',
457
- priority: 'medium',
458
- });
459
- }
460
- if (input.hasCaseSection) {
461
- actions.push({
462
- id: 'review-case-handoff',
463
- label: 'Record or hand off reviewed evidence',
464
- reason: 'Keep observed facts, hypotheses, unknowns, and analyst decisions separate in the case workflow.',
465
- expectedOutcome: 'Preserve a reviewable record of what is known, inferred, unresolved, and recommended next.',
466
- href: '#case-response',
467
- priority: 'low',
468
- });
469
- }
470
- const uniqueActions = [...new Map(actions.map((action) => [action.id, action])).values()].slice(0, MAX_ACTIONS);
471
- return {
472
- version: 1,
473
- guidance: taskGuidance(input.task),
474
- entries,
475
- actions: uniqueActions,
476
- counts: {
477
- conflicts: entries.filter((entry) => entry.state === 'conflict').length,
478
- uncertainties: entries.filter((entry) => entry.state === 'uncertain').length,
479
- },
480
- };
481
- }
482
- export function projectLookupNextActions(actions, task) {
483
- const taskActionId = TASK_ACTION_ID[task];
484
- return actions
485
- .slice(0, MAX_ACTIONS)
486
- .map((action, index) => ({ action, index }))
487
- .sort((left, right) => (ACTION_PRIORITY[left.action.priority] - ACTION_PRIORITY[right.action.priority]
488
- || Number(right.action.id === taskActionId) - Number(left.action.id === taskActionId)
489
- || left.index - right.index))
490
- .slice(0, MAX_LOOKUP_PRESENTED_ACTIONS)
491
- .map(({ action }) => action);
492
- }
493
- function timingByEvidence(timing) {
494
- const output = new Map();
495
- if (!timing)
496
- return output;
497
- for (const source of timing.sources) {
498
- for (const evidenceId of TIMING_TO_EVIDENCE[source.source]) {
499
- output.set(evidenceId, source);
500
- }
501
- }
502
- return output;
503
- }
504
- export function buildLookupEvidenceQualityMatrix(input) {
505
- const observedAt = isoDate(input.observedAt);
506
- const currentAgeDays = ageDays(observedAt, input.now ?? new Date().toISOString());
507
- const timings = timingByEvidence(input.timing);
508
- const refreshIds = new Set(input.refreshPlan.items.flatMap((item) => item.evidenceIds));
509
- const entries = input.coverage.entries.slice(0, MAX_ENTRIES).map((entry) => {
510
- const timing = timings.get(entry.id);
511
- const entryObservedAt = isoDate(input.observedAtByEvidence?.[entry.id]) ?? observedAt;
512
- return {
513
- id: entry.id,
514
- label: entry.label,
515
- category: entry.category,
516
- endpointClass: ENDPOINT_CLASS[entry.id] ?? 'Source-specific collection',
517
- description: SOURCE_DESCRIPTION[entry.id]
518
- ?? 'Source-specific evidence retained with its original state and limitations.',
519
- state: entry.state,
520
- statusLabel: entry.statusLabel,
521
- truncated: entry.truncated,
522
- observedAt: entryObservedAt,
523
- ageDays: ageDays(entryObservedAt, input.now ?? new Date().toISOString()),
524
- durationMs: timing?.durationMs ?? null,
525
- timingOutcome: timing?.outcome ?? null,
526
- refreshAvailable: refreshIds.has(entry.id),
527
- limitations: entry.limitations,
528
- supports: SUPPORTS[entry.id] ?? Object.freeze([]),
529
- };
530
- });
531
- return {
532
- version: 1,
533
- observedAt,
534
- totalMs: input.timing?.totalMs ?? null,
535
- entries,
536
- completeCount: input.coverage.completeCount,
537
- limitedCount: input.coverage.limitedCount,
538
- stale: input.refreshPlan.stale,
539
- ageDays: currentAgeDays,
540
- freshnessPolicy: input.refreshPlan.freshnessPolicy,
541
- };
542
- }