@slicedearth/whoisleuth-cli 2.0.0 → 2.1.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 (97) hide show
  1. package/cli/arguments.mjs +21 -5
  2. package/cli/artifact-validation/case-response.mjs +771 -0
  3. package/cli/artifact-validation/investigation-capsule.mjs +426 -0
  4. package/cli/artifact-validation/lookup-evidence.mjs +718 -0
  5. package/cli/artifact-validation/signed-review.mjs +598 -0
  6. package/cli/artifact-validation/structure-primitives.mjs +105 -0
  7. package/cli/artifact-verify.mjs +1 -1
  8. package/cli/assurance-command-runner.mjs +324 -0
  9. package/cli/command-reference.mjs +1175 -843
  10. package/cli/completion.mjs +56 -9
  11. package/cli/discriminated-command-handlers.mjs +11 -0
  12. package/cli/doctor.mjs +2 -2
  13. package/cli/evidence-command-runner.mjs +41 -31
  14. package/cli/export-evidence.mjs +1 -1
  15. package/cli/fail-policy.mjs +33 -8
  16. package/cli/formatters/terminal-command-formats.mjs +437 -0
  17. package/cli/formatters/terminal-lookup.mjs +758 -0
  18. package/cli/formatters/terminal-metadata.mjs +58 -0
  19. package/cli/formatters/terminal-shared.mjs +83 -0
  20. package/cli/formatters/terminal.mjs +5 -1232
  21. package/cli/history-command-runner.mjs +132 -0
  22. package/cli/inline-command-families.mjs +45 -0
  23. package/cli/inline-command-runner.mjs +55 -0
  24. package/cli/investigation-plan.mjs +9 -2
  25. package/cli/investigation-run.mjs +114 -9
  26. package/cli/lookup-brief.mjs +141 -23
  27. package/cli/lookup-browser-view.mjs +289 -0
  28. package/cli/lookup-browser.mjs +3 -288
  29. package/cli/mail-review.mjs +2 -6
  30. package/cli/offline-artifact-validation.mjs +19 -0
  31. package/cli/review-command-runner.mjs +269 -0
  32. package/cli/runner.mjs +15 -972
  33. package/cli/saved-lookup.mjs +19 -2
  34. package/cli/support-command-runner.mjs +229 -0
  35. package/cli/workflow-command-runner.mjs +129 -0
  36. package/docs/cli-reference.md +17 -10
  37. package/docs/cli.md +5 -3
  38. package/lib/availability.mjs +12 -9
  39. package/lib/bounded-contract-normalizers.mjs +2 -9
  40. package/lib/browser-library-profile.mjs +1 -2
  41. package/lib/client-behavior-profile.mjs +1 -2
  42. package/lib/credential-surface-profile.mjs +1 -1
  43. package/lib/domain-posture-analysis.mjs +10 -1
  44. package/lib/domain-posture.mjs +51 -23
  45. package/lib/evidence-export.mjs +46 -5
  46. package/lib/evidence-report-markdown.mjs +4 -0
  47. package/lib/evidence-report.mjs +50 -0
  48. package/lib/generated/cisa-kev-catalog.mjs +34 -4
  49. package/lib/generated/registrar-standing-catalogue.mjs +367 -0
  50. package/lib/json-record.mjs +12 -0
  51. package/lib/lookup-child-profile-contract.mjs +858 -0
  52. package/lib/lookup-contract-primitives.mjs +98 -0
  53. package/lib/lookup-diagnostics.mjs +96 -0
  54. package/lib/lookup-network-evidence-bounds.mjs +1 -0
  55. package/lib/lookup-response.mjs +367 -0
  56. package/lib/lookup.mjs +28 -443
  57. package/lib/page-role-profile.mjs +1 -3
  58. package/lib/project-metadata.mjs +1 -1
  59. package/lib/registrar-standing-catalogue-contract.mjs +70 -0
  60. package/lib/registrar-standing-contract.mjs +300 -0
  61. package/lib/registrar-standing.mjs +269 -0
  62. package/lib/registry-insights.mjs +2 -8
  63. package/lib/risk-scoring.mjs +26 -10
  64. package/lib/sslbl-certificates.generated.mjs +138 -136
  65. package/lib/structured-data-identity.mjs +1 -3
  66. package/lib/technology-evidence-role.mjs +51 -0
  67. package/lib/threat-intelligence-runtime.mjs +3 -3
  68. package/lib/threat-intelligence-types.mjs +13 -0
  69. package/lib/threatfox-intelligence.mjs +1 -3
  70. package/lib/tls-intelligence.mjs +1 -2
  71. package/lib/urlhaus-intelligence.mjs +1 -3
  72. package/lib/urlscan-intelligence.mjs +1 -3
  73. package/lib/website-security-posture.mjs +1 -2
  74. package/lib/website-technology.mjs +72 -23
  75. package/lib/whois-common-formats.mjs +276 -0
  76. package/lib/whois-dialects.mjs +534 -0
  77. package/lib/whois-parser-support.mjs +305 -0
  78. package/lib/whois-parser.mjs +135 -0
  79. package/lib/whois.mjs +6 -1187
  80. package/package.json +3 -3
  81. package/packages/cases/case-evidence-model.mjs +9 -0
  82. package/packages/cases/case-migration-model.mjs +17 -11
  83. package/packages/cases/case-response-model.mjs +13 -9
  84. package/packages/cases/case-response-packet.mjs +297 -112
  85. package/packages/comparison/page-similarity.mjs +23 -0
  86. package/packages/contracts/case-portability.mjs +227 -63
  87. package/packages/contracts/cli-lookup.mjs +4 -3
  88. package/packages/contracts/domain-control-manifest.mjs +1 -1
  89. package/packages/contracts/investigation-portability.mjs +1 -1
  90. package/packages/contracts/risk-calibration.mjs +2 -2
  91. package/packages/contracts/workspace-portability.mjs +34 -6
  92. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  93. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  94. package/packages/workspace/brand-profile-model.mjs +1 -1
  95. package/packages/workspace/website-snapshot-model.mjs +79 -6
  96. package/third-party-notices.txt +1 -1
  97. package/cli/artifact-structure.mjs +0 -2507
