@slicedearth/whoisleuth-cli 2.0.1 → 2.2.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 +16 -9
  37. package/docs/cli.md +16 -2
  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 +47 -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/registrar-standing-catalogue-contract.mjs +70 -0
  59. package/lib/registrar-standing-contract.mjs +300 -0
  60. package/lib/registrar-standing.mjs +269 -0
  61. package/lib/registry-insights.mjs +2 -8
  62. package/lib/risk-scoring.mjs +26 -10
  63. package/lib/sslbl-certificates.generated.mjs +138 -136
  64. package/lib/structured-data-identity.mjs +1 -3
  65. package/lib/technology-evidence-role.mjs +51 -0
  66. package/lib/threat-intelligence-runtime.mjs +3 -3
  67. package/lib/threat-intelligence-types.mjs +13 -0
  68. package/lib/threatfox-intelligence.mjs +1 -3
  69. package/lib/tls-intelligence.mjs +1 -2
  70. package/lib/urlhaus-intelligence.mjs +1 -3
  71. package/lib/urlscan-intelligence.mjs +1 -3
  72. package/lib/website-security-posture.mjs +1 -2
  73. package/lib/website-technology.mjs +72 -23
  74. package/lib/whois-common-formats.mjs +276 -0
  75. package/lib/whois-dialects.mjs +534 -0
  76. package/lib/whois-parser-support.mjs +305 -0
  77. package/lib/whois-parser.mjs +135 -0
  78. package/lib/whois.mjs +6 -1187
  79. package/package.json +2 -2
  80. package/packages/cases/case-evidence-model.mjs +9 -0
  81. package/packages/cases/case-migration-model.mjs +17 -11
  82. package/packages/cases/case-record-operations.mjs +235 -0
  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 +235 -61
  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/interchange/external-findings-import.mjs +113 -45
  93. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  94. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  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,437 @@
