@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
|
@@ -1,1232 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const MAX_LOOKUP_TERMINAL_NAMES = 5;
|
|
7
|
-
const MAX_LOOKUP_TERMINAL_ALPN_IDS = 4;
|
|
8
|
-
const MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH = 32;
|
|
9
|
-
const MAX_LOOKUP_TERMINAL_LIMITATIONS = 3;
|
|
10
|
-
const MAX_LOOKUP_TERMINAL_CIDRS = 5;
|
|
11
|
-
const MAX_LOOKUP_TERMINAL_FINDINGS = 5;
|
|
12
|
-
const MAX_LOOKUP_TERMINAL_ABUSE_ROUTES = 6;
|
|
13
|
-
const MAX_CT_TERMINAL_MATCHES = 100;
|
|
14
|
-
const MAX_CT_TERMINAL_HOSTNAMES = 5;
|
|
15
|
-
const MAX_DISCOVER_TERMINAL_CANDIDATES = 200;
|
|
16
|
-
const MAX_POSTURE_TERMINAL_RECORDS = 5;
|
|
17
|
-
const MAX_TLS_TERMINAL_ALT_NAMES = 10;
|
|
18
|
-
const MAX_TLS_TERMINAL_PURPOSES = 8;
|
|
19
|
-
const MAX_RISK_CALIBRATION_TERMINAL_RECORDS = 100;
|
|
20
|
-
// Archive values are attacker-controlled evidence. Remove the complete Unicode
|
|
21
|
-
// default-ignorable class so visually identical identifiers cannot differ only
|
|
22
|
-
// through soft hyphens, combining grapheme joiners, variation selectors, tag
|
|
23
|
-
// characters, bidi controls, or other zero-width formatting code points.
|
|
24
|
-
const TERMINAL_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
25
|
-
function safeTerminalValue(value, fallback = '—') {
|
|
26
|
-
if (value === null || value === undefined || value === '')
|
|
27
|
-
return fallback;
|
|
28
|
-
const normalized = String(value)
|
|
29
|
-
.replace(/[\x00-\x1f\x7f-\x9f]+/g, ' ')
|
|
30
|
-
.replace(TERMINAL_DEFAULT_IGNORABLE_RE, '')
|
|
31
|
-
.replace(/\s+/g, ' ')
|
|
32
|
-
.trim();
|
|
33
|
-
if (!normalized)
|
|
34
|
-
return fallback;
|
|
35
|
-
return normalized.length > MAX_TERMINAL_VALUE_LENGTH
|
|
36
|
-
? `${normalized.slice(0, MAX_TERMINAL_VALUE_LENGTH - 1)}…`
|
|
37
|
-
: normalized;
|
|
38
|
-
}
|
|
39
|
-
function boundedTerminalList(values, omitted) {
|
|
40
|
-
const suffix = omitted > 0 ? ` · +${omitted} more` : '';
|
|
41
|
-
const maximumBodyLength = Math.max(1, MAX_TERMINAL_VALUE_LENGTH - suffix.length);
|
|
42
|
-
const joined = values.join(', ');
|
|
43
|
-
const body = joined.length > maximumBodyLength
|
|
44
|
-
? `${joined.slice(0, Math.max(0, maximumBodyLength - 1))}…`
|
|
45
|
-
: joined;
|
|
46
|
-
return `${body}${suffix}`;
|
|
47
|
-
}
|
|
48
|
-
function boundedTerminalComponent(value, maximum) {
|
|
49
|
-
const normalized = safeTerminalValue(value);
|
|
50
|
-
return normalized.length > maximum ? `${normalized.slice(0, Math.max(0, maximum - 1))}…` : normalized;
|
|
51
|
-
}
|
|
52
|
-
function boundedTerminalWithSuffix(value, suffix) {
|
|
53
|
-
if (!suffix)
|
|
54
|
-
return safeTerminalValue(value);
|
|
55
|
-
const retainedSuffix = suffix.length >= MAX_TERMINAL_VALUE_LENGTH
|
|
56
|
-
? `${suffix.slice(0, MAX_TERMINAL_VALUE_LENGTH - 1)}…`
|
|
57
|
-
: suffix;
|
|
58
|
-
const maximumValueLength = Math.max(1, MAX_TERMINAL_VALUE_LENGTH - retainedSuffix.length);
|
|
59
|
-
const retainedValue = value.length > maximumValueLength
|
|
60
|
-
? `${value.slice(0, Math.max(0, maximumValueLength - 1))}…`
|
|
61
|
-
: value;
|
|
62
|
-
return `${retainedValue}${retainedSuffix}`;
|
|
63
|
-
}
|
|
64
|
-
function titleCase(value) {
|
|
65
|
-
const text = safeTerminalValue(value, 'unknown').replaceAll('_', ' ');
|
|
66
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
67
|
-
}
|
|
68
|
-
function terminalRecord(value) {
|
|
69
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
70
|
-
? value
|
|
71
|
-
: {};
|
|
72
|
-
}
|
|
73
|
-
function terminalCount(value) {
|
|
74
|
-
const count = Number(value);
|
|
75
|
-
return Number.isSafeInteger(count) && count >= 0 ? Math.min(count, 999) : 0;
|
|
76
|
-
}
|
|
77
|
-
function terminalDisplayCount(value) {
|
|
78
|
-
const count = Number(value);
|
|
79
|
-
return Number.isSafeInteger(count) && count >= 0 ? String(count) : '0';
|
|
80
|
-
}
|
|
81
|
-
function terminalCountSummary(value, labels) {
|
|
82
|
-
const source = terminalRecord(value);
|
|
83
|
-
return labels
|
|
84
|
-
.map(([key, label]) => {
|
|
85
|
-
const count = Number(source[key]);
|
|
86
|
-
return [label, Number.isSafeInteger(count) && count >= 0 ? count : 0];
|
|
87
|
-
})
|
|
88
|
-
.filter(([, count]) => count > 0)
|
|
89
|
-
.map(([label, count]) => `${label} ${terminalDisplayCount(count)}`)
|
|
90
|
-
.join(', ');
|
|
91
|
-
}
|
|
92
|
-
function appendPublicationMetadataLines(lines, value, detail) {
|
|
93
|
-
if (detail === 'summary' || !validPagePublicationMetadata(value))
|
|
94
|
-
return;
|
|
95
|
-
const metadata = terminalRecord(value);
|
|
96
|
-
const robots = terminalRecord(metadata.robots);
|
|
97
|
-
const twitter = terminalRecord(metadata.twitterCard);
|
|
98
|
-
const headings = terminalRecord(metadata.headings);
|
|
99
|
-
const images = terminalRecord(metadata.images);
|
|
100
|
-
const blocking = terminalRecord(metadata.renderBlockingCandidates);
|
|
101
|
-
lines.push(`Publication ${metadata.complete === true ? 'Complete' : 'Partial'} · robots ${titleCase(robots.status)} · card ${titleCase(twitter.status)}`);
|
|
102
|
-
const directives = Array.isArray(robots.directives) ? robots.directives.map((item) => safeTerminalValue(item)) : [];
|
|
103
|
-
if (directives.length)
|
|
104
|
-
lines.push(`Robots ${safeTerminalValue(directives.join(', '))}${robots.conflicting === true ? ' · conflicting' : ''}`);
|
|
105
|
-
else if (robots.status === 'not_observed')
|
|
106
|
-
lines.push('Robots No declaration observed in captured static HTML');
|
|
107
|
-
if (twitter.cardType)
|
|
108
|
-
lines.push(`Card type ${safeTerminalValue(twitter.cardType)}`);
|
|
109
|
-
lines.push(`Static page headings ${terminalDisplayCount(headings.total)} · images ${terminalDisplayCount(images.total)} · blocking candidates ${terminalDisplayCount(blocking.total)}`);
|
|
110
|
-
if (detail === 'verbose') {
|
|
111
|
-
lines.push(`Image alt missing ${terminalDisplayCount(images.altMissing)} · empty ${terminalDisplayCount(images.altEmpty)} · non-empty ${terminalDisplayCount(images.altNonEmpty)} · unclassified ${terminalDisplayCount(images.altUnclassified)}`);
|
|
112
|
-
lines.push(`Blocking scripts ${terminalDisplayCount(blocking.script)} · stylesheets ${terminalDisplayCount(blocking.stylesheet)} · static candidates only`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function appendDeliveryMetadataLines(lines, value, verbose = false) {
|
|
116
|
-
if (!validHttpDeliveryMetadata(value))
|
|
117
|
-
return;
|
|
118
|
-
const metadata = terminalRecord(value);
|
|
119
|
-
const encoding = terminalRecord(metadata.contentEncoding);
|
|
120
|
-
const cache = terminalRecord(metadata.cachePolicy);
|
|
121
|
-
const codings = Array.isArray(encoding.codings) ? encoding.codings.map((item) => safeTerminalValue(item)) : [];
|
|
122
|
-
lines.push(`Delivery ${metadata.complete === true ? 'Complete' : 'Partial'} · encoding ${titleCase(encoding.status)} · cache ${titleCase(cache.status)}`);
|
|
123
|
-
if (codings.length)
|
|
124
|
-
lines.push(`Content coding ${safeTerminalValue(codings.join(', '))}`);
|
|
125
|
-
const cacheDirectives = [
|
|
126
|
-
['no-store', cache.noStore], ['no-cache', cache.noCache], ['must-revalidate', cache.mustRevalidate],
|
|
127
|
-
['public', cache.public], ['private', cache.private], ['immutable', cache.immutable],
|
|
128
|
-
].filter(([, present]) => present === true).map(([label]) => label);
|
|
129
|
-
if (cacheDirectives.length)
|
|
130
|
-
lines.push(`Cache policy ${safeTerminalValue(cacheDirectives.join(', '))}`);
|
|
131
|
-
if (verbose) {
|
|
132
|
-
const seconds = [
|
|
133
|
-
cache.maxAgeSeconds === null ? null : `max-age ${terminalDisplayCount(cache.maxAgeSeconds)}s`,
|
|
134
|
-
cache.sMaxAgeSeconds === null ? null : `s-maxage ${terminalDisplayCount(cache.sMaxAgeSeconds)}s`,
|
|
135
|
-
cache.ageSeconds === null ? null : `Age ${terminalDisplayCount(cache.ageSeconds)}s`,
|
|
136
|
-
].filter(Boolean);
|
|
137
|
-
if (seconds.length)
|
|
138
|
-
lines.push(`Cache timing ${safeTerminalValue(seconds.join(' · '))}`);
|
|
139
|
-
const declared = [
|
|
140
|
-
terminalRecord(cache.etag).present === true ? 'ETag' : null,
|
|
141
|
-
terminalRecord(cache.lastModified).present === true ? 'Last-Modified' : null,
|
|
142
|
-
terminalRecord(cache.expires).present === true ? 'Expires' : null,
|
|
143
|
-
].filter(Boolean);
|
|
144
|
-
lines.push(`Validators ${declared.length ? safeTerminalValue(declared.join(', ')) : 'No validator declaration observed'}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function appendSection(lines, label, values) {
|
|
148
|
-
if (!values.length)
|
|
149
|
-
return;
|
|
150
|
-
if (lines.length)
|
|
151
|
-
lines.push('');
|
|
152
|
-
lines.push(`${label}:`, ...values);
|
|
153
|
-
}
|
|
154
|
-
function formatLookupDnsRecord(value) {
|
|
155
|
-
if (typeof value === 'string' || typeof value === 'number')
|
|
156
|
-
return safeTerminalValue(value);
|
|
157
|
-
const record = terminalRecord(value);
|
|
158
|
-
if (!Object.keys(record).length)
|
|
159
|
-
return null;
|
|
160
|
-
if ('exchange' in record) {
|
|
161
|
-
return safeTerminalValue(`${safeTerminalValue(record.priority, '0')} ${record.exchange ? safeTerminalValue(record.exchange) : '.'}`);
|
|
162
|
-
}
|
|
163
|
-
if ('tag' in record && 'value' in record) {
|
|
164
|
-
return safeTerminalValue(`${record.critical ? 'critical ' : ''}${safeTerminalValue(record.tag)} ${safeTerminalValue(record.value)}`);
|
|
165
|
-
}
|
|
166
|
-
if ('nsname' in record) {
|
|
167
|
-
return safeTerminalValue(`${safeTerminalValue(record.nsname)} · serial ${safeTerminalValue(record.serial, 'unknown')}`);
|
|
168
|
-
}
|
|
169
|
-
if (record.type === 'HTTPS') {
|
|
170
|
-
const parameters = terminalRecord(record.parameters);
|
|
171
|
-
const alpnValues = Array.isArray(parameters.alpn) ? parameters.alpn : [];
|
|
172
|
-
const alpnVisible = alpnValues
|
|
173
|
-
.slice(0, MAX_LOOKUP_TERMINAL_ALPN_IDS)
|
|
174
|
-
.map((item) => boundedTerminalComponent(item, MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH));
|
|
175
|
-
const alpn = alpnVisible.length
|
|
176
|
-
? boundedTerminalList(alpnVisible, Math.max(0, alpnValues.length - alpnVisible.length))
|
|
177
|
-
: '';
|
|
178
|
-
const suffix = [parameters.port ? `port ${safeTerminalValue(parameters.port)}` : '', alpn ? `ALPN ${alpn}` : ''].filter(Boolean).join(' · ');
|
|
179
|
-
const target = `${safeTerminalValue(record.priority, '0')} ${safeTerminalValue(record.target, '.')}`;
|
|
180
|
-
return boundedTerminalWithSuffix(target, suffix ? ` · ${suffix}` : '');
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
function appendLookupRecordLine(lines, label, value) {
|
|
185
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
186
|
-
return;
|
|
187
|
-
const visible = value
|
|
188
|
-
.slice(0, MAX_LOOKUP_TERMINAL_RECORDS)
|
|
189
|
-
.map(formatLookupDnsRecord)
|
|
190
|
-
.filter((item) => item !== null);
|
|
191
|
-
if (!visible.length)
|
|
192
|
-
return;
|
|
193
|
-
const omitted = Math.max(0, value.length - visible.length);
|
|
194
|
-
lines.push(`${label.padEnd(14)}${boundedTerminalList(visible, omitted)}`);
|
|
195
|
-
}
|
|
196
|
-
function lookupObservationLabel(value, observed, notObserved) {
|
|
197
|
-
return value === true ? observed : value === false ? notObserved : 'Unavailable';
|
|
198
|
-
}
|
|
199
|
-
function boundedTerminalNames(value) {
|
|
200
|
-
if (typeof value === 'string')
|
|
201
|
-
return safeTerminalValue(value);
|
|
202
|
-
if (!Array.isArray(value))
|
|
203
|
-
return null;
|
|
204
|
-
const names = value
|
|
205
|
-
.filter((item) => typeof item === 'string' && Boolean(item.trim()))
|
|
206
|
-
.slice(0, MAX_LOOKUP_TERMINAL_NAMES)
|
|
207
|
-
.map((item) => safeTerminalValue(item));
|
|
208
|
-
if (!names.length)
|
|
209
|
-
return null;
|
|
210
|
-
return boundedTerminalList(names, Math.max(0, value.length - names.length));
|
|
211
|
-
}
|
|
212
|
-
function boundedTerminalStrings(value, maximum = MAX_LOOKUP_TERMINAL_NAMES) {
|
|
213
|
-
if (!Array.isArray(value))
|
|
214
|
-
return null;
|
|
215
|
-
const inspected = Math.min(value.length, maximum);
|
|
216
|
-
const retained = [];
|
|
217
|
-
let invalid = 0;
|
|
218
|
-
for (let index = 0; index < inspected; index += 1) {
|
|
219
|
-
const item = value[index];
|
|
220
|
-
if (typeof item === 'string' || typeof item === 'number')
|
|
221
|
-
retained.push(safeTerminalValue(item));
|
|
222
|
-
else
|
|
223
|
-
invalid += 1;
|
|
224
|
-
}
|
|
225
|
-
const uninspected = Math.max(0, value.length - inspected);
|
|
226
|
-
const disclosures = [
|
|
227
|
-
invalid > 0 ? `${invalid} malformed retained entr${invalid === 1 ? 'y' : 'ies'} omitted` : null,
|
|
228
|
-
uninspected > 0 ? `+${uninspected} more retained entr${uninspected === 1 ? 'y' : 'ies'}` : null,
|
|
229
|
-
].filter(Boolean).join(' · ');
|
|
230
|
-
if (!retained.length)
|
|
231
|
-
return disclosures || null;
|
|
232
|
-
return boundedTerminalWithSuffix(retained.join(', '), disclosures ? ` · ${disclosures}` : '');
|
|
233
|
-
}
|
|
234
|
-
function sourceCompleteness(value) {
|
|
235
|
-
if (value.complete === true && value.truncated !== true)
|
|
236
|
-
return 'Complete';
|
|
237
|
-
if (value.complete === false || value.truncated === true)
|
|
238
|
-
return value.truncated === true ? 'Incomplete · truncated' : 'Incomplete';
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
function positiveSourceStatus(value) {
|
|
242
|
-
return value === 'success' || value === 'partial';
|
|
243
|
-
}
|
|
244
|
-
function lifecycleDate(value, field) {
|
|
245
|
-
const lifecycle = terminalRecord(value.lifecycle);
|
|
246
|
-
return lifecycle[`${field}Iso`] ?? lifecycle[field];
|
|
247
|
-
}
|
|
248
|
-
function addressSelectionLabel(value) {
|
|
249
|
-
const labels = {
|
|
250
|
-
tls_connection: 'TLS connection',
|
|
251
|
-
dns_a: 'DNS A',
|
|
252
|
-
dns_aaaa: 'DNS AAAA',
|
|
253
|
-
};
|
|
254
|
-
return labels[String(value)] || titleCase(value);
|
|
255
|
-
}
|
|
256
|
-
function retainedUrlRelationship(value, target) {
|
|
257
|
-
const urlValue = terminalRecord(value);
|
|
258
|
-
if (typeof urlValue.url !== 'string')
|
|
259
|
-
return null;
|
|
260
|
-
try {
|
|
261
|
-
const declared = new URL(urlValue.url);
|
|
262
|
-
const targetValue = String(target || '').trim().replace(/\.$/u, '');
|
|
263
|
-
const targetHost = new URL(`http://${targetValue}`).hostname.toLowerCase();
|
|
264
|
-
const relationship = declared.hostname.toLowerCase() === targetHost
|
|
265
|
-
? 'Same target host'
|
|
266
|
-
: 'Different host declared';
|
|
267
|
-
return [
|
|
268
|
-
relationship,
|
|
269
|
-
urlValue.queryOmitted === true ? 'query omitted' : null,
|
|
270
|
-
urlValue.pathTruncated === true ? 'path shortened' : null,
|
|
271
|
-
].filter(Boolean).join(' · ');
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
function boundedFindingLabels(value, formatter) {
|
|
278
|
-
if (!Array.isArray(value))
|
|
279
|
-
return null;
|
|
280
|
-
const inspected = Math.min(value.length, MAX_LOOKUP_TERMINAL_FINDINGS);
|
|
281
|
-
const retained = value
|
|
282
|
-
.slice(0, inspected)
|
|
283
|
-
.map(terminalRecord)
|
|
284
|
-
.map(formatter)
|
|
285
|
-
.filter(Boolean);
|
|
286
|
-
if (!retained.length)
|
|
287
|
-
return null;
|
|
288
|
-
return boundedTerminalList(retained, Math.max(0, value.length - inspected));
|
|
289
|
-
}
|
|
290
|
-
function appendNetworkRegistrationLines(lines, parsed, type, detail) {
|
|
291
|
-
if (!Object.keys(parsed).length || detail === 'summary')
|
|
292
|
-
return;
|
|
293
|
-
if (parsed.handle)
|
|
294
|
-
lines.push(`RDAP handle ${safeTerminalValue(parsed.handle)}`);
|
|
295
|
-
if (parsed.name)
|
|
296
|
-
lines.push(`RDAP name ${safeTerminalValue(parsed.name)}`);
|
|
297
|
-
if (type === 'asn') {
|
|
298
|
-
if (parsed.startAutnum !== undefined && parsed.endAutnum !== undefined) {
|
|
299
|
-
const range = parsed.startAutnum === parsed.endAutnum
|
|
300
|
-
? safeTerminalValue(parsed.startAutnum)
|
|
301
|
-
: `${safeTerminalValue(parsed.startAutnum)} to ${safeTerminalValue(parsed.endAutnum)}`;
|
|
302
|
-
lines.push(`ASN range ${range}`);
|
|
303
|
-
}
|
|
304
|
-
else if (parsed.startAutnum !== undefined) {
|
|
305
|
-
lines.push(`ASN start ${safeTerminalValue(parsed.startAutnum)}`);
|
|
306
|
-
}
|
|
307
|
-
else if (parsed.endAutnum !== undefined) {
|
|
308
|
-
lines.push(`ASN end ${safeTerminalValue(parsed.endAutnum)}`);
|
|
309
|
-
}
|
|
310
|
-
if (parsed.autnumType)
|
|
311
|
-
lines.push(`Allocation ${safeTerminalValue(parsed.autnumType)}`);
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
if (parsed.startAddress && parsed.endAddress) {
|
|
315
|
-
lines.push(`Address range ${safeTerminalValue(parsed.startAddress)} to ${safeTerminalValue(parsed.endAddress)}`);
|
|
316
|
-
}
|
|
317
|
-
else if (parsed.startAddress) {
|
|
318
|
-
lines.push(`Address start ${safeTerminalValue(parsed.startAddress)}`);
|
|
319
|
-
}
|
|
320
|
-
else if (parsed.endAddress) {
|
|
321
|
-
lines.push(`Address end ${safeTerminalValue(parsed.endAddress)}`);
|
|
322
|
-
}
|
|
323
|
-
const cidrs = boundedTerminalStrings(parsed.cidrs, MAX_LOOKUP_TERMINAL_CIDRS);
|
|
324
|
-
if (cidrs)
|
|
325
|
-
lines.push(`CIDR prefixes ${cidrs}`);
|
|
326
|
-
if (parsed.networkType)
|
|
327
|
-
lines.push(`Allocation ${safeTerminalValue(parsed.networkType)}`);
|
|
328
|
-
}
|
|
329
|
-
if (parsed.country)
|
|
330
|
-
lines.push(`Country ${safeTerminalValue(parsed.country)}`);
|
|
331
|
-
const statuses = boundedTerminalStrings(parsed.statuses);
|
|
332
|
-
if (statuses)
|
|
333
|
-
lines.push(`Statuses ${statuses}`);
|
|
334
|
-
if (detail === 'verbose') {
|
|
335
|
-
const created = lifecycleDate(parsed, 'createdDate');
|
|
336
|
-
const updated = lifecycleDate(parsed, 'updatedDate');
|
|
337
|
-
if (created)
|
|
338
|
-
lines.push(`Created ${safeTerminalValue(created)}`);
|
|
339
|
-
if (updated)
|
|
340
|
-
lines.push(`Updated ${safeTerminalValue(updated)}`);
|
|
341
|
-
if (parsed.serverTruncated === true || parsed.cidrsTruncated === true)
|
|
342
|
-
lines.push('RDAP detail Source declared or local list truncation');
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
346
|
-
const availability = terminalRecord(document.availability);
|
|
347
|
-
const diagnostics = terminalRecord(document.diagnostics);
|
|
348
|
-
const rdapDiagnostics = terminalRecord(diagnostics.rdap);
|
|
349
|
-
const whoisDiagnostics = terminalRecord(diagnostics.whois);
|
|
350
|
-
const targetLines = [
|
|
351
|
-
`Query ${safeTerminalValue(document.query)}`,
|
|
352
|
-
`Type ${safeTerminalValue(document.type)}`,
|
|
353
|
-
`Mode ${titleCase(document.mode)}`,
|
|
354
|
-
];
|
|
355
|
-
if (document.inputHostname && document.inputHostname !== document.registrableDomain) {
|
|
356
|
-
targetLines.push(`Input host ${safeTerminalValue(document.inputHostname)}`);
|
|
357
|
-
targetLines.push(`Registry query ${safeTerminalValue(document.registrableDomain)}`);
|
|
358
|
-
}
|
|
359
|
-
if (availability.applicable) {
|
|
360
|
-
targetLines.push(`Availability ${titleCase(availability.state)}`);
|
|
361
|
-
targetLines.push(`Confidence ${titleCase(availability.confidence)}`);
|
|
362
|
-
}
|
|
363
|
-
const registrationLines = [
|
|
364
|
-
`RDAP ${titleCase(rdapDiagnostics.status)}`,
|
|
365
|
-
`WHOIS ${titleCase(whoisDiagnostics.status)}`,
|
|
366
|
-
];
|
|
367
|
-
if (detail !== 'summary' && rdapDiagnostics.endpoint) {
|
|
368
|
-
registrationLines.splice(1, 0, `RDAP source ${safeTerminalValue(rdapDiagnostics.endpoint)}`);
|
|
369
|
-
}
|
|
370
|
-
const registrarRdap = terminalRecord(rdapDiagnostics.registrar);
|
|
371
|
-
if (Object.keys(registrarRdap).length) {
|
|
372
|
-
registrationLines.push(`Registrar RDAP ${titleCase(registrarRdap.status)}`);
|
|
373
|
-
if (detail !== 'summary' && registrarRdap.endpoint) {
|
|
374
|
-
registrationLines.push(`Registrar source ${safeTerminalValue(registrarRdap.endpoint)}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (['ipv4', 'ipv6', 'asn'].includes(String(document.type)) && positiveSourceStatus(rdapDiagnostics.status)) {
|
|
378
|
-
const rdap = terminalRecord(document.rdap);
|
|
379
|
-
appendNetworkRegistrationLines(registrationLines, terminalRecord(rdap.parsed), document.type, detail);
|
|
380
|
-
}
|
|
381
|
-
const registryInsights = terminalRecord(document.registryInsights);
|
|
382
|
-
if (detail !== 'summary' && registryInsights.version === 1) {
|
|
383
|
-
const lifecycle = terminalRecord(registryInsights.lifecycle);
|
|
384
|
-
const disclosure = terminalRecord(registryInsights.contactDisclosure);
|
|
385
|
-
const registryDisclosure = terminalRecord(disclosure.registryRdap);
|
|
386
|
-
const whoisDisclosure = terminalRecord(disclosure.whois);
|
|
387
|
-
const reconciliation = terminalRecord(registryInsights.reconciliation);
|
|
388
|
-
const publications = Array.isArray(registryInsights.publications)
|
|
389
|
-
? registryInsights.publications.map(terminalRecord)
|
|
390
|
-
: [];
|
|
391
|
-
const publicationCounts = {
|
|
392
|
-
complete: publications.filter((item) => item.state === 'complete').length,
|
|
393
|
-
partial: publications.filter((item) => item.state === 'partial').length,
|
|
394
|
-
unavailable: publications.filter((item) => item.state === 'unavailable').length,
|
|
395
|
-
};
|
|
396
|
-
registrationLines.push(`Lifecycle ${titleCase(lifecycle.label)}`);
|
|
397
|
-
registrationLines.push(`Disclosure RDAP ${titleCase(registryDisclosure.state)} · WHOIS ${titleCase(whoisDisclosure.state)}`);
|
|
398
|
-
registrationLines.push(`Reconciliation ${titleCase(reconciliation.state)}`);
|
|
399
|
-
registrationLines.push(`Publications ${publicationCounts.complete} complete · ${publicationCounts.partial} partial · ${publicationCounts.unavailable} unavailable`);
|
|
400
|
-
}
|
|
401
|
-
const dnsLines = [];
|
|
402
|
-
const mailLines = [];
|
|
403
|
-
const tlsLines = [];
|
|
404
|
-
if (document.mode === 'deep' && document.type === 'domain') {
|
|
405
|
-
const dns = terminalRecord(availability.dns);
|
|
406
|
-
const dnsRecords = terminalRecord(dns.records);
|
|
407
|
-
const dnsDiagnostics = terminalRecord(dns.diagnostics);
|
|
408
|
-
if (Object.keys(dns).length) {
|
|
409
|
-
dnsLines.push(`Evidence ${titleCase(dns.status)}`);
|
|
410
|
-
dnsLines.push(`Completeness ${dns.complete === true ? 'Complete' : 'Incomplete'}`);
|
|
411
|
-
const recordTypes = [
|
|
412
|
-
['a', 'A'], ['aaaa', 'AAAA'], ['cname', 'CNAME'], ['ns', 'NS'],
|
|
413
|
-
['mx', 'MX'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['caa', 'CAA'],
|
|
414
|
-
['soa', 'SOA'], ['https', 'HTTPS'],
|
|
415
|
-
];
|
|
416
|
-
const retainedCounts = recordTypes
|
|
417
|
-
.map(([key, label]) => [label, Array.isArray(dnsRecords[key]) ? dnsRecords[key].length : 0])
|
|
418
|
-
.filter(([, count]) => count > 0)
|
|
419
|
-
.map(([label, count]) => `${label} ${count}`);
|
|
420
|
-
dnsLines.push(`Retained ${retainedCounts.length ? safeTerminalValue(retainedCounts.join(' · ')) : 'No records retained'}`);
|
|
421
|
-
if (detail === 'verbose') {
|
|
422
|
-
const unsettled = recordTypes.flatMap(([key, label]) => {
|
|
423
|
-
const diagnostic = terminalRecord(dnsDiagnostics[key]);
|
|
424
|
-
return diagnostic.status && !['success', 'not_found'].includes(String(diagnostic.status))
|
|
425
|
-
? [`${label} ${titleCase(diagnostic.status)}`]
|
|
426
|
-
: [];
|
|
427
|
-
});
|
|
428
|
-
if (unsettled.length)
|
|
429
|
-
dnsLines.push(`Query states ${safeTerminalValue(unsettled.join(' · '))}`);
|
|
430
|
-
}
|
|
431
|
-
if (detail !== 'summary') {
|
|
432
|
-
for (const [key, label] of recordTypes)
|
|
433
|
-
appendLookupRecordLine(dnsLines, label, dnsRecords[key]);
|
|
434
|
-
}
|
|
435
|
-
mailLines.push(`Evidence ${titleCase(dns.status)}`);
|
|
436
|
-
mailLines.push(`Mail exchange ${lookupObservationLabel(dns.hasNullMx === true ? true : dns.hasMx, dns.hasNullMx === true ? 'Null MX observed' : 'MX observed', 'No MX observed')}`);
|
|
437
|
-
mailLines.push(`SPF ${lookupObservationLabel(dns.hasSpf, 'Record observed', 'No record observed')}`);
|
|
438
|
-
mailLines.push(`DMARC ${lookupObservationLabel(dns.hasDmarc, 'Record observed', 'No record observed')}`);
|
|
439
|
-
if (detail !== 'summary') {
|
|
440
|
-
appendLookupRecordLine(mailLines, 'MX hosts', Array.isArray(dns.mxHosts) ? dns.mxHosts : []);
|
|
441
|
-
appendLookupRecordLine(mailLines, 'SPF policy', dnsRecords.spf);
|
|
442
|
-
appendLookupRecordLine(mailLines, 'DMARC policy', dnsRecords.dmarc);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const tls = terminalRecord(availability.tls);
|
|
446
|
-
if (Object.keys(tls).length) {
|
|
447
|
-
const usableTls = positiveSourceStatus(tls.status);
|
|
448
|
-
const certificate = usableTls ? terminalRecord(tls.certificate) : {};
|
|
449
|
-
const subject = terminalRecord(certificate.subject);
|
|
450
|
-
const issuer = terminalRecord(certificate.issuer);
|
|
451
|
-
const authorization = usableTls ? terminalRecord(tls.authorization) : {};
|
|
452
|
-
const hostname = usableTls ? terminalRecord(tls.hostname) : {};
|
|
453
|
-
const validity = usableTls ? terminalRecord(tls.validity) : {};
|
|
454
|
-
const cipher = usableTls ? terminalRecord(tls.cipher) : {};
|
|
455
|
-
tlsLines.push(`Evidence ${titleCase(tls.status)}`);
|
|
456
|
-
const completeness = sourceCompleteness(tls);
|
|
457
|
-
if (completeness)
|
|
458
|
-
tlsLines.push(`Completeness ${completeness}`);
|
|
459
|
-
if (usableTls && tls.connectedAddress && detail !== 'summary')
|
|
460
|
-
tlsLines.push(`Address ${safeTerminalValue(tls.connectedAddress)}`);
|
|
461
|
-
if (usableTls && tls.protocol)
|
|
462
|
-
tlsLines.push(`Protocol ${safeTerminalValue(tls.protocol)}`);
|
|
463
|
-
if (usableTls && tls.alpnProtocol && detail !== 'summary')
|
|
464
|
-
tlsLines.push(`ALPN ${safeTerminalValue(tls.alpnProtocol)}`);
|
|
465
|
-
if (cipher.standardName || cipher.name)
|
|
466
|
-
tlsLines.push(`Cipher ${safeTerminalValue(cipher.standardName || cipher.name)}`);
|
|
467
|
-
tlsLines.push(`Chain trust ${authorization.authorized === true ? 'Authorised' : authorization.authorized === false ? 'Not authorised' : 'Unavailable'}`);
|
|
468
|
-
tlsLines.push(`Hostname ${hostname.matches === true ? 'Matched' : hostname.matches === false ? 'Not matched' : 'Unavailable'}`);
|
|
469
|
-
tlsLines.push(`Validity ${validity.status ? titleCase(validity.status) : 'Unavailable'}`);
|
|
470
|
-
const subjectName = boundedTerminalNames(subject.commonNames ?? subject.CN);
|
|
471
|
-
const issuerName = boundedTerminalNames(issuer.commonNames ?? issuer.CN);
|
|
472
|
-
if (detail !== 'summary' && subjectName)
|
|
473
|
-
tlsLines.push(`Subject ${subjectName}`);
|
|
474
|
-
if (detail !== 'summary' && issuerName)
|
|
475
|
-
tlsLines.push(`Issuer ${issuerName}`);
|
|
476
|
-
if (detail !== 'summary' && certificate.validFrom)
|
|
477
|
-
tlsLines.push(`Valid from ${safeTerminalValue(certificate.validFrom)}`);
|
|
478
|
-
if (detail !== 'summary' && certificate.validTo)
|
|
479
|
-
tlsLines.push(`Valid until ${safeTerminalValue(certificate.validTo)}`);
|
|
480
|
-
const publicKey = terminalRecord(certificate.publicKey);
|
|
481
|
-
if (detail !== 'summary' && (publicKey.type || publicKey.bits || publicKey.curve)) {
|
|
482
|
-
tlsLines.push(`Public key ${safeTerminalValue([publicKey.type, publicKey.bits ? `${publicKey.bits} bits` : null, publicKey.curve].filter(Boolean).join(' '))}`);
|
|
483
|
-
}
|
|
484
|
-
const signature = terminalRecord(certificate.signature);
|
|
485
|
-
if (detail !== 'summary' && (signature.algorithm || signature.oid)) {
|
|
486
|
-
tlsLines.push(`Signature ${safeTerminalValue(signature.algorithm || signature.oid)}`);
|
|
487
|
-
}
|
|
488
|
-
const altNames = terminalRecord(certificate.subjectAltNames);
|
|
489
|
-
const dnsAltNames = Array.isArray(altNames.dnsNames) ? altNames.dnsNames : [];
|
|
490
|
-
const ipAltNames = Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses : [];
|
|
491
|
-
const sanClasses = terminalCountSummary(altNames.classes, [
|
|
492
|
-
['dns', 'DNS'], ['ip', 'IP'], ['email', 'email'], ['uri', 'URI'],
|
|
493
|
-
['directoryName', 'directory name'], ['registeredId', 'registered ID'],
|
|
494
|
-
['otherName', 'other name'], ['unclassified', 'other'],
|
|
495
|
-
]);
|
|
496
|
-
if (detail !== 'summary' && (dnsAltNames.length || ipAltNames.length || sanClasses)) {
|
|
497
|
-
tlsLines.push(`SAN summary ${sanClasses || `DNS ${dnsAltNames.length} · IP ${ipAltNames.length}`}${altNames.truncated ? ' · truncated' : ''}`);
|
|
498
|
-
}
|
|
499
|
-
const chainCount = Array.isArray(tls.chain) ? tls.chain.length : 0;
|
|
500
|
-
if (detail !== 'summary' && (chainCount || tls.chainTruncated === true)) {
|
|
501
|
-
tlsLines.push(`Chain ${chainCount} retained certificate${chainCount === 1 ? '' : 's'}${tls.chainTruncated ? ' · truncated' : ''}`);
|
|
502
|
-
}
|
|
503
|
-
const aia = terminalRecord(certificate.authorityInformationAccess);
|
|
504
|
-
if (detail !== 'summary' && Object.keys(aia).length) {
|
|
505
|
-
const ocsp = terminalRecord(aia.ocsp);
|
|
506
|
-
const caIssuers = terminalRecord(aia.caIssuers);
|
|
507
|
-
tlsLines.push(`AIA presence OCSP ${terminalCount(ocsp.total)} · CA issuers ${terminalCount(caIssuers.total)} · `
|
|
508
|
-
+ `unknown methods ${terminalCount(aia.unknownMethods)}${aia.truncated ? ' · truncated' : ''}`);
|
|
509
|
-
}
|
|
510
|
-
if (detail === 'verbose') {
|
|
511
|
-
const visibleAltNames = [...dnsAltNames, ...ipAltNames]
|
|
512
|
-
.slice(0, MAX_TLS_TERMINAL_ALT_NAMES)
|
|
513
|
-
.map((value) => safeTerminalValue(value));
|
|
514
|
-
if (visibleAltNames.length)
|
|
515
|
-
tlsLines.push(`Alt names ${boundedTerminalList(visibleAltNames, Math.max(0, dnsAltNames.length + ipAltNames.length - visibleAltNames.length))}`);
|
|
516
|
-
const purposes = terminalRecord(certificate.extendedKeyUsage);
|
|
517
|
-
const purposeValues = Array.isArray(purposes.values) ? purposes.values : [];
|
|
518
|
-
const visiblePurposes = purposeValues.slice(0, MAX_TLS_TERMINAL_PURPOSES).map((value) => {
|
|
519
|
-
const purpose = terminalRecord(value);
|
|
520
|
-
return safeTerminalValue(purpose.name || purpose.oid || 'Unrecognized purpose');
|
|
521
|
-
});
|
|
522
|
-
if (visiblePurposes.length)
|
|
523
|
-
tlsLines.push(`Purposes ${boundedTerminalList(visiblePurposes, Math.max(0, purposeValues.length - visiblePurposes.length))}${purposes.truncated ? ' · truncated' : ''}`);
|
|
524
|
-
const findings = boundedFindingLabels(tls.findings, (finding) => safeTerminalValue(finding.label, 'Unlabelled finding'));
|
|
525
|
-
if (findings)
|
|
526
|
-
tlsLines.push(`Findings ${findings}`);
|
|
527
|
-
}
|
|
528
|
-
if (detail === 'verbose' && certificate.fingerprintSha256)
|
|
529
|
-
tlsLines.push(`SHA-256 ${safeTerminalValue(certificate.fingerprintSha256)}`);
|
|
530
|
-
if (detail === 'verbose' && Array.isArray(tls.limitations)) {
|
|
531
|
-
for (const limitation of tls.limitations.slice(0, MAX_LOOKUP_TERMINAL_LIMITATIONS))
|
|
532
|
-
tlsLines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
533
|
-
const omittedLimitations = Math.max(0, tls.limitations.length - MAX_LOOKUP_TERMINAL_LIMITATIONS);
|
|
534
|
-
if (omittedLimitations)
|
|
535
|
-
tlsLines.push(`Limitation +${omittedLimitations} more retained limitation${omittedLimitations === 1 ? '' : 's'}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const websiteLines = [];
|
|
540
|
-
if (document.mode === 'deep' && document.type === 'domain') {
|
|
541
|
-
const http = terminalRecord(availability.http);
|
|
542
|
-
const httpResponse = terminalRecord(http.response);
|
|
543
|
-
const credentialSurface = terminalRecord(availability.credentialSurfaceProfile);
|
|
544
|
-
const structuredIdentity = terminalRecord(availability.structuredDataIdentity);
|
|
545
|
-
const technology = terminalRecord(availability.technologyProfile);
|
|
546
|
-
const browserLibraries = terminalRecord(technology.browserLibraryProfile);
|
|
547
|
-
const posture = terminalRecord(availability.securityPosture);
|
|
548
|
-
const postureSummary = terminalRecord(posture.summary);
|
|
549
|
-
const pageIdentity = terminalRecord(availability.pageIdentity);
|
|
550
|
-
const pageRole = terminalRecord(availability.pageRoleProfile);
|
|
551
|
-
const clientBehavior = terminalRecord(availability.clientBehaviorProfile);
|
|
552
|
-
if (availability.activityStatus)
|
|
553
|
-
websiteLines.push(`Web activity ${titleCase(availability.activityStatus)}`);
|
|
554
|
-
if (detail !== 'summary' && availability.pageTitle)
|
|
555
|
-
websiteLines.push(`Page title ${safeTerminalValue(availability.pageTitle)}`);
|
|
556
|
-
if (http.status) {
|
|
557
|
-
websiteLines.push(`HTTP evidence ${titleCase(http.status)}`);
|
|
558
|
-
const responseDetail = [
|
|
559
|
-
httpResponse.status ? `HTTP ${safeTerminalValue(httpResponse.status)}` : null,
|
|
560
|
-
http.transportSecurity ? safeTerminalValue(http.transportSecurity).toUpperCase() : null,
|
|
561
|
-
].filter(Boolean).join(' · ');
|
|
562
|
-
if (detail !== 'summary' && responseDetail)
|
|
563
|
-
websiteLines.push(`HTTP response ${responseDetail}`);
|
|
564
|
-
if (detail !== 'summary')
|
|
565
|
-
appendDeliveryMetadataLines(websiteLines, httpResponse.deliveryMetadata, detail === 'verbose');
|
|
566
|
-
}
|
|
567
|
-
if (pageIdentity.status || pageIdentity.source === 'html') {
|
|
568
|
-
websiteLines.push(`Page identity ${titleCase(pageIdentity.status)}`);
|
|
569
|
-
const completeness = sourceCompleteness(pageIdentity);
|
|
570
|
-
if (completeness)
|
|
571
|
-
websiteLines.push(`Page coverage ${completeness}`);
|
|
572
|
-
if (positiveSourceStatus(pageIdentity.status) && detail !== 'summary') {
|
|
573
|
-
if (pageIdentity.documentLanguage)
|
|
574
|
-
websiteLines.push(`Language ${safeTerminalValue(pageIdentity.documentLanguage)}`);
|
|
575
|
-
const canonical = retainedUrlRelationship(pageIdentity.canonical, document.registrableDomain || availability.domain || document.query);
|
|
576
|
-
if (canonical)
|
|
577
|
-
websiteLines.push(`Canonical ${canonical}`);
|
|
578
|
-
const openGraph = terminalRecord(pageIdentity.openGraph);
|
|
579
|
-
if (openGraph.title || openGraph.siteName) {
|
|
580
|
-
websiteLines.push(`Open Graph ${safeTerminalValue([openGraph.siteName, openGraph.title].filter(Boolean).join(' · '))}`);
|
|
581
|
-
}
|
|
582
|
-
if (pageIdentity.generator)
|
|
583
|
-
websiteLines.push(`Generator ${safeTerminalValue(pageIdentity.generator)}`);
|
|
584
|
-
const forms = terminalRecord(pageIdentity.forms);
|
|
585
|
-
if (Object.keys(forms).length)
|
|
586
|
-
websiteLines.push(`Page forms ${terminalDisplayCount(forms.count)} total · ${terminalDisplayCount(forms.postCount)} POST · ${terminalDisplayCount(forms.insecureActionCount)} insecure action`);
|
|
587
|
-
const resources = terminalRecord(pageIdentity.resources);
|
|
588
|
-
const resourceTypes = terminalCountSummary(resources.byType, [
|
|
589
|
-
['image', 'image'], ['script', 'script'], ['stylesheet', 'stylesheet'], ['link', 'link'],
|
|
590
|
-
['frame', 'frame'], ['media', 'media'], ['object', 'object'],
|
|
591
|
-
]);
|
|
592
|
-
if (Object.keys(resources).length)
|
|
593
|
-
websiteLines.push(`Resources ${terminalDisplayCount(resources.count)} retained${resourceTypes ? ` · ${resourceTypes}` : ''}${resources.truncated ? ' · truncated' : ''}`);
|
|
594
|
-
const downloads = terminalRecord(pageIdentity.downloads);
|
|
595
|
-
if (Object.keys(downloads).length)
|
|
596
|
-
websiteLines.push(`Downloads ${terminalDisplayCount(downloads.count)} retained · ${terminalDisplayCount(downloads.riskyCount)} review file${Number(downloads.riskyCount) === 1 ? '' : 's'}${downloads.truncated ? ' · truncated' : ''}`);
|
|
597
|
-
if ([pageIdentity.embeddedOrigins, pageIdentity.contactDomains, pageIdentity.trackingIdentifiers].some(Array.isArray)) {
|
|
598
|
-
websiteLines.push(`Relationships ${Array.isArray(pageIdentity.embeddedOrigins) ? pageIdentity.embeddedOrigins.length : 0} embedded retained · `
|
|
599
|
-
+ `${Array.isArray(pageIdentity.contactDomains) ? pageIdentity.contactDomains.length : 0} contact domain${Array.isArray(pageIdentity.contactDomains) && pageIdentity.contactDomains.length === 1 ? '' : 's'} retained · `
|
|
600
|
-
+ `${Array.isArray(pageIdentity.trackingIdentifiers) ? pageIdentity.trackingIdentifiers.length : 0} tracking identifier${Array.isArray(pageIdentity.trackingIdentifiers) && pageIdentity.trackingIdentifiers.length === 1 ? '' : 's'} retained`);
|
|
601
|
-
}
|
|
602
|
-
appendPublicationMetadataLines(websiteLines, pageIdentity.publicationMetadata, detail);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (availability.phishingLanguageMatch && detail !== 'summary') {
|
|
606
|
-
websiteLines.push(`Content cue ${safeTerminalValue(availability.phishingLanguageMatch)} · static review label`);
|
|
607
|
-
}
|
|
608
|
-
if (credentialSurface.status || credentialSurface.source === 'html') {
|
|
609
|
-
const forms = terminalRecord(credentialSurface.forms);
|
|
610
|
-
const inputs = terminalRecord(credentialSurface.inputs);
|
|
611
|
-
const categories = terminalRecord(inputs.categories);
|
|
612
|
-
const actions = terminalRecord(forms.actions);
|
|
613
|
-
const formCount = terminalCount(forms.count);
|
|
614
|
-
const inputCount = terminalCount(inputs.count);
|
|
615
|
-
const externalActionCount = terminalCount(actions.external);
|
|
616
|
-
websiteLines.push(`Credential UI ${titleCase(credentialSurface.status)} · ${safeTerminalValue(inputs.classifiedCount, '0')} classified input${Number(inputs.classifiedCount) === 1 ? '' : 's'}`);
|
|
617
|
-
if (detail !== 'summary')
|
|
618
|
-
websiteLines.push(`Form surface ${safeTerminalValue(formCount)} form${formCount === 1 ? '' : 's'} · ${safeTerminalValue(inputCount)} input${inputCount === 1 ? '' : 's'} · ${safeTerminalValue(externalActionCount)} external action${externalActionCount === 1 ? '' : 's'}`);
|
|
619
|
-
const visible = [
|
|
620
|
-
['password', categories.password],
|
|
621
|
-
['email', categories.email],
|
|
622
|
-
['username', categories.username],
|
|
623
|
-
['one-time code', categories.one_time_code],
|
|
624
|
-
['payment related', categories.payment],
|
|
625
|
-
].filter(([, count]) => Number(count) > 0).map(([label, count]) => `${safeTerminalValue(label)} ${safeTerminalValue(count)}`);
|
|
626
|
-
if (detail !== 'summary' && visible.length)
|
|
627
|
-
websiteLines.push(`Input purposes ${safeTerminalValue(visible.join(' · '))}`);
|
|
628
|
-
}
|
|
629
|
-
if (structuredIdentity.status || structuredIdentity.source === 'html') {
|
|
630
|
-
const entities = Array.isArray(structuredIdentity.entities) ? structuredIdentity.entities : [];
|
|
631
|
-
websiteLines.push(`Structured ID ${titleCase(structuredIdentity.status)} · ${entities.length} declared entit${entities.length === 1 ? 'y' : 'ies'}`);
|
|
632
|
-
const visible = entities.slice(0, 4).map((entity) => {
|
|
633
|
-
const item = terminalRecord(entity);
|
|
634
|
-
const types = Array.isArray(item.types) ? item.types.slice(0, 3).map((value) => safeTerminalValue(value)).join('/') : '';
|
|
635
|
-
return `${safeTerminalValue(item.name, 'Unnamed declaration')}${types ? ` (${types})` : ''}`;
|
|
636
|
-
});
|
|
637
|
-
if (detail !== 'summary' && visible.length)
|
|
638
|
-
websiteLines.push(`Declarations ${safeTerminalValue(visible.join('; '))}`);
|
|
639
|
-
}
|
|
640
|
-
if (technology.status || technology.source === 'derived') {
|
|
641
|
-
const findings = Array.isArray(technology.findings) ? technology.findings : [];
|
|
642
|
-
websiteLines.push(`Technology ${titleCase(technology.status)} · ${findings.length} indicator${findings.length === 1 ? '' : 's'}`);
|
|
643
|
-
const visible = findings.slice(0, 6).map((finding) => {
|
|
644
|
-
const item = terminalRecord(finding);
|
|
645
|
-
const qualifiers = [item.category, item.confidence].filter(Boolean).map((value) => safeTerminalValue(value));
|
|
646
|
-
return `${safeTerminalValue(item.name, 'Unnamed indicator')}${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`;
|
|
647
|
-
});
|
|
648
|
-
if (detail !== 'summary' && visible.length) {
|
|
649
|
-
const omitted = findings.length - visible.length;
|
|
650
|
-
websiteLines.push(`Indicators ${safeTerminalValue(`${visible.join('; ')}${omitted > 0 ? `; +${omitted} more` : ''}`)}`);
|
|
651
|
-
}
|
|
652
|
-
if (detail !== 'summary' && (browserLibraries.profileVersion === 1 || browserLibraries.source === 'derived')) {
|
|
653
|
-
const libraries = Array.isArray(browserLibraries.findings) ? browserLibraries.findings : [];
|
|
654
|
-
const advisoryMatches = libraries.filter((finding) => terminalCount(terminalRecord(finding).advisoryCount) > 0).length;
|
|
655
|
-
websiteLines.push(`JS libraries ${titleCase(browserLibraries.status)} · ${libraries.length} apparent · `
|
|
656
|
-
+ `${advisoryMatches} with catalogue advisory match${advisoryMatches === 1 ? '' : 'es'}`);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (posture.status || posture.source === 'derived') {
|
|
660
|
-
websiteLines.push(`Posture ${titleCase(posture.status)}`);
|
|
661
|
-
if (detail !== 'summary' && Object.keys(postureSummary).length)
|
|
662
|
-
websiteLines.push(`Posture counts ${terminalCount(postureSummary.observed)} observed · `
|
|
663
|
-
+ `${terminalCount(postureSummary.potentialExposure)} potential exposure · `
|
|
664
|
-
+ `${terminalCount(postureSummary.observedAbsence)} observed absence · `
|
|
665
|
-
+ `${terminalCount(postureSummary.unavailable)} unavailable`);
|
|
666
|
-
if (detail === 'verbose' && positiveSourceStatus(posture.status)) {
|
|
667
|
-
const findings = boundedFindingLabels(posture.findings, (finding) => {
|
|
668
|
-
const state = finding.state ? ` (${titleCase(finding.state)})` : '';
|
|
669
|
-
return `${safeTerminalValue(finding.label, 'Unlabelled finding')}${state}`;
|
|
670
|
-
});
|
|
671
|
-
if (findings)
|
|
672
|
-
websiteLines.push(`Posture labels ${findings}`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
if (pageRole.status || pageRole.source === 'derived') {
|
|
676
|
-
websiteLines.push(`Page role ${titleCase(pageRole.status)}`);
|
|
677
|
-
const roleFindings = Array.isArray(pageRole.findings) ? pageRole.findings : [];
|
|
678
|
-
const primary = roleFindings.map(terminalRecord).find((finding) => finding.role === pageRole.primaryRole)
|
|
679
|
-
|| terminalRecord(roleFindings[0]);
|
|
680
|
-
if (positiveSourceStatus(pageRole.status) && detail !== 'summary' && Object.keys(primary).length) {
|
|
681
|
-
websiteLines.push(`Primary role ${safeTerminalValue(primary.label, 'Unclassified')} · ${titleCase(primary.confidence)}`);
|
|
682
|
-
}
|
|
683
|
-
if (positiveSourceStatus(pageRole.status) && detail === 'verbose') {
|
|
684
|
-
const findings = boundedFindingLabels(roleFindings, (finding) => `${safeTerminalValue(finding.label, 'Unclassified')} (${titleCase(finding.confidence)})`);
|
|
685
|
-
if (findings)
|
|
686
|
-
websiteLines.push(`Role labels ${findings}`);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
if (clientBehavior.status || clientBehavior.source === 'derived') {
|
|
690
|
-
websiteLines.push(`Client signals ${titleCase(clientBehavior.status)}`);
|
|
691
|
-
const scriptSummary = terminalRecord(clientBehavior.scriptSummary);
|
|
692
|
-
if (positiveSourceStatus(clientBehavior.status) && detail !== 'summary' && Object.keys(scriptSummary).length)
|
|
693
|
-
websiteLines.push(`Scripts ${terminalCount(scriptSummary.elementsObserved)} elements · ${terminalCount(scriptSummary.referencedScripts)} referenced · `
|
|
694
|
-
+ `${terminalCount(scriptSummary.inlineScripts)} inline · ${terminalCount(scriptSummary.moduleScripts)} modules`);
|
|
695
|
-
if (positiveSourceStatus(clientBehavior.status) && detail === 'verbose') {
|
|
696
|
-
const indicators = boundedFindingLabels(clientBehavior.indicators, (indicator) => {
|
|
697
|
-
const occurrences = terminalCount(indicator.occurrences);
|
|
698
|
-
return `${safeTerminalValue(indicator.label, 'Static indicator')} (${titleCase(indicator.evidenceClass)}, ${occurrences})`;
|
|
699
|
-
});
|
|
700
|
-
if (indicators)
|
|
701
|
-
websiteLines.push(`Client labels ${indicators}`);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
const networkLines = [];
|
|
706
|
-
if (document.mode === 'deep' && (document.type === 'ipv4' || document.type === 'ipv6')) {
|
|
707
|
-
const reverseDns = terminalRecord(document.reverseDns);
|
|
708
|
-
const reverseDnsRecords = terminalRecord(reverseDns.records);
|
|
709
|
-
const ptrNames = boundedTerminalStrings(reverseDnsRecords.ptr);
|
|
710
|
-
if (reverseDns.status)
|
|
711
|
-
networkLines.push(`Reverse DNS ${titleCase(reverseDns.status)}`);
|
|
712
|
-
const completeness = sourceCompleteness(reverseDns);
|
|
713
|
-
if (completeness)
|
|
714
|
-
networkLines.push(`PTR coverage ${completeness}`);
|
|
715
|
-
if (detail !== 'summary' && ptrNames)
|
|
716
|
-
networkLines.push(`PTR names ${ptrNames}`);
|
|
717
|
-
if (detail !== 'summary' && !ptrNames && reverseDns.status)
|
|
718
|
-
networkLines.push('PTR names No names retained; source state remains inconclusive');
|
|
719
|
-
}
|
|
720
|
-
const network = terminalRecord(document.networkContext);
|
|
721
|
-
if (network.contextVersion === 1) {
|
|
722
|
-
const endpoint = terminalRecord(network.endpoint);
|
|
723
|
-
const networkRecord = terminalRecord(network.network);
|
|
724
|
-
networkLines.push(`Network RDAP ${titleCase(network.status)}`);
|
|
725
|
-
const completeness = sourceCompleteness(network);
|
|
726
|
-
if (completeness)
|
|
727
|
-
networkLines.push(`Completeness ${completeness}`);
|
|
728
|
-
if (detail !== 'summary' && endpoint.address)
|
|
729
|
-
networkLines.push(`Selected IP ${safeTerminalValue(endpoint.address)}`);
|
|
730
|
-
if (detail !== 'summary' && endpoint.selectedFrom)
|
|
731
|
-
networkLines.push(`Selected from ${addressSelectionLabel(endpoint.selectedFrom)}`);
|
|
732
|
-
if (positiveSourceStatus(network.status) && (networkRecord.name || networkRecord.holder)) {
|
|
733
|
-
networkLines.push(`Network ${safeTerminalValue(networkRecord.name || networkRecord.holder)}`);
|
|
734
|
-
}
|
|
735
|
-
if (positiveSourceStatus(network.status) && detail !== 'summary') {
|
|
736
|
-
if (networkRecord.handle)
|
|
737
|
-
networkLines.push(`Network handle ${safeTerminalValue(networkRecord.handle)}`);
|
|
738
|
-
if (networkRecord.holder && networkRecord.holder !== networkRecord.name)
|
|
739
|
-
networkLines.push(`Holder ${safeTerminalValue(networkRecord.holder)}`);
|
|
740
|
-
const cidrs = boundedTerminalStrings(networkRecord.cidrs, MAX_LOOKUP_TERMINAL_CIDRS);
|
|
741
|
-
if (cidrs)
|
|
742
|
-
networkLines.push(`CIDR prefixes ${cidrs}`);
|
|
743
|
-
if (networkRecord.startAddress || networkRecord.endAddress)
|
|
744
|
-
networkLines.push(`Address range ${safeTerminalValue(networkRecord.startAddress)} to ${safeTerminalValue(networkRecord.endAddress)}`);
|
|
745
|
-
if (networkRecord.country)
|
|
746
|
-
networkLines.push(`Country ${safeTerminalValue(networkRecord.country)}`);
|
|
747
|
-
if (networkRecord.networkType)
|
|
748
|
-
networkLines.push(`Allocation ${safeTerminalValue(networkRecord.networkType)}`);
|
|
749
|
-
const allAbuseRoutes = Array.isArray(network.abuseRouting) ? network.abuseRouting : [];
|
|
750
|
-
const inspectedRoutes = Math.min(allAbuseRoutes.length, MAX_LOOKUP_TERMINAL_ABUSE_ROUTES);
|
|
751
|
-
const abuseRoutes = allAbuseRoutes.slice(0, inspectedRoutes).map(terminalRecord);
|
|
752
|
-
if (abuseRoutes.length) {
|
|
753
|
-
const channelCounts = new Map();
|
|
754
|
-
for (const route of abuseRoutes) {
|
|
755
|
-
const channel = ['email', 'phone'].includes(String(route.channel)) ? String(route.channel) : 'other';
|
|
756
|
-
channelCounts.set(channel, (channelCounts.get(channel) || 0) + 1);
|
|
757
|
-
}
|
|
758
|
-
const summary = [...channelCounts].map(([channel, count]) => `${channel} ${count}`).join(' · ');
|
|
759
|
-
const omittedRoutes = Math.max(0, allAbuseRoutes.length - inspectedRoutes);
|
|
760
|
-
networkLines.push(`Published routes ${abuseRoutes.length} retained${omittedRoutes ? ` · +${omittedRoutes} more entries` : ''} (${summary}); values omitted`);
|
|
761
|
-
}
|
|
762
|
-
if (detail === 'verbose' && networkRecord.databaseUpdatedAt)
|
|
763
|
-
networkLines.push(`Registry date ${safeTerminalValue(networkRecord.databaseUpdatedAt)}`);
|
|
764
|
-
if (detail === 'verbose' && Array.isArray(network.limitations)) {
|
|
765
|
-
for (const limitation of network.limitations.slice(0, MAX_LOOKUP_TERMINAL_LIMITATIONS))
|
|
766
|
-
networkLines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
767
|
-
const omitted = Math.max(0, network.limitations.length - MAX_LOOKUP_TERMINAL_LIMITATIONS);
|
|
768
|
-
if (omitted)
|
|
769
|
-
networkLines.push(`Limitation +${omitted} more retained limitation${omitted === 1 ? '' : 's'}`);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
const sourceHealthLines = [];
|
|
774
|
-
const registryAccess = terminalRecord(diagnostics.registryAccess);
|
|
775
|
-
if (Object.keys(registryAccess).length) {
|
|
776
|
-
sourceHealthLines.push(`Registry access .${safeTerminalValue(registryAccess.suffix)}`);
|
|
777
|
-
sourceHealthLines.push(`WHOIS access ${registryAccessProfileLabel(registryAccess.whoisAccessProfile)}`);
|
|
778
|
-
sourceHealthLines.push(`RDAP access ${registryAccessProfileLabel(registryAccess.rdapAccessProfile)}`);
|
|
779
|
-
if (detail !== 'summary' && registryAccess.limitation)
|
|
780
|
-
sourceHealthLines.push(`Access note ${safeTerminalValue(registryAccess.limitation)}`);
|
|
781
|
-
}
|
|
782
|
-
const collectionLines = [];
|
|
783
|
-
if (detail === 'verbose') {
|
|
784
|
-
collectionLines.push(`Generated ${safeTerminalValue(document.generatedAt, 'unknown')}`);
|
|
785
|
-
const timing = terminalRecord(document.timing);
|
|
786
|
-
if (Number(timing.totalMs) >= 0)
|
|
787
|
-
collectionLines.push(`Total time ${safeTerminalValue(timing.totalMs)} ms`);
|
|
788
|
-
const sources = Array.isArray(timing.sources) ? timing.sources.map(terminalRecord) : [];
|
|
789
|
-
for (const source of sources.slice(0, 16)) {
|
|
790
|
-
collectionLines.push(`${safeTerminalValue(source.source, 'source')} ${titleCase(source.outcome)} · ${safeTerminalValue(source.durationMs, '0')} ms`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const lines = [];
|
|
794
|
-
appendSection(lines, 'Target', targetLines);
|
|
795
|
-
appendSection(lines, 'Registration', registrationLines);
|
|
796
|
-
appendSection(lines, 'DNS', dnsLines);
|
|
797
|
-
appendSection(lines, 'Mail', mailLines);
|
|
798
|
-
appendSection(lines, 'Website and security', websiteLines);
|
|
799
|
-
appendSection(lines, 'TLS and certificate', tlsLines);
|
|
800
|
-
appendSection(lines, 'Network', networkLines);
|
|
801
|
-
appendSection(lines, 'Source health', sourceHealthLines);
|
|
802
|
-
appendSection(lines, 'Collection', collectionLines);
|
|
803
|
-
return `${lines.join('\n')}\n`;
|
|
804
|
-
}
|
|
805
|
-
function formatTerminalRegistrySupport(document) {
|
|
806
|
-
const profile = terminalRecord(document.profile);
|
|
807
|
-
const rdap = terminalRecord(profile.rdap);
|
|
808
|
-
const whois = terminalRecord(profile.whois);
|
|
809
|
-
const verification = terminalRecord(document.verification);
|
|
810
|
-
const fixtures = Array.isArray(verification.fixtureScenarios) ? verification.fixtureScenarios : [];
|
|
811
|
-
const files = Array.isArray(verification.files) ? verification.files : [];
|
|
812
|
-
const documentation = Array.isArray(verification.documentationUrls) ? verification.documentationUrls : [];
|
|
813
|
-
const standards = terminalRecord(document.standardsCoverage);
|
|
814
|
-
const genericCoverage = terminalRecord(standards.genericAndRestricted);
|
|
815
|
-
const supportLabel = (value) => titleCase(safeTerminalValue(value, 'unknown').replaceAll('-', '_'));
|
|
816
|
-
const lines = [
|
|
817
|
-
`Input ${safeTerminalValue(document.requestedInput)}`,
|
|
818
|
-
`Suffix .${safeTerminalValue(document.suffix)}`,
|
|
819
|
-
`Catalogue Version ${safeTerminalValue(document.catalogueVersion, '0')}`,
|
|
820
|
-
`gTLD RDAP ${safeTerminalValue(genericCoverage.rdapCovered, '0')} / ${safeTerminalValue(genericCoverage.total, '0')}`,
|
|
821
|
-
`Coverage date ${safeTerminalValue(standards.verifiedAt, 'unknown')}`,
|
|
822
|
-
`Profile ${profile.explicitSuffixProfile ? 'Explicit suffix profile' : 'Generic IANA discovery profile'}`,
|
|
823
|
-
`Profile ID ${safeTerminalValue(profile.id)}`,
|
|
824
|
-
`Registry class ${supportLabel(profile.registryClass)}`,
|
|
825
|
-
`Coverage ${supportLabel(profile.coverageState)}`,
|
|
826
|
-
`RDAP discovery ${registryAccessProfileLabel(rdap.discovery)}`,
|
|
827
|
-
`RDAP access ${registryAccessProfileLabel(rdap.accessProfile)}`,
|
|
828
|
-
`WHOIS discovery ${registryAccessProfileLabel(whois.discovery)}`,
|
|
829
|
-
`WHOIS access ${registryAccessProfileLabel(whois.accessProfile)}`,
|
|
830
|
-
`WHOIS query ${supportLabel(whois.queryProfile)}`,
|
|
831
|
-
`WHOIS scope ${supportLabel(whois.queryScope)}`,
|
|
832
|
-
`WHOIS encoding ${supportLabel(whois.encodingProfile)}`,
|
|
833
|
-
`WHOIS parser ${supportLabel(whois.parserProfile)}`,
|
|
834
|
-
...(typeof profile.officialLookupUrl === 'string' && profile.officialLookupUrl
|
|
835
|
-
? [`Official lookup ${safeTerminalValue(profile.officialLookupUrl)}`]
|
|
836
|
-
: []),
|
|
837
|
-
`Fixture states ${fixtures.length ? fixtures.map((value) => supportLabel(value)).join(', ') : 'None documented'}`,
|
|
838
|
-
];
|
|
839
|
-
for (const file of files)
|
|
840
|
-
lines.push(`Verified by ${safeTerminalValue(file)}`);
|
|
841
|
-
for (const url of documentation)
|
|
842
|
-
lines.push(`Documentation ${safeTerminalValue(url)}`);
|
|
843
|
-
lines.push(`Limitation ${safeTerminalValue(document.limitation)}`, '', safeTerminalValue(terminalRecord(document.interpretation).statement));
|
|
844
|
-
return `${lines.join('\n')}\n`;
|
|
845
|
-
}
|
|
846
|
-
function formatTerminalBulk(items, metadata) {
|
|
847
|
-
const lines = items.map((item) => {
|
|
848
|
-
if (!item.ok)
|
|
849
|
-
return `! ${safeTerminalValue(item.query)}: ${safeTerminalValue(item.error, 'Lookup failed')}`;
|
|
850
|
-
const result = terminalRecord(item.result);
|
|
851
|
-
const availability = terminalRecord(result.availability);
|
|
852
|
-
const state = titleCase(availability.state);
|
|
853
|
-
const confidence = titleCase(availability.confidence);
|
|
854
|
-
return `✓ ${safeTerminalValue(item.query)}: ${state} (${confidence} confidence)`;
|
|
855
|
-
});
|
|
856
|
-
const succeeded = items.filter((item) => item.ok).length;
|
|
857
|
-
lines.push('');
|
|
858
|
-
const collected = metadata.collectedTotal ?? items.length;
|
|
859
|
-
const filter = metadata.filter && metadata.filter !== 'all'
|
|
860
|
-
? ` · ${items.length} matched ${metadata.filter}`
|
|
861
|
-
: '';
|
|
862
|
-
lines.push(`${collected} collected${filter} · ${succeeded} succeeded · ${items.length - succeeded} failed in output · ${metadata.duplicates || 0} duplicates removed`);
|
|
863
|
-
return `${lines.join('\n')}\n`;
|
|
864
|
-
}
|
|
865
|
-
function formatTerminalCtSearch(document) {
|
|
866
|
-
const matches = Array.isArray(document.matches) ? document.matches : [];
|
|
867
|
-
const certificateGroups = Array.isArray(document.certificateGroups) ? document.certificateGroups : [];
|
|
868
|
-
const crossDomainGroups = certificateGroups.filter((value) => {
|
|
869
|
-
const group = terminalRecord(value);
|
|
870
|
-
return Array.isArray(group.domains) && group.domains.length > 1;
|
|
871
|
-
});
|
|
872
|
-
const visible = matches.slice(0, MAX_CT_TERMINAL_MATCHES);
|
|
873
|
-
const observation = terminalRecord(document.observation);
|
|
874
|
-
const lines = [
|
|
875
|
-
`Keyword ${safeTerminalValue(document.keyword)}`,
|
|
876
|
-
`CT status ${titleCase(observation.status || (document.truncated ? 'partial' : 'success'))}`,
|
|
877
|
-
`Certificates ${safeTerminalValue(document.certCount, '0')}`,
|
|
878
|
-
`Observed hosts ${safeTerminalValue(Array.isArray(document.domains) ? document.domains.length : 0, '0')}`,
|
|
879
|
-
`Matches ${safeTerminalValue(matches.length, '0')}`,
|
|
880
|
-
`Issuance groups ${safeTerminalValue(certificateGroups.length, '0')} (${safeTerminalValue(crossDomainGroups.length, '0')} cross-domain)`,
|
|
881
|
-
`Truncated ${document.truncated ? 'Yes' : 'No'}`,
|
|
882
|
-
`Group cap ${document.certificateGroupsTruncated ? 'Reached' : 'Not reached'}`,
|
|
883
|
-
'',
|
|
884
|
-
];
|
|
885
|
-
if (!visible.length) {
|
|
886
|
-
lines.push('No structured registrable-domain matches.');
|
|
887
|
-
}
|
|
888
|
-
else {
|
|
889
|
-
for (const value of visible) {
|
|
890
|
-
const match = terminalRecord(value);
|
|
891
|
-
const hostnames = Array.isArray(match.hostnames) ? match.hostnames : [];
|
|
892
|
-
const shownHosts = hostnames.slice(0, MAX_CT_TERMINAL_HOSTNAMES).map((value) => safeTerminalValue(value));
|
|
893
|
-
const omitted = hostnames.length - shownHosts.length;
|
|
894
|
-
lines.push(safeTerminalValue(match.domain));
|
|
895
|
-
lines.push(` Certificates ${safeTerminalValue(match.certificateCount, '0')}`);
|
|
896
|
-
lines.push(` Hostnames ${shownHosts.join(', ')}${omitted > 0 ? ` (+${omitted} more)` : ''}`);
|
|
897
|
-
lines.push(` Observed ${safeTerminalValue(match.firstObservedAt)} → ${safeTerminalValue(match.lastObservedAt)}`);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
if (matches.length > visible.length) {
|
|
901
|
-
lines.push('', `Showing ${visible.length} of ${matches.length} structured matches in terminal output; use --json for the complete bounded result.`);
|
|
902
|
-
}
|
|
903
|
-
return `${lines.join('\n')}\n`;
|
|
904
|
-
}
|
|
905
|
-
function formatTerminalDiscover(document, mutationLabels = {}) {
|
|
906
|
-
const candidates = Array.isArray(document.candidates) ? document.candidates : [];
|
|
907
|
-
const visible = candidates.slice(0, MAX_DISCOVER_TERMINAL_CANDIDATES);
|
|
908
|
-
const advanced = terminalRecord(document.advancedConfusable);
|
|
909
|
-
const snapshot = terminalRecord(document.snapshot);
|
|
910
|
-
const lines = [
|
|
911
|
-
`Seed ${safeTerminalValue(document.seed)}`,
|
|
912
|
-
`Preset ${safeTerminalValue(document.preset)}`,
|
|
913
|
-
`Keyboard ${safeTerminalValue(document.keyboardLayout)}`,
|
|
914
|
-
`TLDs ${(Array.isArray(document.tlds) ? document.tlds : []).map((value) => safeTerminalValue(value)).join(', ')}`,
|
|
915
|
-
`Families ${(Array.isArray(document.mutationFamilies) && document.mutationFamilies.length) ? document.mutationFamilies.map((value) => safeTerminalValue(value)).join(', ') : 'Preset defaults'}`,
|
|
916
|
-
`Dictionary ${safeTerminalValue(document.dictionaryTermCount, '0')} accepted custom terms${Number(document.rejectedDictionaryTermCount) > 0 ? `, ${safeTerminalValue(document.rejectedDictionaryTermCount)} rejected` : ''}`,
|
|
917
|
-
`Candidates ${safeTerminalValue(candidates.length, '0')}`,
|
|
918
|
-
`Truncated ${document.truncated ? 'Yes' : 'No'}`,
|
|
919
|
-
];
|
|
920
|
-
if (Object.keys(advanced).length) {
|
|
921
|
-
lines.push(`Advanced IDN ${safeTerminalValue(advanced.generated, '0')} generated, ${safeTerminalValue(advanced.omittedByPolicy, '0')} policy-omitted, ${safeTerminalValue(advanced.omittedByBudget, '0')} budget-omitted`);
|
|
922
|
-
}
|
|
923
|
-
if (Object.keys(snapshot).length) {
|
|
924
|
-
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`}`);
|
|
925
|
-
}
|
|
926
|
-
lines.push('');
|
|
927
|
-
for (const value of visible) {
|
|
928
|
-
const candidate = terminalRecord(value);
|
|
929
|
-
const labels = (Array.isArray(candidate.mutationTypes) ? candidate.mutationTypes : [])
|
|
930
|
-
.map((mutationType) => {
|
|
931
|
-
const value = safeTerminalValue(mutationType);
|
|
932
|
-
return safeTerminalValue(mutationLabels[value] || value);
|
|
933
|
-
});
|
|
934
|
-
const candidateDomain = safeTerminalValue(candidate.domain, '');
|
|
935
|
-
const unicodeDomain = unicodeDomainFromAscii(candidateDomain);
|
|
936
|
-
const unicodeDetail = unicodeDomain && unicodeDomain !== candidateDomain
|
|
937
|
-
? ` [Unicode: ${safeTerminalValue(unicodeDomain)}]`
|
|
938
|
-
: '';
|
|
939
|
-
lines.push(`${safeTerminalValue(candidateDomain)}${unicodeDetail}: ${labels.join(', ') || 'Generated variant'}`);
|
|
940
|
-
}
|
|
941
|
-
if (!visible.length)
|
|
942
|
-
lines.push('No candidates were generated.');
|
|
943
|
-
if (candidates.length > visible.length) {
|
|
944
|
-
lines.push('', `Showing ${visible.length} of ${candidates.length} candidates in terminal output; use --json or --jsonl for the complete bounded result.`);
|
|
945
|
-
}
|
|
946
|
-
return `${lines.join('\n')}\n`;
|
|
947
|
-
}
|
|
948
|
-
function formatTerminalPosture(document) {
|
|
949
|
-
const summary = terminalRecord(document.summary);
|
|
950
|
-
const spfExpansion = terminalRecord(document.spfExpansion);
|
|
951
|
-
const selectors = Array.isArray(document.dkimSelectors) ? document.dkimSelectors : [];
|
|
952
|
-
const checks = Array.isArray(document.checks) ? document.checks : [];
|
|
953
|
-
const dependencies = Array.isArray(document.externalDependencies) ? document.externalDependencies : [];
|
|
954
|
-
const dmarcAuthorizations = Array.isArray(document.dmarcAuthorizations) ? document.dmarcAuthorizations : [];
|
|
955
|
-
const lines = [
|
|
956
|
-
`Domain ${safeTerminalValue(document.domain)}`,
|
|
957
|
-
`Checked ${safeTerminalValue(document.checkedAt)}`,
|
|
958
|
-
`DKIM selectors ${selectors.length ? selectors.map((value) => safeTerminalValue(value)).join(', ') : 'None supplied'}`,
|
|
959
|
-
`Summary ${safeTerminalValue(summary.danger, '0')} action · ${safeTerminalValue(summary.warning, '0')} review · ${safeTerminalValue(summary.pass, '0')} pass · ${safeTerminalValue(summary.info, '0')} info`,
|
|
960
|
-
...(Object.keys(spfExpansion).length ? [
|
|
961
|
-
`SPF expansion ${safeTerminalValue(spfExpansion.state)} · ${safeTerminalValue(spfExpansion.lookupsUsed, '0')}/${safeTerminalValue(spfExpansion.lookupLimit, '0')} policy queries · ${safeTerminalValue(spfExpansion.dnsLookupTerms, '0')} DNS terms`,
|
|
962
|
-
] : []),
|
|
963
|
-
`Dependencies ${dependencies.length} observed · ${dmarcAuthorizations.length} DMARC reporting destination${dmarcAuthorizations.length === 1 ? '' : 's'} checked`,
|
|
964
|
-
'',
|
|
965
|
-
];
|
|
966
|
-
for (const value of checks) {
|
|
967
|
-
const item = terminalRecord(value);
|
|
968
|
-
lines.push(`[${safeTerminalValue(item.status, 'info').toUpperCase()}] ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.summary)}`);
|
|
969
|
-
if (item.detail)
|
|
970
|
-
lines.push(` Detail ${safeTerminalValue(item.detail)}`);
|
|
971
|
-
if (item.remediation)
|
|
972
|
-
lines.push(` Next ${safeTerminalValue(item.remediation)}`);
|
|
973
|
-
const records = Array.isArray(item.records) ? item.records : [];
|
|
974
|
-
for (const record of records.slice(0, MAX_POSTURE_TERMINAL_RECORDS)) {
|
|
975
|
-
lines.push(` Record ${safeTerminalValue(record)}`);
|
|
976
|
-
}
|
|
977
|
-
if (records.length > MAX_POSTURE_TERMINAL_RECORDS) {
|
|
978
|
-
lines.push(` Records ${records.length - MAX_POSTURE_TERMINAL_RECORDS} more omitted from terminal output; use --json for the complete bounded report.`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
if (!checks.length)
|
|
982
|
-
lines.push('No posture checks were returned.');
|
|
983
|
-
return `${lines.join('\n')}\n`;
|
|
984
|
-
}
|
|
985
|
-
function formatTerminalHttp(document) {
|
|
986
|
-
const http = terminalRecord(document.http);
|
|
987
|
-
const response = terminalRecord(http.response);
|
|
988
|
-
const attempts = Array.isArray(http.attempts) ? http.attempts : [];
|
|
989
|
-
const limitations = Array.isArray(http.limitations) ? http.limitations : [];
|
|
990
|
-
const securityHeadersRecord = terminalRecord(response.securityHeaders);
|
|
991
|
-
const securityHeaders = Object.keys(securityHeadersRecord).length
|
|
992
|
-
? Object.entries(securityHeadersRecord).filter(([, value]) => Boolean(value)).map(([name]) => name)
|
|
993
|
-
: [];
|
|
994
|
-
const lines = [
|
|
995
|
-
`Domain ${safeTerminalValue(document.domain)}`,
|
|
996
|
-
`Probe ${titleCase(document.probeStatus)}`,
|
|
997
|
-
`Assessment ${titleCase(document.assessment ?? document.activityStatus)}`,
|
|
998
|
-
`Evidence ${titleCase(http.status)}`,
|
|
999
|
-
`Final URL ${safeTerminalValue(http.finalUrl)}`,
|
|
1000
|
-
`HTTP status ${safeTerminalValue(response.status)}`,
|
|
1001
|
-
`Transport ${safeTerminalValue(http.transportSecurity)}`,
|
|
1002
|
-
`Redirects ${safeTerminalValue(http.redirectCount, '0')}`,
|
|
1003
|
-
`Content type ${safeTerminalValue(response.contentType)}`,
|
|
1004
|
-
`Body inspected ${response.bodyInspected === true ? 'Yes' : response.bodyInspected === false ? 'No' : '—'}`,
|
|
1005
|
-
`Security ${securityHeaders.length ? securityHeaders.join(', ') : 'No selected headers observed'}`,
|
|
1006
|
-
];
|
|
1007
|
-
if (document.detail)
|
|
1008
|
-
lines.push(`Detail ${safeTerminalValue(document.detail)}`);
|
|
1009
|
-
appendDeliveryMetadataLines(lines, response.deliveryMetadata, true);
|
|
1010
|
-
const bodyHash = terminalRecord(response.bodyHash);
|
|
1011
|
-
if (bodyHash.value) {
|
|
1012
|
-
lines.push(`Body hash ${safeTerminalValue(`${bodyHash.algorithm}:${bodyHash.value} (${bodyHash.scope})`)}`);
|
|
1013
|
-
}
|
|
1014
|
-
for (const value of attempts) {
|
|
1015
|
-
const attempt = terminalRecord(value);
|
|
1016
|
-
const outcome = attempt.httpStatus ? `HTTP ${attempt.httpStatus}` : attempt.error || attempt.outcome;
|
|
1017
|
-
lines.push(`Attempt ${safeTerminalValue(attempt.url)}: ${safeTerminalValue(outcome)}`);
|
|
1018
|
-
}
|
|
1019
|
-
for (const limitation of limitations)
|
|
1020
|
-
lines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
1021
|
-
return `${lines.join('\n')}\n`;
|
|
1022
|
-
}
|
|
1023
|
-
function formatTerminalTls(document) {
|
|
1024
|
-
const certificate = terminalRecord(document.certificate);
|
|
1025
|
-
const subject = terminalRecord(certificate.subject);
|
|
1026
|
-
const issuer = terminalRecord(certificate.issuer);
|
|
1027
|
-
const altNames = terminalRecord(certificate.subjectAltNames);
|
|
1028
|
-
const dnsNames = Array.isArray(altNames.dnsNames) ? altNames.dnsNames : [];
|
|
1029
|
-
const ipAddresses = Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses : [];
|
|
1030
|
-
const visibleAltNames = [...dnsNames, ...ipAddresses].slice(0, MAX_TLS_TERMINAL_ALT_NAMES);
|
|
1031
|
-
const omittedAltNames = dnsNames.length + ipAddresses.length - visibleAltNames.length;
|
|
1032
|
-
const cipher = terminalRecord(document.cipher);
|
|
1033
|
-
const publicKey = terminalRecord(certificate.publicKey);
|
|
1034
|
-
const signature = terminalRecord(certificate.signature);
|
|
1035
|
-
const purposes = terminalRecord(certificate.extendedKeyUsage);
|
|
1036
|
-
const purposeValues = Array.isArray(purposes.values)
|
|
1037
|
-
? purposes.values.slice(0, MAX_TLS_TERMINAL_PURPOSES).map(terminalRecord)
|
|
1038
|
-
: [];
|
|
1039
|
-
const sanClasses = terminalCountSummary(altNames.classes, [
|
|
1040
|
-
['dns', 'DNS'],
|
|
1041
|
-
['ip', 'IP'],
|
|
1042
|
-
['email', 'email'],
|
|
1043
|
-
['uri', 'URI'],
|
|
1044
|
-
['directoryName', 'directory name'],
|
|
1045
|
-
['registeredId', 'registered ID'],
|
|
1046
|
-
['otherName', 'other name'],
|
|
1047
|
-
['unclassified', 'other'],
|
|
1048
|
-
]);
|
|
1049
|
-
const aia = terminalRecord(certificate.authorityInformationAccess);
|
|
1050
|
-
const ocsp = terminalRecord(aia.ocsp);
|
|
1051
|
-
const caIssuers = terminalRecord(aia.caIssuers);
|
|
1052
|
-
const authorization = terminalRecord(document.authorization);
|
|
1053
|
-
const hostname = terminalRecord(document.hostname);
|
|
1054
|
-
const validity = terminalRecord(document.validity);
|
|
1055
|
-
const diagnostics = terminalRecord(document.diagnostics);
|
|
1056
|
-
const findings = Array.isArray(document.findings) ? document.findings : [];
|
|
1057
|
-
const limitations = Array.isArray(document.limitations) ? document.limitations : [];
|
|
1058
|
-
const lines = [
|
|
1059
|
-
`Hostname ${safeTerminalValue(document.sniHost)}`,
|
|
1060
|
-
`Evidence ${titleCase(document.status)}`,
|
|
1061
|
-
`Observed ${safeTerminalValue(document.observedAt)}`,
|
|
1062
|
-
`Address ${safeTerminalValue(document.connectedAddress)}`,
|
|
1063
|
-
`Protocol ${safeTerminalValue(document.protocol)}`,
|
|
1064
|
-
`ALPN ${safeTerminalValue(document.alpnProtocol)}`,
|
|
1065
|
-
`Cipher ${safeTerminalValue(cipher.standardName || cipher.name)}`,
|
|
1066
|
-
`Authorized ${authorization.authorized === true ? 'Yes' : authorization.authorized === false ? 'No' : 'Unknown'}`,
|
|
1067
|
-
`Hostname match ${hostname.matches === true ? 'Yes' : hostname.matches === false ? 'No' : 'Unknown'}`,
|
|
1068
|
-
`Validity ${titleCase(validity.status)}`,
|
|
1069
|
-
`Subject ${safeTerminalValue(Array.isArray(subject.commonNames) ? subject.commonNames.join(', ') : null)}`,
|
|
1070
|
-
`Issuer ${safeTerminalValue(Array.isArray(issuer.commonNames) ? issuer.commonNames.join(', ') : null)}`,
|
|
1071
|
-
`Valid from ${safeTerminalValue(certificate.validFrom)}`,
|
|
1072
|
-
`Valid to ${safeTerminalValue(certificate.validTo)}`,
|
|
1073
|
-
`Fingerprint ${safeTerminalValue(certificate.fingerprintSha256)}`,
|
|
1074
|
-
`Public key ${safeTerminalValue([publicKey.type, publicKey.bits ? `${publicKey.bits} bits` : null, publicKey.curve].filter(Boolean).join(' '))}`,
|
|
1075
|
-
...(signature.algorithm || signature.oid
|
|
1076
|
-
? [`Signature ${safeTerminalValue([signature.algorithm, signature.oid ? `(${signature.oid})` : null].filter(Boolean).join(' '))}`]
|
|
1077
|
-
: []),
|
|
1078
|
-
...(Object.keys(purposes).length
|
|
1079
|
-
? [`Purposes ${purposeValues.length
|
|
1080
|
-
? purposeValues.map((purpose) => safeTerminalValue(`${purpose.name || 'Unrecognized purpose'} (${purpose.oid || 'unknown OID'})`)).join(', ')
|
|
1081
|
-
: 'None declared'}${Array.isArray(purposes.values) && purposes.values.length > purposeValues.length ? ` (+${purposes.values.length - purposeValues.length} more)` : ''}${purposes.truncated ? ' (source truncated)' : ''}`]
|
|
1082
|
-
: []),
|
|
1083
|
-
`Alt names ${visibleAltNames.length ? visibleAltNames.map((value) => safeTerminalValue(value)).join(', ') : '—'}${omittedAltNames > 0 ? ` (+${omittedAltNames} more)` : ''}`,
|
|
1084
|
-
...(Object.keys(terminalRecord(altNames.classes)).length
|
|
1085
|
-
? [`SAN classes ${sanClasses || 'None observed'}${altNames.truncated ? ' (truncated)' : ''}`]
|
|
1086
|
-
: []),
|
|
1087
|
-
...(Object.keys(aia).length
|
|
1088
|
-
? [`AIA presence ${[
|
|
1089
|
-
terminalCount(ocsp.total) > 0
|
|
1090
|
-
? `OCSP ${terminalCount(ocsp.total)} (${terminalCount(ocsp.https)} HTTPS, ${terminalCount(ocsp.http)} HTTP, ${terminalCount(ocsp.other)} other)`
|
|
1091
|
-
: null,
|
|
1092
|
-
terminalCount(caIssuers.total) > 0
|
|
1093
|
-
? `CA issuers ${terminalCount(caIssuers.total)} (${terminalCount(caIssuers.https)} HTTPS, ${terminalCount(caIssuers.http)} HTTP, ${terminalCount(caIssuers.other)} other)`
|
|
1094
|
-
: null,
|
|
1095
|
-
terminalCount(aia.unknownMethods) > 0 ? `unknown methods ${terminalCount(aia.unknownMethods)}` : null,
|
|
1096
|
-
].filter(Boolean).join(' · ') || 'None declared'}${aia.truncated ? ' (truncated)' : ''}`]
|
|
1097
|
-
: []),
|
|
1098
|
-
`Chain ${safeTerminalValue(Array.isArray(document.chain) ? document.chain.length : 0, '0')} certificate${Array.isArray(document.chain) && document.chain.length === 1 ? '' : 's'}${document.chainTruncated ? ' (truncated)' : ''}`,
|
|
1099
|
-
];
|
|
1100
|
-
if (authorization.error)
|
|
1101
|
-
lines.push(`Trust detail ${safeTerminalValue(authorization.error)}`);
|
|
1102
|
-
if (hostname.error)
|
|
1103
|
-
lines.push(`Name detail ${safeTerminalValue(hostname.error)}`);
|
|
1104
|
-
if (diagnostics.error)
|
|
1105
|
-
lines.push(`Error ${safeTerminalValue(diagnostics.error)}`);
|
|
1106
|
-
for (const value of findings) {
|
|
1107
|
-
const finding = terminalRecord(value);
|
|
1108
|
-
lines.push(`Finding ${safeTerminalValue(finding.label)}: ${safeTerminalValue(finding.detail)}`);
|
|
1109
|
-
}
|
|
1110
|
-
for (const limitation of limitations)
|
|
1111
|
-
lines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
1112
|
-
return `${lines.join('\n')}\n`;
|
|
1113
|
-
}
|
|
1114
|
-
function comparisonStatusLabel(status) {
|
|
1115
|
-
const labels = {
|
|
1116
|
-
equivalent: 'Equivalent',
|
|
1117
|
-
conflict: 'Conflict',
|
|
1118
|
-
rdap_only: 'RDAP only',
|
|
1119
|
-
whois_only: 'WHOIS only',
|
|
1120
|
-
rdap_redacted: 'RDAP redacted',
|
|
1121
|
-
whois_redacted: 'WHOIS redacted',
|
|
1122
|
-
rdap_unavailable: 'RDAP unavailable',
|
|
1123
|
-
whois_unavailable: 'WHOIS unavailable',
|
|
1124
|
-
rdap_incomplete: 'RDAP incomplete',
|
|
1125
|
-
whois_incomplete: 'WHOIS incomplete',
|
|
1126
|
-
registry_only: 'Registry only',
|
|
1127
|
-
registrar_only: 'Registrar only',
|
|
1128
|
-
registry_redacted: 'Registry redacted',
|
|
1129
|
-
registrar_redacted: 'Registrar redacted',
|
|
1130
|
-
registry_unavailable: 'Registry unavailable',
|
|
1131
|
-
registrar_unavailable: 'Registrar unavailable',
|
|
1132
|
-
registry_incomplete: 'Registry incomplete',
|
|
1133
|
-
registrar_incomplete: 'Registrar incomplete',
|
|
1134
|
-
};
|
|
1135
|
-
return labels[String(status)] || titleCase(status);
|
|
1136
|
-
}
|
|
1137
|
-
function formatTerminalCompare(document) {
|
|
1138
|
-
const fields = Array.isArray(document.fields) ? document.fields : [];
|
|
1139
|
-
const counts = terminalRecord(document.counts);
|
|
1140
|
-
const sourceHealth = terminalRecord(document.sourceHealth);
|
|
1141
|
-
const rdapHealth = terminalRecord(sourceHealth.rdap);
|
|
1142
|
-
const whoisHealth = terminalRecord(sourceHealth.whois);
|
|
1143
|
-
const registryAccess = terminalRecord(document.registryAccess);
|
|
1144
|
-
const differenceCount = fields.length - (Number(counts.equivalent) || 0);
|
|
1145
|
-
const lines = [
|
|
1146
|
-
`Query ${safeTerminalValue(document.query || document.registrableDomain)}`,
|
|
1147
|
-
`Lookup mode ${titleCase(document.lookupMode)}`,
|
|
1148
|
-
`Lookup saved ${safeTerminalValue(document.lookupGeneratedAt)}`,
|
|
1149
|
-
`RDAP source ${comparisonStatusLabel(rdapHealth.status)}`,
|
|
1150
|
-
`WHOIS source ${comparisonStatusLabel(whoisHealth.status)}`,
|
|
1151
|
-
`Compared ${safeTerminalValue(fields.length, '0')} field${fields.length === 1 ? '' : 's'}`,
|
|
1152
|
-
`Equivalent ${safeTerminalValue(counts.equivalent, '0')}`,
|
|
1153
|
-
`Differences ${safeTerminalValue(differenceCount, '0')}`,
|
|
1154
|
-
];
|
|
1155
|
-
if (Object.keys(registryAccess).length) {
|
|
1156
|
-
lines.push(`Registry access .${safeTerminalValue(registryAccess.suffix)}`, `WHOIS access ${registryAccessProfileLabel(registryAccess.whoisAccessProfile)}`, `RDAP access ${registryAccessProfileLabel(registryAccess.rdapAccessProfile)}`, `Access note ${safeTerminalValue(registryAccess.limitation)}`);
|
|
1157
|
-
}
|
|
1158
|
-
lines.push('');
|
|
1159
|
-
if (!fields.length) {
|
|
1160
|
-
lines.push('Neither source published a comparable normalised field.');
|
|
1161
|
-
}
|
|
1162
|
-
else {
|
|
1163
|
-
for (const value of fields) {
|
|
1164
|
-
const field = terminalRecord(value);
|
|
1165
|
-
lines.push(`[${comparisonStatusLabel(field.status).toUpperCase()}] ${safeTerminalValue(field.label)}`);
|
|
1166
|
-
lines.push(` RDAP ${safeTerminalValue(field.rdapDisplay)}`);
|
|
1167
|
-
lines.push(` WHOIS ${safeTerminalValue(field.whoisDisplay)}`);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
const registrarComparison = terminalRecord(document.registrarPublicationComparison);
|
|
1171
|
-
if (Object.keys(registrarComparison).length) {
|
|
1172
|
-
const publicationFields = Array.isArray(registrarComparison.fields) ? registrarComparison.fields : [];
|
|
1173
|
-
const publicationCounts = terminalRecord(registrarComparison.counts);
|
|
1174
|
-
const publicationHealth = terminalRecord(registrarComparison.sourceHealth);
|
|
1175
|
-
const registryHealth = terminalRecord(publicationHealth.registry);
|
|
1176
|
-
const registrarHealth = terminalRecord(publicationHealth.registrar);
|
|
1177
|
-
const publicationDifferences = publicationFields.length - (Number(publicationCounts.equivalent) || 0);
|
|
1178
|
-
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')}`, '');
|
|
1179
|
-
if (!publicationFields.length) {
|
|
1180
|
-
lines.push('Neither RDAP publication exposed a comparable normalised field.');
|
|
1181
|
-
}
|
|
1182
|
-
else {
|
|
1183
|
-
for (const value of publicationFields) {
|
|
1184
|
-
const field = terminalRecord(value);
|
|
1185
|
-
lines.push(`[${comparisonStatusLabel(field.status).toUpperCase()}] ${safeTerminalValue(field.label)}`);
|
|
1186
|
-
lines.push(` Registry ${safeTerminalValue(field.registryDisplay)}`);
|
|
1187
|
-
lines.push(` Registrar ${safeTerminalValue(field.registrarDisplay)}`);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
lines.push('', 'Comparison is source reconciliation, not an availability or ownership decision.');
|
|
1192
|
-
if (Object.keys(registryAccess).length) {
|
|
1193
|
-
lines.push('Registry access describes collection reachability only; it does not decide registration, availability, ownership, safety, or maliciousness.');
|
|
1194
|
-
}
|
|
1195
|
-
return `${lines.join('\n')}\n`;
|
|
1196
|
-
}
|
|
1197
|
-
function formatTerminalRiskCalibration(document) {
|
|
1198
|
-
const summary = terminalRecord(document.summary);
|
|
1199
|
-
const bands = terminalRecord(summary.scoreBands);
|
|
1200
|
-
const thresholds = Array.isArray(document.thresholds) ? document.thresholds : [];
|
|
1201
|
-
const records = Array.isArray(document.records) ? document.records : [];
|
|
1202
|
-
const comparison = terminalRecord(document.modelComparison);
|
|
1203
|
-
const visible = records.slice(0, MAX_RISK_CALIBRATION_TERMINAL_RECORDS);
|
|
1204
|
-
const lines = [
|
|
1205
|
-
`Risk model v${safeTerminalValue(document.riskModelVersion)}`,
|
|
1206
|
-
`Review band ${safeTerminalValue(document.currentReviewThreshold)}+`,
|
|
1207
|
-
`Records ${safeTerminalValue(summary.total, '0')}`,
|
|
1208
|
-
`Metric labels ${safeTerminalValue((Number(summary.positive) || 0) + (Number(summary.negative) || 0), '0')}`,
|
|
1209
|
-
`Excluded ${safeTerminalValue(summary.excluded, '0')}`,
|
|
1210
|
-
`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')}`,
|
|
1211
|
-
'',
|
|
1212
|
-
'Threshold replay',
|
|
1213
|
-
];
|
|
1214
|
-
for (const value of thresholds) {
|
|
1215
|
-
const threshold = terminalRecord(value);
|
|
1216
|
-
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)}`);
|
|
1217
|
-
}
|
|
1218
|
-
if (comparison.available === true) {
|
|
1219
|
-
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`);
|
|
1220
|
-
}
|
|
1221
|
-
lines.push('', 'Records');
|
|
1222
|
-
for (const value of visible) {
|
|
1223
|
-
const record = terminalRecord(value);
|
|
1224
|
-
const score = record.score === null ? 'not scored' : String(record.score);
|
|
1225
|
-
lines.push(`${safeTerminalValue(record.id)} ${safeTerminalValue(record.domain)} ${titleCase(record.analystDisposition)} ${score}`);
|
|
1226
|
-
}
|
|
1227
|
-
if (records.length > visible.length)
|
|
1228
|
-
lines.push(`… ${records.length - visible.length} additional records omitted from terminal output; use --json for the complete bounded report.`);
|
|
1229
|
-
lines.push('', safeTerminalValue(terminalRecord(document.interpretation).statement));
|
|
1230
|
-
return `${lines.join('\n')}\n`;
|
|
1231
|
-
}
|
|
1232
|
-
export { MAX_CT_TERMINAL_HOSTNAMES, MAX_CT_TERMINAL_MATCHES, MAX_DISCOVER_TERMINAL_CANDIDATES, MAX_LOOKUP_TERMINAL_ALPN_IDS, MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH, MAX_LOOKUP_TERMINAL_LIMITATIONS, MAX_LOOKUP_TERMINAL_NAMES, MAX_LOOKUP_TERMINAL_RECORDS, MAX_POSTURE_TERMINAL_RECORDS, MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, MAX_RISK_CALIBRATION_TERMINAL_RECORDS, MAX_TERMINAL_VALUE_LENGTH, formatTerminalBulk, formatTerminalCompare, formatTerminalCtSearch, formatTerminalDiscover, formatTerminalHttp, formatTerminalLookup, formatTerminalPosture, formatTerminalRegistrySupport, formatTerminalRiskCalibration, formatTerminalTls, safeTerminalValue, };
|
|
1
|
+
// Stable terminal-formatting facade. Command and evidence families retain
|
|
2
|
+
// focused framework-neutral owners while callers keep one compatibility import.
|
|
3
|
+
export { MAX_LOOKUP_TERMINAL_ALPN_IDS, MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH, MAX_LOOKUP_TERMINAL_LIMITATIONS, MAX_LOOKUP_TERMINAL_NAMES, MAX_LOOKUP_TERMINAL_RECORDS, formatTerminalLookup, } from "./terminal-lookup.mjs";
|
|
4
|
+
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, } from "./terminal-command-formats.mjs";
|
|
5
|
+
export { MAX_TERMINAL_VALUE_LENGTH, MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, safeTerminalValue, } from "./terminal-shared.mjs";
|