@@ -0,0 +1,534 @@
1
+ // Marker-gated WHOIS registry dialect parsing. Ambiguous aliases stay scoped
2
+ // to complete response signatures and share the generic parser's bounds.
3
+ import { domainToASCII } from 'node:url';
4
+ import { hasNicKgRegistrationEvidence } from "./whois-authority.mjs";
5
+ import { parseIndentedContactBlock, resolveIrnicContact, resolveIsnicRole, } from "./whois-contacts.mjs";
6
+ import { MAX_WHOIS_NAMESERVERS, MAX_WHOIS_STATUSES, WHOIS_DOMAIN_RE, addBoundedWhoisSetValue, assignBoundedWhoisMatch, collectBareWhoisNameservers, } from "./whois-parser-support.mjs";
7
+ import { MAX_WHOIS_FIELD_LENGTH, boundedWhoisValue, parseBoundedWhoisSection, parseIndentedWhoisSubfield, parseIndentedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
8
+ function applyWhoisDialects(text, isRootHop, context) {
9
+ const { fields, nameservers, statuses, truncatedFields } = context;
10
+ const isDnsBelgium = !isRootHop
11
+ && /^%[^\r\n]*\.be Whois Server/im.test(text)
12
+ && /^[ \t]*Registered[ \t]*:/im.test(text);
13
+ const isEurid = !isRootHop
14
+ && /^[ \t]*Script[ \t]*:/im.test(text)
15
+ && /(?:^|\s)(?:www\.)?eurid\.eu(?:\s|\/|$)/im.test(text);
16
+ const isNorid = !isRootHop
17
+ && /^[ \t]*Domain Information[ \t]*$/im.test(text)
18
+ && /^[ \t]*NORID Handle[ \t.]*:/im.test(text)
19
+ && /^[ \t]*Registrar Handle[ \t.]*:/im.test(text)
20
+ && /^[ \t]*Additional information[ \t]*:/im.test(text);
21
+ const isCnnic = !isRootHop
22
+ && /^[ \t]*ROID[ \t]*:/im.test(text)
23
+ && /^[ \t]*Sponsoring Registrar[ \t]*:/im.test(text)
24
+ && /^[ \t]*Registration Time[ \t]*:/im.test(text)
25
+ && /^[ \t]*Expiration Time[ \t]*:/im.test(text);
26
+ const isPunktum = !isRootHop
27
+ && /^[ \t]*Registration period[ \t]*:/im.test(text)
28
+ && /^[ \t]*VID[ \t]*:/im.test(text)
29
+ && /^[ \t]*Nameservers[ \t]*$/im.test(text);
30
+ const isPandi = !isRootHop
31
+ && /^[ \t]*Domain ID[ \t]*:/im.test(text)
32
+ && /^[ \t]*Sponsoring Registrar Organization[ \t]*:/im.test(text);
33
+ const isIsocIl = !isRootHop
34
+ && /^[ \t]*query[ \t]*:/im.test(text)
35
+ && /^[ \t]*reg-name[ \t]*:/im.test(text)
36
+ && /^[ \t]*validity[ \t]*:/im.test(text);
37
+ const isTwnic = !isRootHop
38
+ && /^[ \t]*Record created on[ \t]*:/im.test(text)
39
+ && /^[ \t]*Record expires on[ \t]*:/im.test(text)
40
+ && /^[ \t]*Registration Service Provider[ \t]*:/im.test(text);
41
+ const isRegisterBg = !isRootHop
42
+ && /^[ \t]*registration status[ \t]*:/im.test(text)
43
+ && /^[ \t]*NAME SERVER INFORMATION[ \t]*:[ \t]*$/im.test(text);
44
+ const isEif = !isRootHop
45
+ && /Estonia \.ee Top Level Domain WHOIS server/i.test(text)
46
+ && /^[ \t]*Domain[ \t]*:[ \t]*$/im.test(text)
47
+ && /^[ \t]*Name servers[ \t]*:[ \t]*$/im.test(text);
48
+ const isIszt = !isRootHop
49
+ && /^% Whois server[^\r\n]*hu ccTLD/im.test(text)
50
+ && /^[ \t]*record created[ \t]*:/im.test(text);
51
+ const isIsnic = !isRootHop
52
+ && /^% This is the ISNIC Whois server\./im.test(text)
53
+ && /^[ \t]*source[ \t]*:[ \t]*ISNIC(?:\s|$)/im.test(text);
54
+ const isNicLv = !isRootHop
55
+ && /^[ \t]*\[Domain\][ \t]*$/im.test(text)
56
+ && /^[ \t]*\[Holder\][ \t]*$/im.test(text)
57
+ && /^[ \t]*\[Whois\][ \t]*$/im.test(text);
58
+ const isSidn = !isRootHop
59
+ && /^[ \t]*Domain nameservers[ \t]*:[ \t]*$/im.test(text)
60
+ && /^[ \t]*Abuse Contact[ \t]*:[ \t]*$/im.test(text)
61
+ && /^[ \t]*Record maintained by[ \t]*:/im.test(text);
62
+ const isRnids = !isRootHop
63
+ && /^[ \t]*Registration date[ \t]*:/im.test(text)
64
+ && /^[ \t]*Modification date[ \t]*:/im.test(text)
65
+ && /^[ \t]*DNSSEC signed[ \t]*:/im.test(text);
66
+ const isCctldBy = !isRootHop
67
+ && /^[ \t]*Domain name[ \t]*:/im.test(text)
68
+ && /^[ \t]*Registration or other identification number[ \t]*:/im.test(text);
69
+ const isHkirc = !isRootHop
70
+ && /^[ \t]*Domain Name Information[ \t]*:[ \t]*$/im.test(text)
71
+ && /^[ \t]*Name Servers Information[ \t]*:[ \t]*$/im.test(text)
72
+ && /^[ \t]*Domain Name Commencement Date[ \t]*:/im.test(text);
73
+ const isIrnic = !isRootHop
74
+ && /^[ \t]*source[ \t]*:[ \t]*IRNIC(?:\s|$)/im.test(text)
75
+ && /^[ \t]*holder-c[ \t]*:/im.test(text)
76
+ && /^[ \t]*nic-hdl[ \t]*:/im.test(text);
77
+ const isNicKz = !isRootHop
78
+ && /^[ \t]*Domain Name[ \t.]*:/im.test(text)
79
+ && /^[ \t]*Current Registar[ \t]*:/im.test(text)
80
+ && /^[ \t]*Primary server[ \t.]*:/im.test(text);
81
+ const isDnsLu = !isRootHop
82
+ && /^[ \t]*domainname[ \t]*:/im.test(text)
83
+ && /^[ \t]*domaintype[ \t]*:/im.test(text)
84
+ && /^[ \t]*registrar-name[ \t]*:/im.test(text);
85
+ const isNicMd = !isRootHop
86
+ && /^[ \t]*Domain[ \t]+name[ \t]*:/im.test(text)
87
+ && /^[ \t]*Domain state[ \t]*:/im.test(text)
88
+ && /^[ \t]*Registered on[ \t]*:/im.test(text);
89
+ const isThnic = !isRootHop
90
+ && /^[ \t]*Domain Holder Organization[ \t]*:/im.test(text)
91
+ && /^[ \t]*Created date[ \t]*:/im.test(text)
92
+ && /^[ \t]*Exp date[ \t]*:/im.test(text);
93
+ const isAtiTn = !isRootHop
94
+ && /^[ \t]*NIC Whois server for cTLDs[ \t.]*:/im.test(text)
95
+ && /^[ \t]*Domain name[ \t.]*:/im.test(text)
96
+ && /^[ \t]*Creation date[ \t.]*:/im.test(text);
97
+ const isMonic = !isRootHop
98
+ && /^%[ \t]*Monic Whois Server Version[ \t]+\d/im.test(text)
99
+ && /^[ \t]*Domain Name[ \t]*:/im.test(text)
100
+ && /^[ \t]*Record created on[ \t]+[^:\r\n]/im.test(text)
101
+ && /^[ \t]*Domain name servers[ \t]*:[ \t]*$/im.test(text);
102
+ const isNicKg = !isRootHop && hasNicKgRegistrationEvidence(text);
103
+ // A small set of ccTLD dialects reuse terse labels that would be unsafe
104
+ // as global aliases (for example `org:` and `state:` also occur inside
105
+ // contact/address blocks). Gate those fields on registry-specific marker
106
+ // combinations, while retaining the same scalar/list bounds as the
107
+ // generic parser and leaving endpoint discovery and authority untouched.
108
+ if (!isRootHop) {
109
+ if (isRegisterBg) {
110
+ const domainLine = text.match(/^[ \t]*DOMAIN NAME[ \t]*:[ \t]*([^\r\n]+)/im);
111
+ const rawDomainLine = domainLine?.[1];
112
+ if (rawDomainLine) {
113
+ const boundedLine = rawDomainLine.slice(0, MAX_WHOIS_FIELD_LENGTH);
114
+ const parenthesized = boundedLine.match(/\(([a-z0-9.-]{1,253})\)[ \t]*$/i)?.[1] || '';
115
+ const leadingAscii = boundedLine.match(/^([a-z0-9.-]{1,253})(?:[ \t]|$)/i)?.[1] || '';
116
+ const unicodeDomain = boundedLine.replace(/[ \t]+\([^\r\n()]{1,253}\)[ \t]*$/, '').trim();
117
+ const domain = [parenthesized, leadingAscii, domainToASCII(unicodeDomain)]
118
+ .find((candidate) => WHOIS_DOMAIN_RE.test(candidate));
119
+ const bounded = boundedWhoisValue(domain, whoisFieldLimit('domainName'));
120
+ if (bounded.value)
121
+ fields.domainName = bounded.value;
122
+ if (bounded.truncated)
123
+ truncatedFields.add('domainName');
124
+ }
125
+ for (const match of text.matchAll(/^[ \t]*registration status[ \t]*:[ \t]*([^\r\n]+)/gim)) {
126
+ if (addBoundedWhoisSetValue(statuses, match[1], {
127
+ maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
128
+ field: 'statuses', truncatedFields,
129
+ }) === 'capped')
130
+ break;
131
+ }
132
+ collectBareWhoisNameservers(text, /^[ \t]*NAME SERVER INFORMATION[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
133
+ }
134
+ if (isEif) {
135
+ const domainSection = parseBoundedWhoisSection(text, /^[ \t]*Domain[ \t]*:[ \t]*$/im);
136
+ const registrantSection = parseBoundedWhoisSection(text, /^[ \t]*Registrant[ \t]*:[ \t]*$/im);
137
+ const adminSection = parseBoundedWhoisSection(text, /^[ \t]*Administrative contact[ \t]*:[ \t]*$/im);
138
+ const techSection = parseBoundedWhoisSection(text, /^[ \t]*Technical contact[ \t]*:[ \t]*$/im);
139
+ const registrarSection = parseBoundedWhoisSection(text, /^[ \t]*Registrar[ \t]*:[ \t]*$/im);
140
+ assignBoundedWhoisMatch(domainSection, fields, 'domainName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
141
+ assignBoundedWhoisMatch(registrantSection, fields, 'registrantName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
142
+ assignBoundedWhoisMatch(registrantSection, fields, 'registrantId', /^[ \t]*org id[ \t]*:[ \t]*(.+)$/im, truncatedFields);
143
+ assignBoundedWhoisMatch(registrantSection, fields, 'registrantCountry', /^[ \t]*country[ \t]*:[ \t]*(.+)$/im, truncatedFields);
144
+ assignBoundedWhoisMatch(adminSection, fields, 'adminName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
145
+ assignBoundedWhoisMatch(techSection, fields, 'techName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
146
+ assignBoundedWhoisMatch(registrarSection, fields, 'registrar', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
147
+ assignBoundedWhoisMatch(registrarSection, fields, 'registrarUrl', /^[ \t]*url[ \t]*:[ \t]*(.+)$/im, truncatedFields);
148
+ }
149
+ if (isIszt) {
150
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*record created[ \t]*:[ \t]*(.+)$/im, truncatedFields);
151
+ }
152
+ if (isIsnic) {
153
+ const handle = text.match(/^[ \t]*registrant[ \t]*:[ \t]*(.+)$/im)?.[1] || null;
154
+ if (handle) {
155
+ const boundedHandle = boundedWhoisValue(handle, whoisFieldLimit('registrantId'));
156
+ if (boundedHandle.value)
157
+ fields.registrantId = boundedHandle.value;
158
+ if (boundedHandle.truncated)
159
+ truncatedFields.add('registrantId');
160
+ delete fields.registrantName;
161
+ const role = resolveIsnicRole(text, boundedHandle.value);
162
+ if (role) {
163
+ if (role.org.value)
164
+ fields.registrantOrg = role.org.value;
165
+ if (role.email.value)
166
+ fields.registrantEmail = role.email.value;
167
+ if (role.phone.value)
168
+ fields.registrantPhone = role.phone.value;
169
+ if (role.address.value)
170
+ fields.registrantAddress = role.address.value;
171
+ if (role.org.truncated)
172
+ truncatedFields.add('registrantOrg');
173
+ if (role.email.truncated)
174
+ truncatedFields.add('registrantEmail');
175
+ if (role.phone.truncated)
176
+ truncatedFields.add('registrantPhone');
177
+ if (role.address.truncated || role.truncated)
178
+ truncatedFields.add('registrantAddress');
179
+ }
180
+ }
181
+ assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
182
+ assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
183
+ assignBoundedWhoisMatch(text, fields, 'billingId', /^[ \t]*billing-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
184
+ }
185
+ if (isNicLv) {
186
+ const holderSection = parseBoundedWhoisSection(text, /^[ \t]*\[Holder\][ \t]*$/im);
187
+ assignBoundedWhoisMatch(holderSection, fields, 'registrantName', /^[ \t]*Name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
188
+ assignBoundedWhoisMatch(holderSection, fields, 'registrantId', /^[ \t]*RegNr[ \t]*:[ \t]*(.+)$/im, truncatedFields);
189
+ assignBoundedWhoisMatch(holderSection, fields, 'registrantAddress', /^[ \t]*Address[ \t]*:[ \t]*(.+)$/im, truncatedFields);
190
+ assignBoundedWhoisMatch(holderSection, fields, 'registrantCountry', /^[ \t]*Country[ \t]*:[ \t]*(.+)$/im, truncatedFields);
191
+ }
192
+ if (isSidn) {
193
+ const registrar = parseIndentedWhoisValue(text, /^[ \t]*Registrar[ \t]*:[ \t]*$/im, whoisFieldLimit('registrar'));
194
+ if (registrar?.value)
195
+ fields.registrar = registrar.value;
196
+ if (registrar?.truncated)
197
+ truncatedFields.add('registrar');
198
+ const abuse = parseIndentedContactBlock(text, /^[ \t]*Abuse Contact[ \t]*:[ \t]*$/im);
199
+ if (abuse?.email)
200
+ fields.abuseEmail = abuse.email;
201
+ if (abuse?.phone)
202
+ fields.abusePhone = abuse.phone;
203
+ if (abuse?.truncated) {
204
+ truncatedFields.add('abuseEmail');
205
+ truncatedFields.add('abusePhone');
206
+ }
207
+ collectBareWhoisNameservers(text, /^[ \t]*Domain nameservers[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
208
+ }
209
+ if (isRnids) {
210
+ assignBoundedWhoisMatch(text, fields, 'adminName', /^[ \t]*Administrative contact[ \t]*:[ \t]*(.+)$/im, truncatedFields);
211
+ assignBoundedWhoisMatch(text, fields, 'techName', /^[ \t]*Technical contact[ \t]*:[ \t]*(.+)$/im, truncatedFields);
212
+ assignBoundedWhoisMatch(text, fields, 'dnssec', /^[ \t]*DNSSEC signed[ \t]*:[ \t]*(.+)$/im, truncatedFields);
213
+ for (const match of text.matchAll(/^[ \t]*Domain status[ \t]*:[ \t]*(.*?)(?:[ \t]+https?:\/\/\S+)?[ \t]*$/gim)) {
214
+ if (addBoundedWhoisSetValue(statuses, match[1], {
215
+ maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
216
+ field: 'statuses', truncatedFields,
217
+ }) === 'capped')
218
+ break;
219
+ }
220
+ }
221
+ if (isCctldBy) {
222
+ assignBoundedWhoisMatch(text, fields, 'registrantOrg', /^[ \t]*Org[ \t]*:[ \t]*(.+)$/im, truncatedFields);
223
+ assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*Registration or other identification number[ \t]*:[ \t]*(.+)$/im, truncatedFields);
224
+ }
225
+ if (isHkirc) {
226
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Domain Name Commencement Date[ \t]*:[ \t]*(.+)$/im, truncatedFields);
227
+ collectBareWhoisNameservers(text, /^[ \t]*Name Servers Information[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
228
+ }
229
+ if (isIrnic) {
230
+ const registrantId = text.match(/^[ \t]*holder-c[ \t]*:[ \t]*(.+)$/im)?.[1] || null;
231
+ if (registrantId) {
232
+ assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*holder-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
233
+ const contact = resolveIrnicContact(text, fields.registrantId);
234
+ if (contact) {
235
+ if (contact.name.value)
236
+ fields.registrantName = contact.name.value;
237
+ if (contact.org.value)
238
+ fields.registrantOrg = contact.org.value;
239
+ if (contact.email.value)
240
+ fields.registrantEmail = contact.email.value;
241
+ if (contact.phone.value)
242
+ fields.registrantPhone = contact.phone.value;
243
+ if (contact.address.value)
244
+ fields.registrantAddress = contact.address.value;
245
+ if (contact.name.truncated)
246
+ truncatedFields.add('registrantName');
247
+ if (contact.org.truncated)
248
+ truncatedFields.add('registrantOrg');
249
+ if (contact.email.truncated)
250
+ truncatedFields.add('registrantEmail');
251
+ if (contact.phone.truncated)
252
+ truncatedFields.add('registrantPhone');
253
+ if (contact.address.truncated || contact.truncated)
254
+ truncatedFields.add('registrantAddress');
255
+ }
256
+ }
257
+ assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
258
+ assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
259
+ assignBoundedWhoisMatch(text, fields, 'billingId', /^[ \t]*bill-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
260
+ }
261
+ if (isNicKz) {
262
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Domain created[ \t]*:[ \t]*(.+)$/im, truncatedFields);
263
+ assignBoundedWhoisMatch(text, fields, 'updatedDate', /^[ \t]*Last modified[ \t.]*:[ \t]*(.+)$/im, truncatedFields);
264
+ assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Current Registar[ \t]*:[ \t]*(.+)$/im, truncatedFields);
265
+ for (const pattern of [
266
+ /^[ \t]*Primary server[ \t.]*:[ \t]*([a-zA-Z0-9.-]+)/gim,
267
+ /^[ \t]*Secondary server[ \t.]*:[ \t]*([a-zA-Z0-9.-]+)/gim,
268
+ ]) {
269
+ for (const match of text.matchAll(pattern)) {
270
+ const result = addBoundedWhoisSetValue(nameservers, match[1], {
271
+ maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
272
+ field: 'nameservers', truncatedFields,
273
+ });
274
+ if (result === 'capped')
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ if (isDnsLu) {
280
+ assignBoundedWhoisMatch(text, fields, 'domainName', /^[ \t]*domainname[ \t]*:[ \t]*(.+)$/im, truncatedFields);
281
+ assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*registrar-name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
282
+ assignBoundedWhoisMatch(text, fields, 'registrarUrl', /^[ \t]*registrar-url[ \t]*:[ \t]*(.+)$/im, truncatedFields);
283
+ }
284
+ if (isNicMd) {
285
+ assignBoundedWhoisMatch(text, fields, 'domainName', /^[ \t]*Domain[ \t]+name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
286
+ for (const match of text.matchAll(/^[ \t]*Domain state[ \t]*:[ \t]*([^\r\n]+)/gim)) {
287
+ if (addBoundedWhoisSetValue(statuses, match[1], {
288
+ maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
289
+ field: 'statuses', truncatedFields,
290
+ }) === 'capped')
291
+ break;
292
+ }
293
+ }
294
+ if (isThnic) {
295
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Created date[ \t]*:[ \t]*(.+)$/im, truncatedFields);
296
+ assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Exp date[ \t]*:[ \t]*(.+)$/im, truncatedFields);
297
+ assignBoundedWhoisMatch(text, fields, 'registrantOrg', /^[ \t]*Domain Holder Organization[ \t]*:[ \t]*(.+)$/im, truncatedFields);
298
+ assignBoundedWhoisMatch(text, fields, 'registrantCountry', /^[ \t]*Domain Holder Country[ \t]*:[ \t]*(.+)$/im, truncatedFields);
299
+ assignBoundedWhoisMatch(text, fields, 'registrantStreet', /^[ \t]*Domain Holder Street[ \t]*:[ \t]*(.+)$/im, truncatedFields);
300
+ }
301
+ if (isMonic) {
302
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Record created on[ \t]+(.+)$/im, truncatedFields);
303
+ collectBareWhoisNameservers(text, /^[ \t]*Domain name servers[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
304
+ }
305
+ if (isNicKg) {
306
+ const domainHeader = text.match(/^[ \t]*Domain[ \t]+([a-z0-9.-]+)[ \t]+\(([A-Z][A-Z0-9_-]*)\)[ \t]*$/im);
307
+ if (domainHeader) {
308
+ const domain = boundedWhoisValue(domainHeader[1], whoisFieldLimit('domainName'));
309
+ if (domain.value)
310
+ fields.domainName = domain.value;
311
+ if (domain.truncated)
312
+ truncatedFields.add('domainName');
313
+ addBoundedWhoisSetValue(statuses, domainHeader[2], {
314
+ maxEntries: MAX_WHOIS_STATUSES,
315
+ maxLength: 160,
316
+ field: 'statuses',
317
+ truncatedFields,
318
+ });
319
+ }
320
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Record created[ \t]*:[ \t]*(.+)$/im, truncatedFields);
321
+ assignBoundedWhoisMatch(text, fields, 'updatedDate', /^[ \t]*Record last updated on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
322
+ assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Record expires on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
323
+ collectBareWhoisNameservers(text, /^[ \t]*Name servers in the listed order[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
324
+ }
325
+ const isRegistroBr = /^[ \t]*owner-c[ \t]*:/im.test(text)
326
+ && /^[ \t]*country[ \t]*:[ \t]*BR(?:\s|$)/im.test(text);
327
+ if (isRegistroBr) {
328
+ assignBoundedWhoisMatch(text, fields, 'registrantName', /^[ \t]*owner[ \t]*:[ \t]*(.+)$/im, truncatedFields);
329
+ assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*owner-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
330
+ assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
331
+ assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
332
+ }
333
+ const isAfnic = /^[ \t]*source[ \t]*:[ \t]*FRNIC(?:\s|$)/im.test(text);
334
+ if (isAfnic) {
335
+ assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*holder-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
336
+ assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
337
+ assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
338
+ for (const match of text.matchAll(/^[ \t]*eppstatus[ \t]*:[ \t]*(.+)$/gim)) {
339
+ if (addBoundedWhoisSetValue(statuses, match[1], {
340
+ maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
341
+ field: 'statuses', truncatedFields,
342
+ }) === 'capped')
343
+ break;
344
+ }
345
+ }
346
+ const isTci = /^[ \t]*source[ \t]*:[ \t]*TCI(?:\s|$)/im.test(text)
347
+ && /^[ \t]*paid-till[ \t]*:/im.test(text);
348
+ if (isTci) {
349
+ assignBoundedWhoisMatch(text, fields, 'registrantOrg', /^[ \t]*org[ \t]*:[ \t]*(.+)$/im, truncatedFields);
350
+ for (const match of text.matchAll(/^[ \t]*state[ \t]*:[ \t]*(.+)$/gim)) {
351
+ if (addBoundedWhoisSetValue(statuses, match[1], {
352
+ maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
353
+ field: 'statuses', truncatedFields,
354
+ }) === 'capped')
355
+ break;
356
+ }
357
+ }
358
+ const isInternetstiftelsen = /^[ \t]*registry-lock[ \t]*:/im.test(text)
359
+ && /^[ \t]*holder[ \t]*:/im.test(text);
360
+ if (isInternetstiftelsen) {
361
+ assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*holder[ \t]*:[ \t]*(.+)$/im, truncatedFields);
362
+ for (const match of text.matchAll(/^[ \t]*state[ \t]*:[ \t]*(.+)$/gim)) {
363
+ if (addBoundedWhoisSetValue(statuses, match[1], {
364
+ maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
365
+ field: 'statuses', truncatedFields,
366
+ }) === 'capped')
367
+ break;
368
+ }
369
+ }
370
+ const isNask = /^[ \t]*DOMAIN NAME[ \t]*:/im.test(text)
371
+ && /^[ \t]*registrant type[ \t]*:/im.test(text)
372
+ && /^[ \t]*renewal date[ \t]*:/im.test(text)
373
+ && /^[ \t]*REGISTRAR[ \t]*:[ \t]*$/im.test(text);
374
+ if (isNask) {
375
+ if (!fields.registrar) {
376
+ const registrarHeader = text.match(/^[ \t]*REGISTRAR[ \t]*:[ \t]*$/im);
377
+ const followingLines = registrarHeader
378
+ ? text.slice((registrarHeader.index ?? 0) + registrarHeader[0].length).split('\n').slice(0, 8)
379
+ : [];
380
+ for (const line of followingLines) {
381
+ if (!line.trim())
382
+ continue;
383
+ const bounded = boundedWhoisValue(line, whoisFieldLimit('registrar'));
384
+ if (bounded.value && !/^[a-z][a-z0-9+.-]*:\/\//i.test(bounded.value)
385
+ && !/^[a-z][a-z0-9 -]{0,50}:[ \t]/i.test(bounded.value)) {
386
+ fields.registrar = bounded.value;
387
+ }
388
+ if (bounded.truncated)
389
+ truncatedFields.add('registrar');
390
+ break;
391
+ }
392
+ }
393
+ const nameserverHeader = text.match(/^[ \t]*nameservers[ \t]*:[ \t]*(.*)$/im);
394
+ if (nameserverHeader) {
395
+ const candidates = [
396
+ nameserverHeader[1] ?? '',
397
+ ...text.slice((nameserverHeader.index ?? 0) + nameserverHeader[0].length)
398
+ .split('\n').slice(1, MAX_WHOIS_NAMESERVERS + 2),
399
+ ];
400
+ let found = 0;
401
+ for (const line of candidates) {
402
+ if (line === undefined)
403
+ continue;
404
+ const trimmed = line.trim();
405
+ if (!trimmed) {
406
+ if (found > 0)
407
+ break;
408
+ continue;
409
+ }
410
+ const hostMatch = trimmed.match(/^([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\.?(?:\s|$)/);
411
+ if (!hostMatch)
412
+ break;
413
+ const hostname = hostMatch[1];
414
+ if (!hostname)
415
+ break;
416
+ const result = addBoundedWhoisSetValue(nameservers, hostname.replace(/\.$/, ''), {
417
+ maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
418
+ field: 'nameservers', truncatedFields,
419
+ });
420
+ if (result === 'capped')
421
+ break;
422
+ if (result === 'added')
423
+ found += 1;
424
+ }
425
+ }
426
+ }
427
+ if ((isDnsBelgium || isEurid) && !fields.registrar) {
428
+ const registrar = parseIndentedWhoisSubfield(text, /^[ \t]*Registrar[ \t]*:[ \t]*$/im, /^[ \t]*Name[ \t]*:[ \t]*(.+)$/i, whoisFieldLimit('registrar'));
429
+ if (registrar?.value)
430
+ fields.registrar = registrar.value;
431
+ if (registrar?.truncated)
432
+ truncatedFields.add('registrar');
433
+ }
434
+ if (isNorid) {
435
+ assignBoundedWhoisMatch(text, fields, 'registryDomainId', /^[ \t]*NORID Handle[ \t.]*:[ \t]*(.+)$/im, truncatedFields);
436
+ assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Registrar Handle[ \t.]*:[ \t]*(.+)$/im, truncatedFields);
437
+ }
438
+ // These aliases are meaningful only inside their registry's
439
+ // distinctive response dialect. Keeping them marker-gated avoids
440
+ // treating generic contact IDs, validity text, or service-provider
441
+ // prose as domain-level evidence in unrelated WHOIS responses.
442
+ if (isCnnic) {
443
+ assignBoundedWhoisMatch(text, fields, 'registryDomainId', /^[ \t]*ROID[ \t]*:[ \t]*(.+)$/im, truncatedFields);
444
+ assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Expiration Time[ \t]*:[ \t]*(.+)$/im, truncatedFields);
445
+ }
446
+ if (isPandi) {
447
+ assignBoundedWhoisMatch(text, fields, 'registryDomainId', /^[ \t]*Domain ID[ \t]*:[ \t]*(.+)$/im, truncatedFields);
448
+ assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Sponsoring Registrar Organization[ \t]*:[ \t]*(.+)$/im, truncatedFields);
449
+ }
450
+ if (isIsocIl) {
451
+ assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*validity[ \t]*:[ \t]*(.+)$/im, truncatedFields);
452
+ }
453
+ if (isTwnic) {
454
+ assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Registration Service Provider[ \t]*:[ \t]*(.+)$/im, truncatedFields);
455
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Record created on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
456
+ assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Record expires on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
457
+ }
458
+ }
459
+ // Sectioned legacy port-43 responses use a distinctive pair of section
460
+ // headers and place several values on following indented lines. Gate the
461
+ // dialect on both markers so generic responses with one blank field are
462
+ // not reinterpreted. This is parser compatibility only: IANA still
463
+ // selects the endpoint and authority analysis remains unchanged.
464
+ const isSectionedRegistryResponse = !isRootHop
465
+ && /^[ \t]*Relevant dates[ \t]*:[ \t]*$/im.test(text)
466
+ && /^[ \t]*Registration status[ \t]*:[ \t]*$/im.test(text);
467
+ if (isSectionedRegistryResponse) {
468
+ for (const [key, headerRe] of [
469
+ ['domainName', /^[ \t]*Domain(?: name)?[ \t]*:[ \t]*$/im],
470
+ ['registrantName', /^[ \t]*Registrant[ \t]*:[ \t]*$/im],
471
+ ['registrar', /^[ \t]*Registrar[ \t]*:[ \t]*$/im],
472
+ ]) {
473
+ if (fields[key])
474
+ continue;
475
+ const parsed = parseIndentedWhoisValue(text, headerRe, whoisFieldLimit(key));
476
+ if (!parsed?.value)
477
+ continue;
478
+ fields[key] = parsed.value;
479
+ if (parsed.truncated)
480
+ truncatedFields.add(key);
481
+ }
482
+ const registrationStatus = parseIndentedWhoisValue(text, /^[ \t]*Registration status[ \t]*:[ \t]*$/im, 160);
483
+ if (registrationStatus?.value) {
484
+ addBoundedWhoisSetValue(statuses, registrationStatus.value, {
485
+ maxEntries: MAX_WHOIS_STATUSES,
486
+ maxLength: 160,
487
+ field: 'statuses',
488
+ truncatedFields,
489
+ });
490
+ }
491
+ const isChannelIslandsResponse = /^[ \t]*Domain Status[ \t]*:[ \t]*$/im.test(text)
492
+ && /^[ \t]*WHOIS lookup made on[ \t]+[^\r\n]+$/im.test(text);
493
+ if (isChannelIslandsResponse) {
494
+ assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Registered on[ \t]+(.+)$/im, truncatedFields);
495
+ const domainStatus = parseIndentedWhoisValue(text, /^[ \t]*Domain Status[ \t]*:[ \t]*$/im, 160);
496
+ if (domainStatus?.value) {
497
+ addBoundedWhoisSetValue(statuses, domainStatus.value, {
498
+ maxEntries: MAX_WHOIS_STATUSES,
499
+ maxLength: 160,
500
+ field: 'statuses',
501
+ truncatedFields,
502
+ });
503
+ }
504
+ }
505
+ }
506
+ return {
507
+ isDnsBelgium,
508
+ isEurid,
509
+ isNorid,
510
+ isCnnic,
511
+ isPunktum,
512
+ isPandi,
513
+ isIsocIl,
514
+ isTwnic,
515
+ isRegisterBg,
516
+ isEif,
517
+ isIszt,
518
+ isIsnic,
519
+ isNicLv,
520
+ isSidn,
521
+ isRnids,
522
+ isCctldBy,
523
+ isHkirc,
524
+ isIrnic,
525
+ isNicKz,
526
+ isDnsLu,
527
+ isNicMd,
528
+ isThnic,
529
+ isAtiTn,
530
+ isMonic,
531
+ isNicKg,
532
+ };
533
+ }
534
+ export { applyWhoisDialects };