1
+ import { unicodeDomainFromAscii } from "../../lib/idn-confusables.mjs";
2
+ import { registryAccessProfileLabel } from "../registry-access.mjs";
3
+ import { appendDeliveryMetadataLines } from "./terminal-metadata.mjs";
4
+ import { MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, safeTerminalValue, terminalCount, terminalCountSummary, terminalRecord, titleCase, } from "./terminal-shared.mjs";
5
+ const MAX_CT_TERMINAL_MATCHES = 100;
6
+ const MAX_CT_TERMINAL_HOSTNAMES = 5;
7
+ const MAX_DISCOVER_TERMINAL_CANDIDATES = 200;
8
+ const MAX_POSTURE_TERMINAL_RECORDS = 5;
9
+ const MAX_RISK_CALIBRATION_TERMINAL_RECORDS = 100;
10
+ function formatTerminalRegistrySupport(document) {
11
+ const profile = terminalRecord(document.profile);
12
+ const rdap = terminalRecord(profile.rdap);
13
+ const whois = terminalRecord(profile.whois);
14
+ const verification = terminalRecord(document.verification);
15
+ const fixtures = Array.isArray(verification.fixtureScenarios) ? verification.fixtureScenarios : [];
16
+ const files = Array.isArray(verification.files) ? verification.files : [];
17
+ const documentation = Array.isArray(verification.documentationUrls) ? verification.documentationUrls : [];
18
+ const standards = terminalRecord(document.standardsCoverage);
19
+ const genericCoverage = terminalRecord(standards.genericAndRestricted);
20
+ const supportLabel = (value) => titleCase(safeTerminalValue(value, 'unknown').replaceAll('-', '_'));
21
+ const lines = [
22
+ `Input ${safeTerminalValue(document.requestedInput)}`,
23
+ `Suffix .${safeTerminalValue(document.suffix)}`,
24
+ `Catalogue Version ${safeTerminalValue(document.catalogueVersion, '0')}`,
25
+ `gTLD RDAP ${safeTerminalValue(genericCoverage.rdapCovered, '0')} / ${safeTerminalValue(genericCoverage.total, '0')}`,
26
+ `Coverage date ${safeTerminalValue(standards.verifiedAt, 'unknown')}`,
27
+ `Profile ${profile.explicitSuffixProfile ? 'Explicit suffix profile' : 'Generic IANA discovery profile'}`,
28
+ `Profile ID ${safeTerminalValue(profile.id)}`,
29
+ `Registry class ${supportLabel(profile.registryClass)}`,
30
+ `Coverage ${supportLabel(profile.coverageState)}`,
31
+ `RDAP discovery ${registryAccessProfileLabel(rdap.discovery)}`,
32
+ `RDAP access ${registryAccessProfileLabel(rdap.accessProfile)}`,
33
+ `WHOIS discovery ${registryAccessProfileLabel(whois.discovery)}`,
34
+ `WHOIS access ${registryAccessProfileLabel(whois.accessProfile)}`,
35
+ `WHOIS query ${supportLabel(whois.queryProfile)}`,
36
+ `WHOIS scope ${supportLabel(whois.queryScope)}`,
37
+ `WHOIS encoding ${supportLabel(whois.encodingProfile)}`,
38
+ `WHOIS parser ${supportLabel(whois.parserProfile)}`,
39
+ ...(typeof profile.officialLookupUrl === 'string' && profile.officialLookupUrl
40
+ ? [`Official lookup ${safeTerminalValue(profile.officialLookupUrl)}`]
41
+ : []),
42
+ `Fixture states ${fixtures.length ? fixtures.map((value) => supportLabel(value)).join(', ') : 'None documented'}`,
43
+ ];
44
+ for (const file of files)
45
+ lines.push(`Verified by ${safeTerminalValue(file)}`);
46
+ for (const url of documentation)
47
+ lines.push(`Documentation ${safeTerminalValue(url)}`);
48
+ lines.push(`Limitation ${safeTerminalValue(document.limitation)}`, '', safeTerminalValue(terminalRecord(document.interpretation).statement));
49
+ return `${lines.join('\n')}\n`;
50
+ }
51
+ function formatTerminalBulk(items, metadata) {
52
+ const lines = items.map((item) => {
53
+ if (!item.ok)
54
+ return `! ${safeTerminalValue(item.query)}: ${safeTerminalValue(item.error, 'Lookup failed')}`;
55
+ const result = terminalRecord(item.result);
56
+ const availability = terminalRecord(result.availability);
57
+ const state = titleCase(availability.state);
58
+ const confidence = titleCase(availability.confidence);
59
+ return `✓ ${safeTerminalValue(item.query)}: ${state} (${confidence} confidence)`;
60
+ });
61
+ const succeeded = items.filter((item) => item.ok).length;
62
+ lines.push('');
63
+ const collected = metadata.collectedTotal ?? items.length;
64
+ const filter = metadata.filter && metadata.filter !== 'all'
65
+ ? ` · ${items.length} matched ${metadata.filter}`
66
+ : '';
67
+ lines.push(`${collected} collected${filter} · ${succeeded} succeeded · ${items.length - succeeded} failed in output · ${metadata.duplicates || 0} duplicates removed`);
68
+ return `${lines.join('\n')}\n`;
69
+ }
70
+ function formatTerminalCtSearch(document) {
71
+ const matches = Array.isArray(document.matches) ? document.matches : [];
72
+ const certificateGroups = Array.isArray(document.certificateGroups) ? document.certificateGroups : [];
73
+ const crossDomainGroups = certificateGroups.filter((value) => {
74
+ const group = terminalRecord(value);
75
+ return Array.isArray(group.domains) && group.domains.length > 1;
76
+ });
77
+ const visible = matches.slice(0, MAX_CT_TERMINAL_MATCHES);
78
+ const observation = terminalRecord(document.observation);
79
+ const lines = [
80
+ `Keyword ${safeTerminalValue(document.keyword)}`,
81
+ `CT status ${titleCase(observation.status || (document.truncated ? 'partial' : 'success'))}`,
82
+ `Certificates ${safeTerminalValue(document.certCount, '0')}`,
83
+ `Observed hosts ${safeTerminalValue(Array.isArray(document.domains) ? document.domains.length : 0, '0')}`,
84
+ `Matches ${safeTerminalValue(matches.length, '0')}`,
85
+ `Issuance groups ${safeTerminalValue(certificateGroups.length, '0')} (${safeTerminalValue(crossDomainGroups.length, '0')} cross-domain)`,
86
+ `Truncated ${document.truncated ? 'Yes' : 'No'}`,
87
+ `Group cap ${document.certificateGroupsTruncated ? 'Reached' : 'Not reached'}`,
88
+ '',
89
+ ];
90
+ if (!visible.length) {
91
+ lines.push('No structured registrable-domain matches.');
92
+ }
93
+ else {
94
+ for (const value of visible) {
95
+ const match = terminalRecord(value);
96
+ const hostnames = Array.isArray(match.hostnames) ? match.hostnames : [];
97
+ const shownHosts = hostnames.slice(0, MAX_CT_TERMINAL_HOSTNAMES).map((value) => safeTerminalValue(value));
98
+ const omitted = hostnames.length - shownHosts.length;
99
+ lines.push(safeTerminalValue(match.domain));
100
+ lines.push(` Certificates ${safeTerminalValue(match.certificateCount, '0')}`);
101
+ lines.push(` Hostnames ${shownHosts.join(', ')}${omitted > 0 ? ` (+${omitted} more)` : ''}`);
102
+ lines.push(` Observed ${safeTerminalValue(match.firstObservedAt)} → ${safeTerminalValue(match.lastObservedAt)}`);
103
+ }
104
+ }
105
+ if (matches.length > visible.length) {
106
+ lines.push('', `Showing ${visible.length} of ${matches.length} structured matches in terminal output; use --json for the complete bounded result.`);
107
+ }
108
+ return `${lines.join('\n')}\n`;
109
+ }
110
+ function formatTerminalDiscover(document, mutationLabels = {}) {
111
+ const candidates = Array.isArray(document.candidates) ? document.candidates : [];
112
+ const visible = candidates.slice(0, MAX_DISCOVER_TERMINAL_CANDIDATES);
113
+ const advanced = terminalRecord(document.advancedConfusable);
114
+ const snapshot = terminalRecord(document.snapshot);
115
+ const lines = [
116
+ `Seed ${safeTerminalValue(document.seed)}`,
117
+ `Preset ${safeTerminalValue(document.preset)}`,
118
+ `Keyboard ${safeTerminalValue(document.keyboardLayout)}`,
119
+ `TLDs ${(Array.isArray(document.tlds) ? document.tlds : []).map((value) => safeTerminalValue(value)).join(', ')}`,
120
+ `Families ${(Array.isArray(document.mutationFamilies) && document.mutationFamilies.length) ? document.mutationFamilies.map((value) => safeTerminalValue(value)).join(', ') : 'Preset defaults'}`,
121
+ `Dictionary ${safeTerminalValue(document.dictionaryTermCount, '0')} accepted custom terms${Number(document.rejectedDictionaryTermCount) > 0 ? `, ${safeTerminalValue(document.rejectedDictionaryTermCount)} rejected` : ''}`,
122
+ `Candidates ${safeTerminalValue(candidates.length, '0')}`,
123
+ `Truncated ${document.truncated ? 'Yes' : 'No'}`,
124
+ ];
125
+ if (Object.keys(advanced).length) {
126
+ lines.push(`Advanced IDN ${safeTerminalValue(advanced.generated, '0')} generated, ${safeTerminalValue(advanced.omittedByPolicy, '0')} policy-omitted, ${safeTerminalValue(advanced.omittedByBudget, '0')} budget-omitted`);
127
+ }
128
+ if (Object.keys(snapshot).length) {
129
+ lines.push(`Snapshot ${snapshot.baselineCreated ? 'Baseline created' : `${safeTerminalValue(Array.isArray(snapshot.added) ? snapshot.added.length : 0, '0')} added · ${safeTerminalValue(Array.isArray(snapshot.removed) ? snapshot.removed.length : 0, '0')} removed`}`);
130
+ }
131
+ lines.push('');
132
+ for (const value of visible) {
133
+ const candidate = terminalRecord(value);
134
+ const labels = (Array.isArray(candidate.mutationTypes) ? candidate.mutationTypes : [])
135
+ .map((mutationType) => {
136
+ const value = safeTerminalValue(mutationType);
137
+ return safeTerminalValue(mutationLabels[value] || value);
138
+ });
139
+ const candidateDomain = safeTerminalValue(candidate.domain, '');
140
+ const unicodeDomain = unicodeDomainFromAscii(candidateDomain);
141
+ const unicodeDetail = unicodeDomain && unicodeDomain !== candidateDomain
142
+ ? ` [Unicode: ${safeTerminalValue(unicodeDomain)}]`
143
+ : '';
144
+ lines.push(`${safeTerminalValue(candidateDomain)}${unicodeDetail}: ${labels.join(', ') || 'Generated variant'}`);
145
+ }
146
+ if (!visible.length)
147
+ lines.push('No candidates were generated.');
148
+ if (candidates.length > visible.length) {
149
+ lines.push('', `Showing ${visible.length} of ${candidates.length} candidates in terminal output; use --json or --jsonl for the complete bounded result.`);
150
+ }
151
+ return `${lines.join('\n')}\n`;
152
+ }
153
+ function formatTerminalPosture(document) {
154
+ const summary = terminalRecord(document.summary);
155
+ const spfExpansion = terminalRecord(document.spfExpansion);
156
+ const selectors = Array.isArray(document.dkimSelectors) ? document.dkimSelectors : [];
157
+ const checks = Array.isArray(document.checks) ? document.checks : [];
158
+ const dependencies = Array.isArray(document.externalDependencies) ? document.externalDependencies : [];
159
+ const dmarcAuthorizations = Array.isArray(document.dmarcAuthorizations) ? document.dmarcAuthorizations : [];
160
+ const lines = [
161
+ `Domain ${safeTerminalValue(document.domain)}`,
162
+ `Checked ${safeTerminalValue(document.checkedAt)}`,
163
+ `DKIM selectors ${selectors.length ? selectors.map((value) => safeTerminalValue(value)).join(', ') : 'None supplied'}`,
164
+ `Summary ${safeTerminalValue(summary.danger, '0')} action · ${safeTerminalValue(summary.warning, '0')} review · ${safeTerminalValue(summary.pass, '0')} pass · ${safeTerminalValue(summary.info, '0')} info`,
165
+ ...(Object.keys(spfExpansion).length ? [
166
+ `SPF expansion ${safeTerminalValue(spfExpansion.state)} · ${safeTerminalValue(spfExpansion.lookupsUsed, '0')}/${safeTerminalValue(spfExpansion.lookupLimit, '0')} policy queries · ${safeTerminalValue(spfExpansion.dnsLookupTerms, '0')} DNS terms`,
167
+ ] : []),
168
+ `Dependencies ${dependencies.length} observed · ${dmarcAuthorizations.length} DMARC reporting destination${dmarcAuthorizations.length === 1 ? '' : 's'} checked`,
169
+ '',
170
+ ];
171
+ for (const value of checks) {
172
+ const item = terminalRecord(value);
173
+ lines.push(`[${safeTerminalValue(item.status, 'info').toUpperCase()}] ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.summary)}`);
174
+ if (item.detail)
175
+ lines.push(` Detail ${safeTerminalValue(item.detail)}`);
176
+ if (item.remediation)
177
+ lines.push(` Next ${safeTerminalValue(item.remediation)}`);
178
+ const records = Array.isArray(item.records) ? item.records : [];
179
+ for (const record of records.slice(0, MAX_POSTURE_TERMINAL_RECORDS)) {
180
+ lines.push(` Record ${safeTerminalValue(record)}`);
181
+ }
182
+ if (records.length > MAX_POSTURE_TERMINAL_RECORDS) {
183
+ lines.push(` Records ${records.length - MAX_POSTURE_TERMINAL_RECORDS} more omitted from terminal output; use --json for the complete bounded report.`);
184
+ }
185
+ }
186
+ if (!checks.length)
187
+ lines.push('No posture checks were returned.');
188
+ return `${lines.join('\n')}\n`;
189
+ }
190
+ function formatTerminalHttp(document) {
191
+ const http = terminalRecord(document.http);
192
+ const response = terminalRecord(http.response);
193
+ const attempts = Array.isArray(http.attempts) ? http.attempts : [];
194
+ const limitations = Array.isArray(http.limitations) ? http.limitations : [];
195
+ const securityHeadersRecord = terminalRecord(response.securityHeaders);
196
+ const securityHeaders = Object.keys(securityHeadersRecord).length
197
+ ? Object.entries(securityHeadersRecord).filter(([, value]) => Boolean(value)).map(([name]) => name)
198
+ : [];
199
+ const lines = [
200
+ `Domain ${safeTerminalValue(document.domain)}`,
201
+ `Probe ${titleCase(document.probeStatus)}`,
202
+ `Assessment ${titleCase(document.assessment ?? document.activityStatus)}`,
203
+ `Evidence ${titleCase(http.status)}`,
204
+ `Final URL ${safeTerminalValue(http.finalUrl)}`,
205
+ `HTTP status ${safeTerminalValue(response.status)}`,
206
+ `Transport ${safeTerminalValue(http.transportSecurity)}`,
207
+ `Redirects ${safeTerminalValue(http.redirectCount, '0')}`,
208
+ `Content type ${safeTerminalValue(response.contentType)}`,
209
+ `Body inspected ${response.bodyInspected === true ? 'Yes' : response.bodyInspected === false ? 'No' : '—'}`,
210
+ `Security ${securityHeaders.length ? securityHeaders.join(', ') : 'No selected headers observed'}`,
211
+ ];
212
+ if (document.detail)
213
+ lines.push(`Detail ${safeTerminalValue(document.detail)}`);
214
+ appendDeliveryMetadataLines(lines, response.deliveryMetadata, true);
215
+ const bodyHash = terminalRecord(response.bodyHash);
216
+ if (bodyHash.value) {
217
+ lines.push(`Body hash ${safeTerminalValue(`${bodyHash.algorithm}:${bodyHash.value} (${bodyHash.scope})`)}`);
218
+ }
219
+ for (const value of attempts) {
220
+ const attempt = terminalRecord(value);
221
+ const outcome = attempt.httpStatus ? `HTTP ${attempt.httpStatus}` : attempt.error || attempt.outcome;
222
+ lines.push(`Attempt ${safeTerminalValue(attempt.url)}: ${safeTerminalValue(outcome)}`);
223
+ }
224
+ for (const limitation of limitations)
225
+ lines.push(`Limitation ${safeTerminalValue(limitation)}`);
226
+ return `${lines.join('\n')}\n`;
227
+ }
228
+ function formatTerminalTls(document) {
229
+ const certificate = terminalRecord(document.certificate);
230
+ const subject = terminalRecord(certificate.subject);
231
+ const issuer = terminalRecord(certificate.issuer);
232
+ const altNames = terminalRecord(certificate.subjectAltNames);
233
+ const dnsNames = Array.isArray(altNames.dnsNames) ? altNames.dnsNames : [];
234
+ const ipAddresses = Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses : [];
235
+ const visibleAltNames = [...dnsNames, ...ipAddresses].slice(0, MAX_TLS_TERMINAL_ALT_NAMES);
236
+ const omittedAltNames = dnsNames.length + ipAddresses.length - visibleAltNames.length;
237
+ const cipher = terminalRecord(document.cipher);
238
+ const publicKey = terminalRecord(certificate.publicKey);
239
+ const signature = terminalRecord(certificate.signature);
240
+ const purposes = terminalRecord(certificate.extendedKeyUsage);
241
+ const purposeValues = Array.isArray(purposes.values)
242
+ ? purposes.values.slice(0, MAX_TLS_TERMINAL_PURPOSES).map(terminalRecord)
243
+ : [];
244
+ const sanClasses = terminalCountSummary(altNames.classes, [
245
+ ['dns', 'DNS'],
246
+ ['ip', 'IP'],
247
+ ['email', 'email'],
248
+ ['uri', 'URI'],
249
+ ['directoryName', 'directory name'],
250
+ ['registeredId', 'registered ID'],
251
+ ['otherName', 'other name'],
252
+ ['unclassified', 'other'],
253
+ ]);
254
+ const aia = terminalRecord(certificate.authorityInformationAccess);
255
+ const ocsp = terminalRecord(aia.ocsp);
256
+ const caIssuers = terminalRecord(aia.caIssuers);
257
+ const authorization = terminalRecord(document.authorization);
258
+ const hostname = terminalRecord(document.hostname);
259
+ const validity = terminalRecord(document.validity);
260
+ const diagnostics = terminalRecord(document.diagnostics);
261
+ const findings = Array.isArray(document.findings) ? document.findings : [];
262
+ const limitations = Array.isArray(document.limitations) ? document.limitations : [];
263
+ const lines = [
264
+ `Hostname ${safeTerminalValue(document.sniHost)}`,
265
+ `Evidence ${titleCase(document.status)}`,
266
+ `Observed ${safeTerminalValue(document.observedAt)}`,
267
+ `Address ${safeTerminalValue(document.connectedAddress)}`,
268
+ `Protocol ${safeTerminalValue(document.protocol)}`,
269
+ `ALPN ${safeTerminalValue(document.alpnProtocol)}`,
270
+ `Cipher ${safeTerminalValue(cipher.standardName || cipher.name)}`,
271
+ `Authorized ${authorization.authorized === true ? 'Yes' : authorization.authorized === false ? 'No' : 'Unknown'}`,
272
+ `Hostname match ${hostname.matches === true ? 'Yes' : hostname.matches === false ? 'No' : 'Unknown'}`,
273
+ `Validity ${titleCase(validity.status)}`,
274
+ `Subject ${safeTerminalValue(Array.isArray(subject.commonNames) ? subject.commonNames.join(', ') : null)}`,
275
+ `Issuer ${safeTerminalValue(Array.isArray(issuer.commonNames) ? issuer.commonNames.join(', ') : null)}`,
276
+ `Valid from ${safeTerminalValue(certificate.validFrom)}`,
277
+ `Valid to ${safeTerminalValue(certificate.validTo)}`,
278
+ `Fingerprint ${safeTerminalValue(certificate.fingerprintSha256)}`,
279
+ `Public key ${safeTerminalValue([publicKey.type, publicKey.bits ? `${publicKey.bits} bits` : null, publicKey.curve].filter(Boolean).join(' '))}`,
280
+ ...(signature.algorithm || signature.oid
281
+ ? [`Signature ${safeTerminalValue([signature.algorithm, signature.oid ? `(${signature.oid})` : null].filter(Boolean).join(' '))}`]
282
+ : []),
283
+ ...(Object.keys(purposes).length
284
+ ? [`Purposes ${purposeValues.length
285
+ ? purposeValues.map((purpose) => safeTerminalValue(`${purpose.name || 'Unrecognized purpose'} (${purpose.oid || 'unknown OID'})`)).join(', ')
286
+ : 'None declared'}${Array.isArray(purposes.values) && purposes.values.length > purposeValues.length ? ` (+${purposes.values.length - purposeValues.length} more)` : ''}${purposes.truncated ? ' (source truncated)' : ''}`]
287
+ : []),
288
+ `Alt names ${visibleAltNames.length ? visibleAltNames.map((value) => safeTerminalValue(value)).join(', ') : '—'}${omittedAltNames > 0 ? ` (+${omittedAltNames} more)` : ''}`,
289
+ ...(Object.keys(terminalRecord(altNames.classes)).length
290
+ ? [`SAN classes ${sanClasses || 'None observed'}${altNames.truncated ? ' (truncated)' : ''}`]
291
+ : []),
292
+ ...(Object.keys(aia).length
293
+ ? [`AIA presence ${[
294
+ terminalCount(ocsp.total) > 0
295
+ ? `OCSP ${terminalCount(ocsp.total)} (${terminalCount(ocsp.https)} HTTPS, ${terminalCount(ocsp.http)} HTTP, ${terminalCount(ocsp.other)} other)`
296
+ : null,
297
+ terminalCount(caIssuers.total) > 0
298
+ ? `CA issuers ${terminalCount(caIssuers.total)} (${terminalCount(caIssuers.https)} HTTPS, ${terminalCount(caIssuers.http)} HTTP, ${terminalCount(caIssuers.other)} other)`
299
+ : null,
300
+ terminalCount(aia.unknownMethods) > 0 ? `unknown methods ${terminalCount(aia.unknownMethods)}` : null,
301
+ ].filter(Boolean).join(' · ') || 'None declared'}${aia.truncated ? ' (truncated)' : ''}`]
302
+ : []),
303
+ `Chain ${safeTerminalValue(Array.isArray(document.chain) ? document.chain.length : 0, '0')} certificate${Array.isArray(document.chain) && document.chain.length === 1 ? '' : 's'}${document.chainTruncated ? ' (truncated)' : ''}`,
304
+ ];
305
+ if (authorization.error)
306
+ lines.push(`Trust detail ${safeTerminalValue(authorization.error)}`);
307
+ if (hostname.error)
308
+ lines.push(`Name detail ${safeTerminalValue(hostname.error)}`);
309
+ if (diagnostics.error)
310
+ lines.push(`Error ${safeTerminalValue(diagnostics.error)}`);
311
+ for (const value of findings) {
312
+ const finding = terminalRecord(value);
313
+ lines.push(`Finding ${safeTerminalValue(finding.label)}: ${safeTerminalValue(finding.detail)}`);
314
+ }
315
+ for (const limitation of limitations)
316
+ lines.push(`Limitation ${safeTerminalValue(limitation)}`);
317
+ return `${lines.join('\n')}\n`;
318
+ }
319
+ function comparisonStatusLabel(status) {
320
+ const labels = {
321
+ equivalent: 'Equivalent',
322
+ conflict: 'Conflict',
323
+ rdap_only: 'RDAP only',
324
+ whois_only: 'WHOIS only',
325
+ rdap_redacted: 'RDAP redacted',
326
+ whois_redacted: 'WHOIS redacted',
327
+ rdap_unavailable: 'RDAP unavailable',
328
+ whois_unavailable: 'WHOIS unavailable',
329
+ rdap_incomplete: 'RDAP incomplete',
330
+ whois_incomplete: 'WHOIS incomplete',
331
+ registry_only: 'Registry only',
332
+ registrar_only: 'Registrar only',
333
+ registry_redacted: 'Registry redacted',
334
+ registrar_redacted: 'Registrar redacted',
335
+ registry_unavailable: 'Registry unavailable',
336
+ registrar_unavailable: 'Registrar unavailable',
337
+ registry_incomplete: 'Registry incomplete',
338
+ registrar_incomplete: 'Registrar incomplete',
339
+ };
340
+ return labels[String(status)] || titleCase(status);
341
+ }
342
+ function formatTerminalCompare(document) {
343
+ const fields = Array.isArray(document.fields) ? document.fields : [];
344
+ const counts = terminalRecord(document.counts);
345
+ const sourceHealth = terminalRecord(document.sourceHealth);
346
+ const rdapHealth = terminalRecord(sourceHealth.rdap);
347
+ const whoisHealth = terminalRecord(sourceHealth.whois);
348
+ const registryAccess = terminalRecord(document.registryAccess);
349
+ const differenceCount = fields.length - (Number(counts.equivalent) || 0);
350
+ const lines = [
351
+ `Query ${safeTerminalValue(document.query || document.registrableDomain)}`,
352
+ `Lookup mode ${titleCase(document.lookupMode)}`,
353
+ `Lookup saved ${safeTerminalValue(document.lookupGeneratedAt)}`,
354
+ `RDAP source ${comparisonStatusLabel(rdapHealth.status)}`,
355
+ `WHOIS source ${comparisonStatusLabel(whoisHealth.status)}`,
356
+ `Compared ${safeTerminalValue(fields.length, '0')} field${fields.length === 1 ? '' : 's'}`,
357
+ `Equivalent ${safeTerminalValue(counts.equivalent, '0')}`,
358
+ `Differences ${safeTerminalValue(differenceCount, '0')}`,
359
+ ];
360
+ if (Object.keys(registryAccess).length) {
361
+ lines.push(`Registry access .${safeTerminalValue(registryAccess.suffix)}`, `WHOIS access ${registryAccessProfileLabel(registryAccess.whoisAccessProfile)}`, `RDAP access ${registryAccessProfileLabel(registryAccess.rdapAccessProfile)}`, `Access note ${safeTerminalValue(registryAccess.limitation)}`);
362
+ }
363
+ lines.push('');
364
+ if (!fields.length) {
365
+ lines.push('Neither source published a comparable normalised field.');
366
+ }
367
+ else {
368
+ for (const value of fields) {
369
+ const field = terminalRecord(value);
370
+ lines.push(`[${comparisonStatusLabel(field.status).toUpperCase()}] ${safeTerminalValue(field.label)}`);
371
+ lines.push(` RDAP ${safeTerminalValue(field.rdapDisplay)}`);
372
+ lines.push(` WHOIS ${safeTerminalValue(field.whoisDisplay)}`);
373
+ }
374
+ }
375
+ const registrarComparison = terminalRecord(document.registrarPublicationComparison);
376
+ if (Object.keys(registrarComparison).length) {
377
+ const publicationFields = Array.isArray(registrarComparison.fields) ? registrarComparison.fields : [];
378
+ const publicationCounts = terminalRecord(registrarComparison.counts);
379
+ const publicationHealth = terminalRecord(registrarComparison.sourceHealth);
380
+ const registryHealth = terminalRecord(publicationHealth.registry);
381
+ const registrarHealth = terminalRecord(publicationHealth.registrar);
382
+ const publicationDifferences = publicationFields.length - (Number(publicationCounts.equivalent) || 0);
383
+ lines.push('', 'Registry / registrar RDAP publication', `Registry RDAP ${comparisonStatusLabel(registryHealth.status)}`, `Registrar RDAP ${comparisonStatusLabel(registrarHealth.status)}`, `Compared ${safeTerminalValue(publicationFields.length, '0')} field${publicationFields.length === 1 ? '' : 's'}`, `Equivalent ${safeTerminalValue(publicationCounts.equivalent, '0')}`, `Differences ${safeTerminalValue(publicationDifferences, '0')}`, '');
384
+ if (!publicationFields.length) {
385
+ lines.push('Neither RDAP publication exposed a comparable normalised field.');
386
+ }
387
+ else {
388
+ for (const value of publicationFields) {
389
+ const field = terminalRecord(value);
390
+ lines.push(`[${comparisonStatusLabel(field.status).toUpperCase()}] ${safeTerminalValue(field.label)}`);
391
+ lines.push(` Registry ${safeTerminalValue(field.registryDisplay)}`);
392
+ lines.push(` Registrar ${safeTerminalValue(field.registrarDisplay)}`);
393
+ }
394
+ }
395
+ }
396
+ lines.push('', 'Comparison is source reconciliation, not an availability or ownership decision.');
397
+ if (Object.keys(registryAccess).length) {
398
+ lines.push('Registry access describes collection reachability only; it does not decide registration, availability, ownership, safety, or maliciousness.');
399
+ }
400
+ return `${lines.join('\n')}\n`;
401
+ }
402
+ function formatTerminalRiskCalibration(document) {
403
+ const summary = terminalRecord(document.summary);
404
+ const bands = terminalRecord(summary.scoreBands);
405
+ const thresholds = Array.isArray(document.thresholds) ? document.thresholds : [];
406
+ const records = Array.isArray(document.records) ? document.records : [];
407
+ const comparison = terminalRecord(document.modelComparison);
408
+ const visible = records.slice(0, MAX_RISK_CALIBRATION_TERMINAL_RECORDS);
409
+ const lines = [
410
+ `Risk model v${safeTerminalValue(document.riskModelVersion)}`,
411
+ `Review band ${safeTerminalValue(document.currentReviewThreshold)}+`,
412
+ `Records ${safeTerminalValue(summary.total, '0')}`,
413
+ `Metric labels ${safeTerminalValue((Number(summary.positive) || 0) + (Number(summary.negative) || 0), '0')}`,
414
+ `Excluded ${safeTerminalValue(summary.excluded, '0')}`,
415
+ `Score bands 0-39 ${safeTerminalValue(bands['0_39'], '0')} · 40-69 ${safeTerminalValue(bands['40_69'], '0')} · 70-100 ${safeTerminalValue(bands['70_100'], '0')} · not scored ${safeTerminalValue(bands.not_scored, '0')}`,
416
+ '',
417
+ 'Threshold replay',
418
+ ];
419
+ for (const value of thresholds) {
420
+ const threshold = terminalRecord(value);
421
+ lines.push(`${String(safeTerminalValue(threshold.threshold)).padStart(3)}+ TP ${safeTerminalValue(threshold.truePositive, '0')} FP ${safeTerminalValue(threshold.falsePositive, '0')} TN ${safeTerminalValue(threshold.trueNegative, '0')} FN ${safeTerminalValue(threshold.falseNegative, '0')} precision ${safeTerminalValue(threshold.precision)} recall ${safeTerminalValue(threshold.recall)} F1 ${safeTerminalValue(threshold.f1)}`);
422
+ }
423
+ if (comparison.available === true) {
424
+ lines.push('', `Model comparison v${safeTerminalValue(comparison.previousModelVersion)} → v${safeTerminalValue(comparison.currentModelVersion)} · ${safeTerminalValue(comparison.scoresChanged, '0')} score changes · ${safeTerminalValue(comparison.thresholdClassificationsChanged, '0')} review-band changes`);
425
+ }
426
+ lines.push('', 'Records');
427
+ for (const value of visible) {
428
+ const record = terminalRecord(value);
429
+ const score = record.score === null ? 'not scored' : String(record.score);
430
+ lines.push(`${safeTerminalValue(record.id)} ${safeTerminalValue(record.domain)} ${titleCase(record.analystDisposition)} ${score}`);
431
+ }
432
+ if (records.length > visible.length)
433
+ lines.push(`… ${records.length - visible.length} additional records omitted from terminal output; use --json for the complete bounded report.`);
434
+ lines.push('', safeTerminalValue(terminalRecord(document.interpretation).statement));
435
+ return `${lines.join('\n')}\n`;
436
+ }
437
+ export { MAX_CT_TERMINAL_HOSTNAMES, MAX_CT_TERMINAL_MATCHES, MAX_DISCOVER_TERMINAL_CANDIDATES, MAX_POSTURE_TERMINAL_RECORDS, MAX_RISK_CALIBRATION_TERMINAL_RECORDS, formatTerminalBulk, formatTerminalCompare, formatTerminalCtSearch, formatTerminalDiscover, formatTerminalHttp, formatTerminalPosture, formatTerminalRegistrySupport, formatTerminalRiskCalibration, formatTerminalTls, };