@slicedearth/whoisleuth-cli 2.0.1 → 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.
- package/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +16 -9
- package/docs/cli.md +4 -2
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +46 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +2 -2
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +225 -61
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
package/cli/lookup-brief.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
2
2
|
import { safeTerminalValue } from "./formatters/terminal.mjs";
|
|
3
|
+
import { compareRdapPublications, compareRegistrySources } from "../lib/registry-comparison.mjs";
|
|
3
4
|
export const CLI_LOOKUP_BRIEF_SCHEMA = 'whoisleuth.cli.lookup-brief';
|
|
4
|
-
export const CLI_LOOKUP_BRIEF_VERSION =
|
|
5
|
+
export const CLI_LOOKUP_BRIEF_VERSION = 3;
|
|
5
6
|
function record(value) {
|
|
6
7
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
7
8
|
}
|
|
@@ -18,41 +19,145 @@ function text(value, maximum = 300) {
|
|
|
18
19
|
function sourceState(value) {
|
|
19
20
|
return text(value, 40)?.toLowerCase() ?? 'unavailable';
|
|
20
21
|
}
|
|
22
|
+
function joinedText(value, maximumItems = 8) {
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
return null;
|
|
25
|
+
const retained = value.slice(0, maximumItems).flatMap((item) => {
|
|
26
|
+
const normalized = text(item, 253);
|
|
27
|
+
return normalized ? [normalized] : [];
|
|
28
|
+
});
|
|
29
|
+
if (!retained.length)
|
|
30
|
+
return null;
|
|
31
|
+
return `${retained.join(', ')}${value.length > retained.length ? ` (+${value.length - retained.length} more)` : ''}`;
|
|
32
|
+
}
|
|
33
|
+
function observedBoolean(value, positive, negative) {
|
|
34
|
+
return value === true ? positive : value === false ? negative : null;
|
|
35
|
+
}
|
|
36
|
+
function sourceObservation(id, label, stateValue, ...timeValues) {
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
id,
|
|
39
|
+
label,
|
|
40
|
+
state: sourceState(stateValue),
|
|
41
|
+
observedAt: timeValues.map((value) => text(value, 64)).find(Boolean) ?? null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function contradictionRows(value, left, right, leftStateKey, rightStateKey, leftDisplayKey, rightDisplayKey) {
|
|
45
|
+
const fields = Array.isArray(record(value).fields) ? record(value).fields : [];
|
|
46
|
+
return fields.flatMap((fieldValue) => {
|
|
47
|
+
const field = record(fieldValue);
|
|
48
|
+
if (field.status !== 'conflict')
|
|
49
|
+
return [];
|
|
50
|
+
const label = text(field.label, 120) ?? 'Registration publication field';
|
|
51
|
+
const observations = [
|
|
52
|
+
Object.freeze({
|
|
53
|
+
source: left,
|
|
54
|
+
state: sourceState(field[leftStateKey] ?? left.state),
|
|
55
|
+
value: text(field[leftDisplayKey], 300) ?? 'Value not retained',
|
|
56
|
+
}),
|
|
57
|
+
Object.freeze({
|
|
58
|
+
source: right,
|
|
59
|
+
state: sourceState(field[rightStateKey] ?? right.state),
|
|
60
|
+
value: text(field[rightDisplayKey], 300) ?? 'Value not retained',
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
return [Object.freeze({
|
|
64
|
+
field: label,
|
|
65
|
+
detail: `${left.label} and ${right.label} publish different values for ${label}.`,
|
|
66
|
+
observations: Object.freeze(observations),
|
|
67
|
+
})];
|
|
68
|
+
});
|
|
69
|
+
}
|
|
21
70
|
export function buildCliLookupBrief(input, generatedAt = new Date().toISOString()) {
|
|
22
71
|
const document = parseSavedLookupDocument(input, { label: 'Lookup brief input' });
|
|
23
72
|
const diagnostics = record(document.diagnostics);
|
|
24
73
|
const availability = record(document.availability);
|
|
74
|
+
const rdapDiagnostics = record(diagnostics.rdap);
|
|
75
|
+
const registrarRdapDiagnostics = record(rdapDiagnostics.registrar);
|
|
76
|
+
const whoisDiagnostics = record(diagnostics.whois);
|
|
77
|
+
const availabilityDiagnostics = record(diagnostics.availability);
|
|
25
78
|
const rdap = record(record(document.rdap).parsed);
|
|
26
79
|
const whois = record(record(document.whois).parsed);
|
|
80
|
+
const registrarRdap = record(record(record(document.rdap).registrarRdap).parsed);
|
|
27
81
|
const preferred = Object.keys(rdap).length ? rdap : whois;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
82
|
+
const rdapSource = sourceObservation('rdap', 'Registry RDAP', rdapDiagnostics.status, rdapDiagnostics.observedAt, rdapDiagnostics.fetchedAt, record(document.rdap).observedAt, record(document.rdap).fetchedAt);
|
|
83
|
+
const registrarRdapSource = sourceObservation('registrar_rdap', 'Registrar RDAP', registrarRdapDiagnostics.status, registrarRdapDiagnostics.observedAt, registrarRdapDiagnostics.fetchedAt);
|
|
84
|
+
const whoisSource = sourceObservation('whois', 'WHOIS', whoisDiagnostics.status, whoisDiagnostics.observedAt, whoisDiagnostics.queriedAt, record(document.whois).observedAt, record(document.whois).queriedAt);
|
|
85
|
+
const availabilitySource = sourceObservation('availability', 'Authority-aware availability', availabilityDiagnostics.status ?? availability.status, availabilityDiagnostics.observedAt, availability.observedAt);
|
|
86
|
+
const dns = record(availability.dns);
|
|
87
|
+
const http = record(availability.http);
|
|
88
|
+
const tls = record(availability.tls);
|
|
89
|
+
const dnsSource = sourceObservation('dns', 'DNS', dns.status ?? dns.state, dns.observedAt, dns.checkedAt);
|
|
90
|
+
const httpSource = sourceObservation('http', 'HTTP and static page identity', http.status ?? http.state, http.observedAt, http.checkedAt, record(availability.pageIdentity).observedAt);
|
|
91
|
+
const tlsSource = sourceObservation('tls', 'TLS', tls.status ?? tls.state, tls.observedAt, tls.checkedAt);
|
|
92
|
+
const pageIdentity = record(availability.pageIdentity);
|
|
93
|
+
const technology = record(availability.technologyProfile);
|
|
94
|
+
const sslbl = record(document.sslbl);
|
|
95
|
+
const pageAnalysisSource = sourceObservation('page_analysis', 'Static page analysis', pageIdentity.status ?? technology.status, pageIdentity.observedAt, technology.observedAt, http.observedAt);
|
|
96
|
+
const sslblSource = sourceObservation('sslbl', 'Local SSLBL certificate snapshot', sslbl.status, sslbl.observedAt, record(sslbl.snapshot).sourceUpdatedAt);
|
|
97
|
+
const sources = [
|
|
98
|
+
rdapSource,
|
|
99
|
+
registrarRdapSource,
|
|
100
|
+
whoisSource,
|
|
101
|
+
availabilitySource,
|
|
102
|
+
dnsSource,
|
|
103
|
+
httpSource,
|
|
104
|
+
tlsSource,
|
|
105
|
+
pageAnalysisSource,
|
|
106
|
+
...(Object.keys(sslbl).length ? [sslblSource] : []),
|
|
107
|
+
];
|
|
108
|
+
const sourceById = new Map(sources.map((source) => [source.id, source]));
|
|
109
|
+
const preferredRegistrationSource = Object.keys(rdap).length ? rdapSource : whoisSource;
|
|
110
|
+
const externalFormActions = record(pageIdentity.forms).externalActionOrigins;
|
|
32
111
|
const facts = [
|
|
33
|
-
['Registration state', availability.state,
|
|
34
|
-
['Registrar', record(preferred.registrar).name ?? preferred.registrar,
|
|
35
|
-
['Created', record(preferred.lifecycle).createdIso ?? preferred.createdDateIso,
|
|
36
|
-
['Expires', record(preferred.lifecycle).expiryIso ?? preferred.expiryDateIso,
|
|
37
|
-
['
|
|
38
|
-
['
|
|
39
|
-
|
|
112
|
+
['registration-state', 'Registration state', availability.state, availabilitySource.id],
|
|
113
|
+
['registrar', 'Registrar', record(preferred.registrar).name ?? preferred.registrar, preferredRegistrationSource.id],
|
|
114
|
+
['created', 'Created', record(preferred.lifecycle).createdIso ?? preferred.createdDateIso, preferredRegistrationSource.id],
|
|
115
|
+
['expires', 'Expires', record(preferred.lifecycle).expiryIso ?? preferred.expiryDateIso, preferredRegistrationSource.id],
|
|
116
|
+
['name-servers', 'Name servers', joinedText(preferred.nameservers), preferredRegistrationSource.id],
|
|
117
|
+
['website-activity', 'Website activity', availability.activityStatus, httpSource.id],
|
|
118
|
+
['page-title', 'Page title', availability.pageTitle, httpSource.id],
|
|
119
|
+
['dnssec', 'DNSSEC publication', availability.dnssec, dnsSource.id],
|
|
120
|
+
['mail-exchanger', 'Mail exchanger', observedBoolean(availability.hasMx, 'Observed', 'Not observed in retained DNS evidence'), dnsSource.id],
|
|
121
|
+
['spf', 'SPF publication', observedBoolean(availability.hasSpf, 'Observed', 'Not observed in retained DNS evidence'), dnsSource.id],
|
|
122
|
+
['dmarc', 'DMARC publication', observedBoolean(availability.hasDmarc, 'Observed', 'Not observed in retained DNS evidence'), dnsSource.id],
|
|
123
|
+
['password-input', 'Password input', observedBoolean(availability.hasPasswordField, 'Observed in static HTML', 'Not observed in retained static HTML'), pageAnalysisSource.id],
|
|
124
|
+
['external-form-actions', 'External form actions', Array.isArray(externalFormActions)
|
|
125
|
+
? `${externalFormActions.length} retained origin${externalFormActions.length === 1 ? '' : 's'}`
|
|
126
|
+
: null, pageAnalysisSource.id],
|
|
127
|
+
['page-role', 'Page role', record(availability.pageRoleProfile).primaryRole, pageAnalysisSource.id],
|
|
128
|
+
['technology-indicators', 'Technology indicators', joinedText(Array.isArray(technology.findings)
|
|
129
|
+
? technology.findings.map((finding) => record(finding).name ?? record(finding).id)
|
|
130
|
+
: null), pageAnalysisSource.id],
|
|
131
|
+
['certificate-warning-data', 'Certificate warning data', sslbl.verdict === 'listed'
|
|
132
|
+
? 'Listed certificate review lead'
|
|
133
|
+
: sslbl.verdict === 'not_listed' && sslbl.status === 'success' && sslbl.complete === true
|
|
134
|
+
? 'No match in retained local snapshot'
|
|
135
|
+
: Object.keys(sslbl).length
|
|
136
|
+
? 'Inconclusive'
|
|
137
|
+
: null, sslblSource.id],
|
|
138
|
+
].flatMap(([id, label, value, sourceId]) => {
|
|
40
139
|
const normalized = text(value);
|
|
41
|
-
|
|
140
|
+
const source = sourceById.get(String(sourceId));
|
|
141
|
+
return normalized && source ? [Object.freeze({ id: String(id), label: String(label), value: normalized, source })] : [];
|
|
42
142
|
});
|
|
43
143
|
const incomplete = sources.filter((item) => !['complete', 'success'].includes(item.state));
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
144
|
+
const registryComparison = compareRegistrySources(rdap, whois, {
|
|
145
|
+
rdapStatus: rdapDiagnostics.status,
|
|
146
|
+
whoisStatus: whoisDiagnostics.status,
|
|
147
|
+
});
|
|
148
|
+
const registrarComparison = compareRdapPublications(rdap, registrarRdap, {
|
|
149
|
+
registryStatus: rdapDiagnostics.status,
|
|
150
|
+
registrarStatus: registrarRdapDiagnostics.status,
|
|
151
|
+
});
|
|
152
|
+
const contradictions = [
|
|
153
|
+
...contradictionRows(registryComparison, rdapSource, whoisSource, 'rdapState', 'whoisState', 'rdapDisplay', 'whoisDisplay'),
|
|
154
|
+
...contradictionRows(registrarComparison, rdapSource, registrarRdapSource, 'registryState', 'registrarState', 'registryDisplay', 'registrarDisplay'),
|
|
155
|
+
].slice(0, 16);
|
|
51
156
|
const actionPlan = [
|
|
52
157
|
...(contradictions.length ? [{
|
|
53
158
|
id: 'registration-publication-review',
|
|
54
159
|
action: 'Review the separately attributed registration publications before relying on the conflicting fields.',
|
|
55
|
-
reason: `${contradictions.length} conflicting publication field${contradictions.length === 1 ? '' : 's'} were retained.`,
|
|
160
|
+
reason: `${contradictions.length} conflicting publication field${contradictions.length === 1 ? '' : 's'} were retained with separate source values.`,
|
|
56
161
|
expectedOutcome: 'Establish which observation is current, authoritative, or still unresolved.',
|
|
57
162
|
evidence: 'registry publication comparison',
|
|
58
163
|
}] : []),
|
|
@@ -70,6 +175,13 @@ export function buildCliLookupBrief(input, generatedAt = new Date().toISOString(
|
|
|
70
175
|
expectedOutcome: 'Collect the explicitly selected additional source classes without implying that unavailable evidence is absent.',
|
|
71
176
|
evidence: 'collection mode',
|
|
72
177
|
}] : []),
|
|
178
|
+
...(sslbl.verdict === 'listed' ? [{
|
|
179
|
+
id: 'certificate-warning-review',
|
|
180
|
+
action: 'Review the matching certificate fingerprint alongside current TLS, page, and infrastructure evidence.',
|
|
181
|
+
reason: 'The retained local warning-data snapshot contains the observed certificate fingerprint as a review lead.',
|
|
182
|
+
expectedOutcome: 'Determine whether the listing is relevant and current without treating it alone as a maliciousness verdict.',
|
|
183
|
+
evidence: 'local SSLBL certificate snapshot',
|
|
184
|
+
}] : []),
|
|
73
185
|
{
|
|
74
186
|
id: 'case-evidence-review',
|
|
75
187
|
action: 'Pin only the facts needed for the decision and keep analyst hypotheses separate from observations.',
|
|
@@ -105,8 +217,14 @@ export function formatCliLookupBrief(document) {
|
|
|
105
217
|
`Observed ${safeTerminalValue(document.observedAt)}`,
|
|
106
218
|
`Mode ${safeTerminalValue(document.mode)}`,
|
|
107
219
|
'',
|
|
108
|
-
'
|
|
109
|
-
...(document.facts.length ? document.facts.map((item) => ` ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.value)} (${safeTerminalValue(item.source)})`) : [' No bounded fact was available.']),
|
|
220
|
+
'Source-attributed facts',
|
|
221
|
+
...(document.facts.length ? document.facts.map((item) => ` ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.value)} (${safeTerminalValue(item.source.label)}; ${safeTerminalValue(item.source.state)}; ${item.source.observedAt ? `observed ${safeTerminalValue(item.source.observedAt)}` : 'source time not recorded'})`) : [' No bounded fact was available.']),
|
|
222
|
+
'',
|
|
223
|
+
'Contradictions',
|
|
224
|
+
...(document.contradictions.length ? document.contradictions.flatMap((item) => [
|
|
225
|
+
` ${safeTerminalValue(item.field)}: ${safeTerminalValue(item.detail)}`,
|
|
226
|
+
...item.observations.map((observation) => ` ${safeTerminalValue(observation.source.label)}: ${safeTerminalValue(observation.value)} (${safeTerminalValue(observation.state)}; ${observation.source.observedAt ? `observed ${safeTerminalValue(observation.source.observedAt)}` : 'source time not recorded'})`),
|
|
227
|
+
]) : [' No conflicting publication field was retained.']),
|
|
110
228
|
'',
|
|
111
229
|
'Unknown or incomplete',
|
|
112
230
|
...(document.unknowns.length ? document.unknowns.map((item) => ` ${safeTerminalValue(item)}`) : [' No incomplete source state was identified.']),
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { formatTerminalLookup, safeTerminalValue } from "./formatters/terminal.mjs";
|
|
2
|
+
import { presentTerminalOutput, wrapTerminalOutput, } from "./terminal-presentation.mjs";
|
|
3
|
+
const MIN_LOOKUP_BROWSER_HEIGHT = 12;
|
|
4
|
+
const MAX_LOOKUP_BROWSER_HEIGHT = 80;
|
|
5
|
+
const MAX_LOOKUP_BROWSER_PANELS = 9;
|
|
6
|
+
const MAX_LOOKUP_BROWSER_SEARCH_BYTES = 256;
|
|
7
|
+
const MAX_LOOKUP_BROWSER_SEARCH_SCALARS = 80;
|
|
8
|
+
const MAX_LOOKUP_BROWSER_SEARCH_MATCHES = 256;
|
|
9
|
+
const ANSI_SEQUENCE_RE = /\u001b\[[0-?]*[ -/]*[@-~]/gu;
|
|
10
|
+
function boundedHeight(value) {
|
|
11
|
+
const height = Number(value);
|
|
12
|
+
if (!Number.isSafeInteger(height))
|
|
13
|
+
return 24;
|
|
14
|
+
return Math.max(MIN_LOOKUP_BROWSER_HEIGHT, Math.min(MAX_LOOKUP_BROWSER_HEIGHT, height));
|
|
15
|
+
}
|
|
16
|
+
function panelId(value, index) {
|
|
17
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '');
|
|
18
|
+
return normalized || `panel-${index + 1}`;
|
|
19
|
+
}
|
|
20
|
+
function terminalCharacterWidth(character) {
|
|
21
|
+
const codePoint = character.codePointAt(0) || 0;
|
|
22
|
+
if (/\p{Mark}/u.test(character)
|
|
23
|
+
|| codePoint === 0x200d
|
|
24
|
+
|| codePoint >= 0xfe00 && codePoint <= 0xfe0f
|
|
25
|
+
|| codePoint >= 0xe0100 && codePoint <= 0xe01ef)
|
|
26
|
+
return 0;
|
|
27
|
+
return codePoint >= 0x1100 && (codePoint <= 0x115f
|
|
28
|
+
|| codePoint === 0x2329
|
|
29
|
+
|| codePoint === 0x232a
|
|
30
|
+
|| codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f
|
|
31
|
+
|| codePoint >= 0xac00 && codePoint <= 0xd7a3
|
|
32
|
+
|| codePoint >= 0xf900 && codePoint <= 0xfaff
|
|
33
|
+
|| codePoint >= 0xfe10 && codePoint <= 0xfe19
|
|
34
|
+
|| codePoint >= 0xfe30 && codePoint <= 0xfe6f
|
|
35
|
+
|| codePoint >= 0xff00 && codePoint <= 0xff60
|
|
36
|
+
|| codePoint >= 0xffe0 && codePoint <= 0xffe6
|
|
37
|
+
|| codePoint >= 0x1f300 && codePoint <= 0x1faff
|
|
38
|
+
|| codePoint >= 0x20000 && codePoint <= 0x3fffd) ? 2 : 1;
|
|
39
|
+
}
|
|
40
|
+
function terminalDisplayWidth(value) {
|
|
41
|
+
return Array.from(value.replace(ANSI_SEQUENCE_RE, ''))
|
|
42
|
+
.reduce((total, character) => total + terminalCharacterWidth(character), 0);
|
|
43
|
+
}
|
|
44
|
+
function hardWrapTerminalLine(line, width) {
|
|
45
|
+
if (!line || terminalDisplayWidth(line) <= width)
|
|
46
|
+
return [line];
|
|
47
|
+
const lines = [];
|
|
48
|
+
let current = '';
|
|
49
|
+
let currentWidth = 0;
|
|
50
|
+
for (const character of line) {
|
|
51
|
+
const characterWidth = terminalCharacterWidth(character);
|
|
52
|
+
if (current && currentWidth + characterWidth > width) {
|
|
53
|
+
lines.push(current);
|
|
54
|
+
current = '';
|
|
55
|
+
currentWidth = 0;
|
|
56
|
+
}
|
|
57
|
+
current += character;
|
|
58
|
+
currentWidth += characterWidth;
|
|
59
|
+
}
|
|
60
|
+
if (current || !lines.length)
|
|
61
|
+
lines.push(current);
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
function clipTerminalLine(line, width) {
|
|
65
|
+
if (terminalDisplayWidth(line) <= width)
|
|
66
|
+
return line;
|
|
67
|
+
const maximumContentWidth = Math.max(1, width - 1);
|
|
68
|
+
let clipped = '';
|
|
69
|
+
let clippedWidth = 0;
|
|
70
|
+
for (const character of line) {
|
|
71
|
+
const characterWidth = terminalCharacterWidth(character);
|
|
72
|
+
if (clippedWidth + characterWidth > maximumContentWidth)
|
|
73
|
+
break;
|
|
74
|
+
clipped += character;
|
|
75
|
+
clippedWidth += characterWidth;
|
|
76
|
+
}
|
|
77
|
+
return `${clipped}…`;
|
|
78
|
+
}
|
|
79
|
+
function wrapLookupBrowserLines(value, width) {
|
|
80
|
+
const softlyWrapped = wrapTerminalOutput(`${value}\n`, width).replace(/\n$/u, '').split('\n');
|
|
81
|
+
return softlyWrapped.flatMap((line) => hardWrapTerminalLine(line, width));
|
|
82
|
+
}
|
|
83
|
+
function buildLookupBrowserPanels(document) {
|
|
84
|
+
const blocks = formatTerminalLookup(document, { detail: 'verbose' })
|
|
85
|
+
.trimEnd()
|
|
86
|
+
.split(/\n\n/gu)
|
|
87
|
+
.slice(0, MAX_LOOKUP_BROWSER_PANELS);
|
|
88
|
+
return blocks.flatMap((block, index) => {
|
|
89
|
+
const [heading, ...lines] = block.split('\n');
|
|
90
|
+
if (!heading?.endsWith(':') || lines.length === 0)
|
|
91
|
+
return [];
|
|
92
|
+
const label = heading.slice(0, -1);
|
|
93
|
+
return [Object.freeze({
|
|
94
|
+
id: panelId(label, index),
|
|
95
|
+
label,
|
|
96
|
+
lines: Object.freeze(lines),
|
|
97
|
+
})];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function browserTabLine(panels, activeIndex, width) {
|
|
101
|
+
const labels = panels.map((panel, index) => index === activeIndex
|
|
102
|
+
? `[${index + 1} ${panel.label}]`
|
|
103
|
+
: `${index + 1} ${panel.label}`);
|
|
104
|
+
const complete = labels.join(' ');
|
|
105
|
+
if (terminalDisplayWidth(complete) <= width)
|
|
106
|
+
return complete;
|
|
107
|
+
const visible = [activeIndex - 1, activeIndex, activeIndex + 1]
|
|
108
|
+
.filter((index) => index >= 0 && index < panels.length)
|
|
109
|
+
.map((index) => labels[index]);
|
|
110
|
+
const windowed = `${activeIndex > 1 ? '‹ ' : ''}${visible.join(' ')}${activeIndex + 2 < panels.length ? ' ›' : ''}`;
|
|
111
|
+
return clipTerminalLine(windowed, width);
|
|
112
|
+
}
|
|
113
|
+
function boundedTargetLine(query, width) {
|
|
114
|
+
return clipTerminalLine(`Target ${query}`, width);
|
|
115
|
+
}
|
|
116
|
+
function boundedBrowserDimensions(options) {
|
|
117
|
+
return {
|
|
118
|
+
width: Math.max(40, Math.min(160, Number.isSafeInteger(options.width) ? Number(options.width) : 100)),
|
|
119
|
+
height: boundedHeight(options.height),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function boundedSearchText(value) {
|
|
123
|
+
const text = typeof value === 'string' ? value : '';
|
|
124
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_LOOKUP_BROWSER_SEARCH_BYTES
|
|
125
|
+
|| Array.from(text).length > MAX_LOOKUP_BROWSER_SEARCH_SCALARS
|
|
126
|
+
|| /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/u.test(text)) {
|
|
127
|
+
throw new RangeError(`Lookup browser search is limited to ${MAX_LOOKUP_BROWSER_SEARCH_SCALARS} characters and ${MAX_LOOKUP_BROWSER_SEARCH_BYTES} UTF-8 bytes.`);
|
|
128
|
+
}
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
function findLookupBrowserMatches(panels, query) {
|
|
132
|
+
const normalized = boundedSearchText(query).trim().toLocaleLowerCase('en-US');
|
|
133
|
+
if (!normalized)
|
|
134
|
+
return { matches: [], truncated: false };
|
|
135
|
+
const matches = [];
|
|
136
|
+
let truncated = false;
|
|
137
|
+
outer: for (let panelIndex = 0; panelIndex < panels.length; panelIndex += 1) {
|
|
138
|
+
const panel = panels[panelIndex];
|
|
139
|
+
const lines = [panel.label, ...panel.lines];
|
|
140
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
141
|
+
if (!String(lines[lineIndex] || '').toLocaleLowerCase('en-US').includes(normalized))
|
|
142
|
+
continue;
|
|
143
|
+
if (matches.length >= MAX_LOOKUP_BROWSER_SEARCH_MATCHES) {
|
|
144
|
+
truncated = true;
|
|
145
|
+
break outer;
|
|
146
|
+
}
|
|
147
|
+
matches.push(Object.freeze({ panelIndex, lineIndex: Math.max(0, lineIndex - 1) }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { matches, truncated };
|
|
151
|
+
}
|
|
152
|
+
function renderedPanelLineOffset(panel, lineIndex, width) {
|
|
153
|
+
if (lineIndex <= 0)
|
|
154
|
+
return 0;
|
|
155
|
+
return panel.lines
|
|
156
|
+
.slice(0, Math.min(lineIndex, panel.lines.length))
|
|
157
|
+
.reduce((total, line) => total + wrapLookupBrowserLines(line, width).length, 0);
|
|
158
|
+
}
|
|
159
|
+
function renderStandaloneBrowserFrame(lines, options) {
|
|
160
|
+
const { width, height } = boundedBrowserDimensions(options);
|
|
161
|
+
const clipped = lines.slice(0, height).map((line) => clipTerminalLine(line, width));
|
|
162
|
+
return presentTerminalOutput(`${clipped.join('\n')}\n`, {
|
|
163
|
+
color: options.color === true,
|
|
164
|
+
interactive: true,
|
|
165
|
+
palette: options.palette || 'auto',
|
|
166
|
+
width: null,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function buildLookupBrowserHelpFrame(document, options = {}) {
|
|
170
|
+
const query = safeTerminalValue(document?.query ?? options.query, 'unknown target');
|
|
171
|
+
const mode = safeTerminalValue(document?.mode ?? options.mode, 'unknown mode');
|
|
172
|
+
return renderStandaloneBrowserFrame([
|
|
173
|
+
'WHOISleuth evidence browser help',
|
|
174
|
+
boundedTargetLine(query, boundedBrowserDimensions(options).width),
|
|
175
|
+
`Mode ${mode}`,
|
|
176
|
+
'',
|
|
177
|
+
options.collecting ? 'Collection' : 'Panels',
|
|
178
|
+
options.collecting
|
|
179
|
+
? ' q / Ctrl-C cancel the active collection'
|
|
180
|
+
: ' ←/→ h/l Tab move panels · 1-9 select',
|
|
181
|
+
options.collecting
|
|
182
|
+
? ' progress is transient and is never retained'
|
|
183
|
+
: ' ↑/↓ j/k PgUp/PgDn Home/End scroll',
|
|
184
|
+
options.collecting ? ' ? / Esc return to collection progress' : ' / search rendered panels · n/N matches',
|
|
185
|
+
' ? / Esc return · q close',
|
|
186
|
+
], options);
|
|
187
|
+
}
|
|
188
|
+
function buildLookupBrowserSearchFrame(document, query, notice, options = {}) {
|
|
189
|
+
return renderStandaloneBrowserFrame([
|
|
190
|
+
'WHOISleuth evidence browser search',
|
|
191
|
+
boundedTargetLine(safeTerminalValue(document.query, 'unknown target'), boundedBrowserDimensions(options).width),
|
|
192
|
+
`Mode ${safeTerminalValue(document.mode, 'unknown mode')}`,
|
|
193
|
+
'',
|
|
194
|
+
`Search ${query || 'type a bounded term'}`,
|
|
195
|
+
notice || 'Search covers only the bounded text rendered in the evidence panels.',
|
|
196
|
+
'',
|
|
197
|
+
'Enter apply · Backspace edit · Esc cancel',
|
|
198
|
+
], options);
|
|
199
|
+
}
|
|
200
|
+
function buildLookupCollectionFrame(queryValue, modeValue, sources, options = {}) {
|
|
201
|
+
const { width, height } = boundedBrowserDimensions(options);
|
|
202
|
+
const query = safeTerminalValue(queryValue, 'unknown target');
|
|
203
|
+
const mode = modeValue === 'deep' ? 'deep' : 'fast';
|
|
204
|
+
const header = [
|
|
205
|
+
'WHOISleuth evidence browser',
|
|
206
|
+
boundedTargetLine(query, width),
|
|
207
|
+
`Mode ${mode}`,
|
|
208
|
+
'Collection in progress:',
|
|
209
|
+
];
|
|
210
|
+
const footer = ['? help · progress is transient', 'q/Esc/Ctrl-C cancel'];
|
|
211
|
+
const bodyCapacity = Math.max(1, height - header.length - footer.length);
|
|
212
|
+
const body = mode === 'fast'
|
|
213
|
+
? [
|
|
214
|
+
'Registration evidence collecting',
|
|
215
|
+
'Fast mode waits for the final bounded result.',
|
|
216
|
+
'Per-source states are not fabricated.',
|
|
217
|
+
]
|
|
218
|
+
: sources.flatMap((source) => {
|
|
219
|
+
const state = source.state === 'pending'
|
|
220
|
+
? 'pending'
|
|
221
|
+
: `${source.state}${source.complete ? ' · complete' : ' · incomplete'}${source.truncated ? ' · truncated' : ''}`;
|
|
222
|
+
const limitation = source.state !== 'pending' && source.limitation
|
|
223
|
+
? ` · ${source.limitation}`
|
|
224
|
+
: '';
|
|
225
|
+
return [clipTerminalLine(`${source.source.replaceAll('_', ' ')} ${state}${limitation}`, width)];
|
|
226
|
+
});
|
|
227
|
+
const visible = body.slice(0, bodyCapacity);
|
|
228
|
+
if (body.length > visible.length && visible.length) {
|
|
229
|
+
visible[visible.length - 1] = `+${body.length - visible.length + 1} source states not visible at this height`;
|
|
230
|
+
}
|
|
231
|
+
return renderStandaloneBrowserFrame([...header, ...visible, ...footer], {
|
|
232
|
+
...options,
|
|
233
|
+
width,
|
|
234
|
+
height,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function buildLookupBrowserFrame(document, options = {}) {
|
|
238
|
+
const panels = buildLookupBrowserPanels(document);
|
|
239
|
+
const { width, height } = boundedBrowserDimensions(options);
|
|
240
|
+
if (!panels.length)
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
value: `${clipTerminalLine('WHOISleuth evidence browser', width)}\n${clipTerminalLine('No bounded terminal panels were available.', width)}\n`,
|
|
243
|
+
scrollOffset: 0,
|
|
244
|
+
maximumScrollOffset: 0,
|
|
245
|
+
pageSize: 0,
|
|
246
|
+
totalBodyLines: 0,
|
|
247
|
+
});
|
|
248
|
+
const activeIndex = Math.max(0, Math.min(panels.length - 1, Number.isSafeInteger(options.activeIndex) ? Number(options.activeIndex) : 0));
|
|
249
|
+
const panel = panels[activeIndex];
|
|
250
|
+
const query = safeTerminalValue(document.query, 'unknown target');
|
|
251
|
+
const mode = safeTerminalValue(document.mode, 'unknown mode');
|
|
252
|
+
const header = [
|
|
253
|
+
'WHOISleuth evidence browser',
|
|
254
|
+
boundedTargetLine(query, width),
|
|
255
|
+
`Mode ${clipTerminalLine(mode, Math.max(1, width - 8))}`,
|
|
256
|
+
browserTabLine(panels, activeIndex, width),
|
|
257
|
+
`${panel.label}:`,
|
|
258
|
+
];
|
|
259
|
+
const footerLineCount = 2;
|
|
260
|
+
const body = wrapLookupBrowserLines(panel.lines.join('\n'), width);
|
|
261
|
+
const bodyCapacity = Math.max(1, height - header.length - footerLineCount);
|
|
262
|
+
const maximumScrollOffset = Math.max(0, body.length - bodyCapacity);
|
|
263
|
+
const requestedScrollOffset = Number.isSafeInteger(options.scrollOffset) ? Number(options.scrollOffset) : 0;
|
|
264
|
+
const scrollOffset = Math.max(0, Math.min(maximumScrollOffset, requestedScrollOffset));
|
|
265
|
+
const visibleBody = body.slice(scrollOffset, scrollOffset + bodyCapacity);
|
|
266
|
+
const firstVisibleLine = body.length ? scrollOffset + 1 : 0;
|
|
267
|
+
const lastVisibleLine = body.length ? scrollOffset + visibleBody.length : 0;
|
|
268
|
+
const footer = [
|
|
269
|
+
options.searchStatus || `View ${activeIndex + 1}/${panels.length} · lines ${firstVisibleLine}-${lastVisibleLine}/${body.length}`,
|
|
270
|
+
'←/→ panels · ↑/↓ scroll · / search · ? help · q close',
|
|
271
|
+
].map((line) => clipTerminalLine(line, width));
|
|
272
|
+
const value = [...header, ...visibleBody, ...footer].slice(0, height).join('\n');
|
|
273
|
+
return Object.freeze({
|
|
274
|
+
value: presentTerminalOutput(`${value}\n`, {
|
|
275
|
+
color: options.color === true,
|
|
276
|
+
interactive: true,
|
|
277
|
+
palette: options.palette || 'auto',
|
|
278
|
+
width: null,
|
|
279
|
+
}),
|
|
280
|
+
scrollOffset,
|
|
281
|
+
maximumScrollOffset,
|
|
282
|
+
pageSize: bodyCapacity,
|
|
283
|
+
totalBodyLines: body.length,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function renderLookupBrowser(document, options = {}) {
|
|
287
|
+
return buildLookupBrowserFrame(document, options).value;
|
|
288
|
+
}
|
|
289
|
+
export { MIN_LOOKUP_BROWSER_HEIGHT, MAX_LOOKUP_BROWSER_PANELS, MAX_LOOKUP_BROWSER_SEARCH_BYTES, MAX_LOOKUP_BROWSER_SEARCH_MATCHES, MAX_LOOKUP_BROWSER_SEARCH_SCALARS, boundedBrowserDimensions, boundedSearchText, buildLookupBrowserHelpFrame, buildLookupBrowserFrame, buildLookupBrowserPanels, buildLookupBrowserSearchFrame, buildLookupCollectionFrame, findLookupBrowserMatches, renderedPanelLineOffset, renderLookupBrowser, terminalDisplayWidth, };
|