@slicedearth/whoisleuth-cli 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +17 -10
- package/docs/cli.md +5 -3
- 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/project-metadata.mjs +1 -1
- 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 +3 -3
- 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 +227 -63
- 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/brand-profile-model.mjs +1 -1
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { registryAccessProfileLabel } from "../registry-access.mjs";
|
|
2
|
+
import { technologyEvidenceRoles, } from "../../lib/technology-evidence-role.mjs";
|
|
3
|
+
import { appendDeliveryMetadataLines, appendPublicationMetadataLines } from "./terminal-metadata.mjs";
|
|
4
|
+
import { appendSection, boundedTerminalComponent, boundedTerminalList, boundedTerminalWithSuffix, MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, safeTerminalValue, terminalCount, terminalCountSummary, terminalDisplayCount, terminalRecord, titleCase, } from "./terminal-shared.mjs";
|
|
5
|
+
const MAX_LOOKUP_TERMINAL_RECORDS = 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
|
+
function terminalTechnologyRoleNames(findings, role) {
|
|
14
|
+
const names = findings
|
|
15
|
+
.filter((finding) => technologyEvidenceRoles(finding).includes(role))
|
|
16
|
+
.slice(0, 6)
|
|
17
|
+
.map((finding) => safeTerminalValue(terminalRecord(finding).name, 'Unnamed indicator'));
|
|
18
|
+
return names.length ? boundedTerminalList(names, Math.max(0, findings.filter((finding) => technologyEvidenceRoles(finding).includes(role)).length - names.length)) : 'None retained';
|
|
19
|
+
}
|
|
20
|
+
function formatLookupDnsRecord(value) {
|
|
21
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
22
|
+
return safeTerminalValue(value);
|
|
23
|
+
const record = terminalRecord(value);
|
|
24
|
+
if (!Object.keys(record).length)
|
|
25
|
+
return null;
|
|
26
|
+
if ('exchange' in record) {
|
|
27
|
+
return safeTerminalValue(`${safeTerminalValue(record.priority, '0')} ${record.exchange ? safeTerminalValue(record.exchange) : '.'}`);
|
|
28
|
+
}
|
|
29
|
+
if ('tag' in record && 'value' in record) {
|
|
30
|
+
return safeTerminalValue(`${record.critical ? 'critical ' : ''}${safeTerminalValue(record.tag)} ${safeTerminalValue(record.value)}`);
|
|
31
|
+
}
|
|
32
|
+
if ('nsname' in record) {
|
|
33
|
+
return safeTerminalValue(`${safeTerminalValue(record.nsname)} · serial ${safeTerminalValue(record.serial, 'unknown')}`);
|
|
34
|
+
}
|
|
35
|
+
if (record.type === 'HTTPS') {
|
|
36
|
+
const parameters = terminalRecord(record.parameters);
|
|
37
|
+
const alpnValues = Array.isArray(parameters.alpn) ? parameters.alpn : [];
|
|
38
|
+
const alpnVisible = alpnValues
|
|
39
|
+
.slice(0, MAX_LOOKUP_TERMINAL_ALPN_IDS)
|
|
40
|
+
.map((item) => boundedTerminalComponent(item, MAX_LOOKUP_TERMINAL_ALPN_ID_LENGTH));
|
|
41
|
+
const alpn = alpnVisible.length
|
|
42
|
+
? boundedTerminalList(alpnVisible, Math.max(0, alpnValues.length - alpnVisible.length))
|
|
43
|
+
: '';
|
|
44
|
+
const suffix = [parameters.port ? `port ${safeTerminalValue(parameters.port)}` : '', alpn ? `ALPN ${alpn}` : ''].filter(Boolean).join(' · ');
|
|
45
|
+
const target = `${safeTerminalValue(record.priority, '0')} ${safeTerminalValue(record.target, '.')}`;
|
|
46
|
+
return boundedTerminalWithSuffix(target, suffix ? ` · ${suffix}` : '');
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function appendLookupRecordLine(lines, label, value) {
|
|
51
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const visible = value
|
|
54
|
+
.slice(0, MAX_LOOKUP_TERMINAL_RECORDS)
|
|
55
|
+
.map(formatLookupDnsRecord)
|
|
56
|
+
.filter((item) => item !== null);
|
|
57
|
+
if (!visible.length)
|
|
58
|
+
return;
|
|
59
|
+
const omitted = Math.max(0, value.length - visible.length);
|
|
60
|
+
lines.push(`${label.padEnd(14)}${boundedTerminalList(visible, omitted)}`);
|
|
61
|
+
}
|
|
62
|
+
function lookupObservationLabel(value, observed, notObserved) {
|
|
63
|
+
return value === true ? observed : value === false ? notObserved : 'Unavailable';
|
|
64
|
+
}
|
|
65
|
+
function boundedTerminalNames(value) {
|
|
66
|
+
if (typeof value === 'string')
|
|
67
|
+
return safeTerminalValue(value);
|
|
68
|
+
if (!Array.isArray(value))
|
|
69
|
+
return null;
|
|
70
|
+
const names = value
|
|
71
|
+
.filter((item) => typeof item === 'string' && Boolean(item.trim()))
|
|
72
|
+
.slice(0, MAX_LOOKUP_TERMINAL_NAMES)
|
|
73
|
+
.map((item) => safeTerminalValue(item));
|
|
74
|
+
if (!names.length)
|
|
75
|
+
return null;
|
|
76
|
+
return boundedTerminalList(names, Math.max(0, value.length - names.length));
|
|
77
|
+
}
|
|
78
|
+
function boundedTerminalStrings(value, maximum = MAX_LOOKUP_TERMINAL_NAMES) {
|
|
79
|
+
if (!Array.isArray(value))
|
|
80
|
+
return null;
|
|
81
|
+
const inspected = Math.min(value.length, maximum);
|
|
82
|
+
const retained = [];
|
|
83
|
+
let invalid = 0;
|
|
84
|
+
for (let index = 0; index < inspected; index += 1) {
|
|
85
|
+
const item = value[index];
|
|
86
|
+
if (typeof item === 'string' || typeof item === 'number')
|
|
87
|
+
retained.push(safeTerminalValue(item));
|
|
88
|
+
else
|
|
89
|
+
invalid += 1;
|
|
90
|
+
}
|
|
91
|
+
const uninspected = Math.max(0, value.length - inspected);
|
|
92
|
+
const disclosures = [
|
|
93
|
+
invalid > 0 ? `${invalid} malformed retained entr${invalid === 1 ? 'y' : 'ies'} omitted` : null,
|
|
94
|
+
uninspected > 0 ? `+${uninspected} more retained entr${uninspected === 1 ? 'y' : 'ies'}` : null,
|
|
95
|
+
].filter(Boolean).join(' · ');
|
|
96
|
+
if (!retained.length)
|
|
97
|
+
return disclosures || null;
|
|
98
|
+
return boundedTerminalWithSuffix(retained.join(', '), disclosures ? ` · ${disclosures}` : '');
|
|
99
|
+
}
|
|
100
|
+
function sourceCompleteness(value) {
|
|
101
|
+
if (value.complete === true && value.truncated !== true)
|
|
102
|
+
return 'Complete';
|
|
103
|
+
if (value.complete === false || value.truncated === true)
|
|
104
|
+
return value.truncated === true ? 'Incomplete · truncated' : 'Incomplete';
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function positiveSourceStatus(value) {
|
|
108
|
+
return value === 'success' || value === 'partial';
|
|
109
|
+
}
|
|
110
|
+
function registrarComplianceSummary(compliance, actionCount) {
|
|
111
|
+
const health = titleCase(compliance.sourceHealth);
|
|
112
|
+
const year = safeTerminalValue(compliance.catalogueYear || 'current-year');
|
|
113
|
+
if (compliance.state === 'matching_actions') {
|
|
114
|
+
return `${actionCount} matching ${year} action${actionCount === 1 ? '' : 's'} · source ${health}`;
|
|
115
|
+
}
|
|
116
|
+
if (compliance.state === 'reviewed_no_match')
|
|
117
|
+
return `No matching ${year} action · source ${health}`;
|
|
118
|
+
if (compliance.state === 'not_applicable')
|
|
119
|
+
return `Not assessed without one registrar IANA ID · source ${health}`;
|
|
120
|
+
if (compliance.state === 'stale')
|
|
121
|
+
return `No matching action retained · source ${health}`;
|
|
122
|
+
return `${titleCase(compliance.state)} · source ${health}`;
|
|
123
|
+
}
|
|
124
|
+
function lifecycleDate(value, field) {
|
|
125
|
+
const lifecycle = terminalRecord(value.lifecycle);
|
|
126
|
+
return lifecycle[`${field}Iso`] ?? lifecycle[field];
|
|
127
|
+
}
|
|
128
|
+
function addressSelectionLabel(value) {
|
|
129
|
+
const labels = {
|
|
130
|
+
tls_connection: 'TLS connection',
|
|
131
|
+
dns_a: 'DNS A',
|
|
132
|
+
dns_aaaa: 'DNS AAAA',
|
|
133
|
+
};
|
|
134
|
+
return labels[String(value)] || titleCase(value);
|
|
135
|
+
}
|
|
136
|
+
function certificateWarningVerdict(value) {
|
|
137
|
+
if (value.verdict === 'listed')
|
|
138
|
+
return 'Listed certificate review lead';
|
|
139
|
+
if (value.verdict === 'not_listed' && value.status === 'success' && value.complete === true) {
|
|
140
|
+
return 'No match in retained snapshot';
|
|
141
|
+
}
|
|
142
|
+
return 'Inconclusive';
|
|
143
|
+
}
|
|
144
|
+
function retainedUrlRelationship(value, target) {
|
|
145
|
+
const urlValue = terminalRecord(value);
|
|
146
|
+
if (typeof urlValue.url !== 'string')
|
|
147
|
+
return null;
|
|
148
|
+
try {
|
|
149
|
+
const declared = new URL(urlValue.url);
|
|
150
|
+
const targetValue = String(target || '').trim().replace(/\.$/u, '');
|
|
151
|
+
const targetHost = new URL(`http://${targetValue}`).hostname.toLowerCase();
|
|
152
|
+
const relationship = declared.hostname.toLowerCase() === targetHost
|
|
153
|
+
? 'Same target host'
|
|
154
|
+
: 'Different host declared';
|
|
155
|
+
return [
|
|
156
|
+
relationship,
|
|
157
|
+
urlValue.queryOmitted === true ? 'query omitted' : null,
|
|
158
|
+
urlValue.pathTruncated === true ? 'path shortened' : null,
|
|
159
|
+
].filter(Boolean).join(' · ');
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function boundedFindingLabels(value, formatter) {
|
|
166
|
+
if (!Array.isArray(value))
|
|
167
|
+
return null;
|
|
168
|
+
const inspected = Math.min(value.length, MAX_LOOKUP_TERMINAL_FINDINGS);
|
|
169
|
+
const retained = value
|
|
170
|
+
.slice(0, inspected)
|
|
171
|
+
.map(terminalRecord)
|
|
172
|
+
.map(formatter)
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
if (!retained.length)
|
|
175
|
+
return null;
|
|
176
|
+
return boundedTerminalList(retained, Math.max(0, value.length - inspected));
|
|
177
|
+
}
|
|
178
|
+
function appendNetworkRegistrationLines(lines, parsed, type, detail) {
|
|
179
|
+
if (!Object.keys(parsed).length || detail === 'summary')
|
|
180
|
+
return;
|
|
181
|
+
if (parsed.handle)
|
|
182
|
+
lines.push(`RDAP handle ${safeTerminalValue(parsed.handle)}`);
|
|
183
|
+
if (parsed.name)
|
|
184
|
+
lines.push(`RDAP name ${safeTerminalValue(parsed.name)}`);
|
|
185
|
+
if (type === 'asn') {
|
|
186
|
+
if (parsed.startAutnum !== undefined && parsed.endAutnum !== undefined) {
|
|
187
|
+
const range = parsed.startAutnum === parsed.endAutnum
|
|
188
|
+
? safeTerminalValue(parsed.startAutnum)
|
|
189
|
+
: `${safeTerminalValue(parsed.startAutnum)} to ${safeTerminalValue(parsed.endAutnum)}`;
|
|
190
|
+
lines.push(`ASN range ${range}`);
|
|
191
|
+
}
|
|
192
|
+
else if (parsed.startAutnum !== undefined) {
|
|
193
|
+
lines.push(`ASN start ${safeTerminalValue(parsed.startAutnum)}`);
|
|
194
|
+
}
|
|
195
|
+
else if (parsed.endAutnum !== undefined) {
|
|
196
|
+
lines.push(`ASN end ${safeTerminalValue(parsed.endAutnum)}`);
|
|
197
|
+
}
|
|
198
|
+
if (parsed.autnumType)
|
|
199
|
+
lines.push(`Allocation ${safeTerminalValue(parsed.autnumType)}`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
if (parsed.startAddress && parsed.endAddress) {
|
|
203
|
+
lines.push(`Address range ${safeTerminalValue(parsed.startAddress)} to ${safeTerminalValue(parsed.endAddress)}`);
|
|
204
|
+
}
|
|
205
|
+
else if (parsed.startAddress) {
|
|
206
|
+
lines.push(`Address start ${safeTerminalValue(parsed.startAddress)}`);
|
|
207
|
+
}
|
|
208
|
+
else if (parsed.endAddress) {
|
|
209
|
+
lines.push(`Address end ${safeTerminalValue(parsed.endAddress)}`);
|
|
210
|
+
}
|
|
211
|
+
const cidrs = boundedTerminalStrings(parsed.cidrs, MAX_LOOKUP_TERMINAL_CIDRS);
|
|
212
|
+
if (cidrs)
|
|
213
|
+
lines.push(`CIDR prefixes ${cidrs}`);
|
|
214
|
+
if (parsed.networkType)
|
|
215
|
+
lines.push(`Allocation ${safeTerminalValue(parsed.networkType)}`);
|
|
216
|
+
}
|
|
217
|
+
if (parsed.country)
|
|
218
|
+
lines.push(`Country ${safeTerminalValue(parsed.country)}`);
|
|
219
|
+
const statuses = boundedTerminalStrings(parsed.statuses);
|
|
220
|
+
if (statuses)
|
|
221
|
+
lines.push(`Statuses ${statuses}`);
|
|
222
|
+
if (detail === 'verbose') {
|
|
223
|
+
const created = lifecycleDate(parsed, 'createdDate');
|
|
224
|
+
const updated = lifecycleDate(parsed, 'updatedDate');
|
|
225
|
+
if (created)
|
|
226
|
+
lines.push(`Created ${safeTerminalValue(created)}`);
|
|
227
|
+
if (updated)
|
|
228
|
+
lines.push(`Updated ${safeTerminalValue(updated)}`);
|
|
229
|
+
if (parsed.serverTruncated === true || parsed.cidrsTruncated === true)
|
|
230
|
+
lines.push('RDAP detail Source declared or local list truncation');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
234
|
+
const availability = terminalRecord(document.availability);
|
|
235
|
+
const diagnostics = terminalRecord(document.diagnostics);
|
|
236
|
+
const rdapDiagnostics = terminalRecord(diagnostics.rdap);
|
|
237
|
+
const whoisDiagnostics = terminalRecord(diagnostics.whois);
|
|
238
|
+
const targetLines = [
|
|
239
|
+
`Query ${safeTerminalValue(document.query)}`,
|
|
240
|
+
`Type ${safeTerminalValue(document.type)}`,
|
|
241
|
+
`Mode ${titleCase(document.mode)}`,
|
|
242
|
+
];
|
|
243
|
+
if (document.inputHostname && document.inputHostname !== document.registrableDomain) {
|
|
244
|
+
targetLines.push(`Input host ${safeTerminalValue(document.inputHostname)}`);
|
|
245
|
+
targetLines.push(`Registry query ${safeTerminalValue(document.registrableDomain)}`);
|
|
246
|
+
}
|
|
247
|
+
if (availability.applicable) {
|
|
248
|
+
targetLines.push(`Availability ${titleCase(availability.state)}`);
|
|
249
|
+
targetLines.push(`Confidence ${titleCase(availability.confidence)}`);
|
|
250
|
+
}
|
|
251
|
+
const registrationLines = [
|
|
252
|
+
`RDAP ${titleCase(rdapDiagnostics.status)}`,
|
|
253
|
+
`WHOIS ${titleCase(whoisDiagnostics.status)}`,
|
|
254
|
+
];
|
|
255
|
+
if (detail !== 'summary' && rdapDiagnostics.endpoint) {
|
|
256
|
+
registrationLines.splice(1, 0, `RDAP source ${safeTerminalValue(rdapDiagnostics.endpoint)}`);
|
|
257
|
+
}
|
|
258
|
+
const registrarRdap = terminalRecord(rdapDiagnostics.registrar);
|
|
259
|
+
if (Object.keys(registrarRdap).length) {
|
|
260
|
+
registrationLines.push(`Registrar RDAP ${titleCase(registrarRdap.status)}`);
|
|
261
|
+
if (detail !== 'summary' && registrarRdap.endpoint) {
|
|
262
|
+
registrationLines.push(`Registrar source ${safeTerminalValue(registrarRdap.endpoint)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (['ipv4', 'ipv6', 'asn'].includes(String(document.type)) && positiveSourceStatus(rdapDiagnostics.status)) {
|
|
266
|
+
const rdap = terminalRecord(document.rdap);
|
|
267
|
+
appendNetworkRegistrationLines(registrationLines, terminalRecord(rdap.parsed), document.type, detail);
|
|
268
|
+
}
|
|
269
|
+
const registryInsights = terminalRecord(document.registryInsights);
|
|
270
|
+
if (detail !== 'summary' && registryInsights.version === 1) {
|
|
271
|
+
const lifecycle = terminalRecord(registryInsights.lifecycle);
|
|
272
|
+
const disclosure = terminalRecord(registryInsights.contactDisclosure);
|
|
273
|
+
const registryDisclosure = terminalRecord(disclosure.registryRdap);
|
|
274
|
+
const whoisDisclosure = terminalRecord(disclosure.whois);
|
|
275
|
+
const reconciliation = terminalRecord(registryInsights.reconciliation);
|
|
276
|
+
const publications = Array.isArray(registryInsights.publications)
|
|
277
|
+
? registryInsights.publications.map(terminalRecord)
|
|
278
|
+
: [];
|
|
279
|
+
const publicationCounts = {
|
|
280
|
+
complete: publications.filter((item) => item.state === 'complete').length,
|
|
281
|
+
partial: publications.filter((item) => item.state === 'partial').length,
|
|
282
|
+
unavailable: publications.filter((item) => item.state === 'unavailable').length,
|
|
283
|
+
};
|
|
284
|
+
registrationLines.push(`Lifecycle ${titleCase(lifecycle.label)}`);
|
|
285
|
+
registrationLines.push(`Disclosure RDAP ${titleCase(registryDisclosure.state)} · WHOIS ${titleCase(whoisDisclosure.state)}`);
|
|
286
|
+
registrationLines.push(`Reconciliation ${titleCase(reconciliation.state)}`);
|
|
287
|
+
registrationLines.push(`Publications ${publicationCounts.complete} complete · ${publicationCounts.partial} partial · ${publicationCounts.unavailable} unavailable`);
|
|
288
|
+
}
|
|
289
|
+
const registrarStanding = terminalRecord(document.registrarStanding);
|
|
290
|
+
if (registrarStanding.version === 1) {
|
|
291
|
+
const accreditation = terminalRecord(registrarStanding.accreditation);
|
|
292
|
+
const compliance = terminalRecord(registrarStanding.compliance);
|
|
293
|
+
const assessment = terminalRecord(registrarStanding.assessment);
|
|
294
|
+
const actions = Array.isArray(compliance.actions)
|
|
295
|
+
? compliance.actions.map(terminalRecord).slice(0, 5)
|
|
296
|
+
: [];
|
|
297
|
+
registrationLines.push(`Registrar standing ${safeTerminalValue(assessment.label || assessment.state || 'Unavailable')}`);
|
|
298
|
+
if (detail !== 'summary') {
|
|
299
|
+
registrationLines.push(`Registrar ID ${safeTerminalValue(registrarStanding.ianaId || 'Unavailable')}`);
|
|
300
|
+
registrationLines.push(`Accreditation ${titleCase(accreditation.state)} · source ${titleCase(accreditation.sourceHealth)}`);
|
|
301
|
+
registrationLines.push(`Compliance ${registrarComplianceSummary(compliance, actions.length)}`);
|
|
302
|
+
for (const action of actions) {
|
|
303
|
+
registrationLines.push(`Official notice ${titleCase(action.type)} · ${safeTerminalValue(action.issuedOn)}`);
|
|
304
|
+
if (action.sourceUrl)
|
|
305
|
+
registrationLines.push(`Notice source ${safeTerminalValue(action.sourceUrl)}`);
|
|
306
|
+
}
|
|
307
|
+
if (assessment.detail)
|
|
308
|
+
registrationLines.push(`Standing detail ${safeTerminalValue(assessment.detail)}`);
|
|
309
|
+
registrationLines.push('Standing scope Provider standing is context, not a classification of this domain.');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const dnsLines = [];
|
|
313
|
+
const mailLines = [];
|
|
314
|
+
const tlsLines = [];
|
|
315
|
+
const certificateWarningLines = [];
|
|
316
|
+
if (document.mode === 'deep' && document.type === 'domain') {
|
|
317
|
+
const dns = terminalRecord(availability.dns);
|
|
318
|
+
const dnsRecords = terminalRecord(dns.records);
|
|
319
|
+
const dnsDiagnostics = terminalRecord(dns.diagnostics);
|
|
320
|
+
if (Object.keys(dns).length) {
|
|
321
|
+
dnsLines.push(`Evidence ${titleCase(dns.status)}`);
|
|
322
|
+
dnsLines.push(`Completeness ${dns.complete === true ? 'Complete' : 'Incomplete'}`);
|
|
323
|
+
const recordTypes = [
|
|
324
|
+
['a', 'A'], ['aaaa', 'AAAA'], ['cname', 'CNAME'], ['ns', 'NS'],
|
|
325
|
+
['mx', 'MX'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['caa', 'CAA'],
|
|
326
|
+
['soa', 'SOA'], ['https', 'HTTPS'],
|
|
327
|
+
];
|
|
328
|
+
const retainedCounts = recordTypes
|
|
329
|
+
.map(([key, label]) => [label, Array.isArray(dnsRecords[key]) ? dnsRecords[key].length : 0])
|
|
330
|
+
.filter(([, count]) => count > 0)
|
|
331
|
+
.map(([label, count]) => `${label} ${count}`);
|
|
332
|
+
dnsLines.push(`Retained ${retainedCounts.length ? safeTerminalValue(retainedCounts.join(' · ')) : 'No records retained'}`);
|
|
333
|
+
if (detail === 'verbose') {
|
|
334
|
+
const unsettled = recordTypes.flatMap(([key, label]) => {
|
|
335
|
+
const diagnostic = terminalRecord(dnsDiagnostics[key]);
|
|
336
|
+
return diagnostic.status && !['success', 'not_found'].includes(String(diagnostic.status))
|
|
337
|
+
? [`${label} ${titleCase(diagnostic.status)}`]
|
|
338
|
+
: [];
|
|
339
|
+
});
|
|
340
|
+
if (unsettled.length)
|
|
341
|
+
dnsLines.push(`Query states ${safeTerminalValue(unsettled.join(' · '))}`);
|
|
342
|
+
}
|
|
343
|
+
if (detail !== 'summary') {
|
|
344
|
+
for (const [key, label] of recordTypes)
|
|
345
|
+
appendLookupRecordLine(dnsLines, label, dnsRecords[key]);
|
|
346
|
+
}
|
|
347
|
+
mailLines.push(`Evidence ${titleCase(dns.status)}`);
|
|
348
|
+
mailLines.push(`Mail exchange ${lookupObservationLabel(dns.hasNullMx === true ? true : dns.hasMx, dns.hasNullMx === true ? 'Null MX observed' : 'MX observed', 'No MX observed')}`);
|
|
349
|
+
mailLines.push(`SPF ${lookupObservationLabel(dns.hasSpf, 'Record observed', 'No record observed')}`);
|
|
350
|
+
mailLines.push(`DMARC ${lookupObservationLabel(dns.hasDmarc, 'Record observed', 'No record observed')}`);
|
|
351
|
+
if (detail !== 'summary') {
|
|
352
|
+
appendLookupRecordLine(mailLines, 'MX hosts', Array.isArray(dns.mxHosts) ? dns.mxHosts : []);
|
|
353
|
+
appendLookupRecordLine(mailLines, 'SPF policy', dnsRecords.spf);
|
|
354
|
+
appendLookupRecordLine(mailLines, 'DMARC policy', dnsRecords.dmarc);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const tls = terminalRecord(availability.tls);
|
|
358
|
+
if (Object.keys(tls).length) {
|
|
359
|
+
const usableTls = positiveSourceStatus(tls.status);
|
|
360
|
+
const certificate = usableTls ? terminalRecord(tls.certificate) : {};
|
|
361
|
+
const subject = terminalRecord(certificate.subject);
|
|
362
|
+
const issuer = terminalRecord(certificate.issuer);
|
|
363
|
+
const authorization = usableTls ? terminalRecord(tls.authorization) : {};
|
|
364
|
+
const hostname = usableTls ? terminalRecord(tls.hostname) : {};
|
|
365
|
+
const validity = usableTls ? terminalRecord(tls.validity) : {};
|
|
366
|
+
const cipher = usableTls ? terminalRecord(tls.cipher) : {};
|
|
367
|
+
tlsLines.push(`Evidence ${titleCase(tls.status)}`);
|
|
368
|
+
const completeness = sourceCompleteness(tls);
|
|
369
|
+
if (completeness)
|
|
370
|
+
tlsLines.push(`Completeness ${completeness}`);
|
|
371
|
+
if (usableTls && tls.connectedAddress && detail !== 'summary')
|
|
372
|
+
tlsLines.push(`Address ${safeTerminalValue(tls.connectedAddress)}`);
|
|
373
|
+
if (usableTls && tls.protocol)
|
|
374
|
+
tlsLines.push(`Protocol ${safeTerminalValue(tls.protocol)}`);
|
|
375
|
+
if (usableTls && tls.alpnProtocol && detail !== 'summary')
|
|
376
|
+
tlsLines.push(`ALPN ${safeTerminalValue(tls.alpnProtocol)}`);
|
|
377
|
+
if (cipher.standardName || cipher.name)
|
|
378
|
+
tlsLines.push(`Cipher ${safeTerminalValue(cipher.standardName || cipher.name)}`);
|
|
379
|
+
tlsLines.push(`Chain trust ${authorization.authorized === true ? 'Authorised' : authorization.authorized === false ? 'Not authorised' : 'Unavailable'}`);
|
|
380
|
+
tlsLines.push(`Hostname ${hostname.matches === true ? 'Matched' : hostname.matches === false ? 'Not matched' : 'Unavailable'}`);
|
|
381
|
+
tlsLines.push(`Validity ${validity.status ? titleCase(validity.status) : 'Unavailable'}`);
|
|
382
|
+
const subjectName = boundedTerminalNames(subject.commonNames ?? subject.CN);
|
|
383
|
+
const issuerName = boundedTerminalNames(issuer.commonNames ?? issuer.CN);
|
|
384
|
+
if (detail !== 'summary' && subjectName)
|
|
385
|
+
tlsLines.push(`Subject ${subjectName}`);
|
|
386
|
+
if (detail !== 'summary' && issuerName)
|
|
387
|
+
tlsLines.push(`Issuer ${issuerName}`);
|
|
388
|
+
if (detail !== 'summary' && certificate.validFrom)
|
|
389
|
+
tlsLines.push(`Valid from ${safeTerminalValue(certificate.validFrom)}`);
|
|
390
|
+
if (detail !== 'summary' && certificate.validTo)
|
|
391
|
+
tlsLines.push(`Valid until ${safeTerminalValue(certificate.validTo)}`);
|
|
392
|
+
const publicKey = terminalRecord(certificate.publicKey);
|
|
393
|
+
if (detail !== 'summary' && (publicKey.type || publicKey.bits || publicKey.curve)) {
|
|
394
|
+
tlsLines.push(`Public key ${safeTerminalValue([publicKey.type, publicKey.bits ? `${publicKey.bits} bits` : null, publicKey.curve].filter(Boolean).join(' '))}`);
|
|
395
|
+
}
|
|
396
|
+
const signature = terminalRecord(certificate.signature);
|
|
397
|
+
if (detail !== 'summary' && (signature.algorithm || signature.oid)) {
|
|
398
|
+
tlsLines.push(`Signature ${safeTerminalValue(signature.algorithm || signature.oid)}`);
|
|
399
|
+
}
|
|
400
|
+
const altNames = terminalRecord(certificate.subjectAltNames);
|
|
401
|
+
const dnsAltNames = Array.isArray(altNames.dnsNames) ? altNames.dnsNames : [];
|
|
402
|
+
const ipAltNames = Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses : [];
|
|
403
|
+
const sanClasses = terminalCountSummary(altNames.classes, [
|
|
404
|
+
['dns', 'DNS'], ['ip', 'IP'], ['email', 'email'], ['uri', 'URI'],
|
|
405
|
+
['directoryName', 'directory name'], ['registeredId', 'registered ID'],
|
|
406
|
+
['otherName', 'other name'], ['unclassified', 'other'],
|
|
407
|
+
]);
|
|
408
|
+
if (detail !== 'summary' && (dnsAltNames.length || ipAltNames.length || sanClasses)) {
|
|
409
|
+
tlsLines.push(`SAN summary ${sanClasses || `DNS ${dnsAltNames.length} · IP ${ipAltNames.length}`}${altNames.truncated ? ' · truncated' : ''}`);
|
|
410
|
+
}
|
|
411
|
+
const chainCount = Array.isArray(tls.chain) ? tls.chain.length : 0;
|
|
412
|
+
if (detail !== 'summary' && (chainCount || tls.chainTruncated === true)) {
|
|
413
|
+
tlsLines.push(`Chain ${chainCount} retained certificate${chainCount === 1 ? '' : 's'}${tls.chainTruncated ? ' · truncated' : ''}`);
|
|
414
|
+
}
|
|
415
|
+
const aia = terminalRecord(certificate.authorityInformationAccess);
|
|
416
|
+
if (detail !== 'summary' && Object.keys(aia).length) {
|
|
417
|
+
const ocsp = terminalRecord(aia.ocsp);
|
|
418
|
+
const caIssuers = terminalRecord(aia.caIssuers);
|
|
419
|
+
tlsLines.push(`AIA presence OCSP ${terminalCount(ocsp.total)} · CA issuers ${terminalCount(caIssuers.total)} · `
|
|
420
|
+
+ `unknown methods ${terminalCount(aia.unknownMethods)}${aia.truncated ? ' · truncated' : ''}`);
|
|
421
|
+
}
|
|
422
|
+
if (detail === 'verbose') {
|
|
423
|
+
const visibleAltNames = [...dnsAltNames, ...ipAltNames]
|
|
424
|
+
.slice(0, MAX_TLS_TERMINAL_ALT_NAMES)
|
|
425
|
+
.map((value) => safeTerminalValue(value));
|
|
426
|
+
if (visibleAltNames.length)
|
|
427
|
+
tlsLines.push(`Alt names ${boundedTerminalList(visibleAltNames, Math.max(0, dnsAltNames.length + ipAltNames.length - visibleAltNames.length))}`);
|
|
428
|
+
const purposes = terminalRecord(certificate.extendedKeyUsage);
|
|
429
|
+
const purposeValues = Array.isArray(purposes.values) ? purposes.values : [];
|
|
430
|
+
const visiblePurposes = purposeValues.slice(0, MAX_TLS_TERMINAL_PURPOSES).map((value) => {
|
|
431
|
+
const purpose = terminalRecord(value);
|
|
432
|
+
return safeTerminalValue(purpose.name || purpose.oid || 'Unrecognized purpose');
|
|
433
|
+
});
|
|
434
|
+
if (visiblePurposes.length)
|
|
435
|
+
tlsLines.push(`Purposes ${boundedTerminalList(visiblePurposes, Math.max(0, purposeValues.length - visiblePurposes.length))}${purposes.truncated ? ' · truncated' : ''}`);
|
|
436
|
+
const findings = boundedFindingLabels(tls.findings, (finding) => safeTerminalValue(finding.label, 'Unlabelled finding'));
|
|
437
|
+
if (findings)
|
|
438
|
+
tlsLines.push(`Findings ${findings}`);
|
|
439
|
+
}
|
|
440
|
+
if (detail === 'verbose' && certificate.fingerprintSha256)
|
|
441
|
+
tlsLines.push(`SHA-256 ${safeTerminalValue(certificate.fingerprintSha256)}`);
|
|
442
|
+
if (detail === 'verbose' && Array.isArray(tls.limitations)) {
|
|
443
|
+
for (const limitation of tls.limitations.slice(0, MAX_LOOKUP_TERMINAL_LIMITATIONS))
|
|
444
|
+
tlsLines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
445
|
+
const omittedLimitations = Math.max(0, tls.limitations.length - MAX_LOOKUP_TERMINAL_LIMITATIONS);
|
|
446
|
+
if (omittedLimitations)
|
|
447
|
+
tlsLines.push(`Limitation +${omittedLimitations} more retained limitation${omittedLimitations === 1 ? '' : 's'}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const sslbl = terminalRecord(document.sslbl);
|
|
451
|
+
if (Object.keys(sslbl).length) {
|
|
452
|
+
certificateWarningLines.push('Source Local SSLBL certificate snapshot');
|
|
453
|
+
certificateWarningLines.push(`Evidence ${titleCase(sslbl.status)}`);
|
|
454
|
+
certificateWarningLines.push(`Result ${certificateWarningVerdict(sslbl)}`);
|
|
455
|
+
const completeness = sourceCompleteness(sslbl);
|
|
456
|
+
if (completeness)
|
|
457
|
+
certificateWarningLines.push(`Completeness ${completeness}`);
|
|
458
|
+
certificateWarningLines.push('Interpretation Review lead only; not a maliciousness verdict');
|
|
459
|
+
if (detail !== 'summary' && sslbl.observedAt)
|
|
460
|
+
certificateWarningLines.push(`Observed ${safeTerminalValue(sslbl.observedAt)}`);
|
|
461
|
+
if (detail === 'verbose') {
|
|
462
|
+
const snapshot = terminalRecord(sslbl.snapshot);
|
|
463
|
+
if (snapshot.sourceUpdatedAt)
|
|
464
|
+
certificateWarningLines.push(`Snapshot date ${safeTerminalValue(snapshot.sourceUpdatedAt)}`);
|
|
465
|
+
if (sslbl.fingerprintSha1)
|
|
466
|
+
certificateWarningLines.push(`SHA-1 ${safeTerminalValue(sslbl.fingerprintSha1)}`);
|
|
467
|
+
if (sslbl.detail)
|
|
468
|
+
certificateWarningLines.push(`Detail ${safeTerminalValue(sslbl.detail)}`);
|
|
469
|
+
if (Array.isArray(sslbl.limitations)) {
|
|
470
|
+
for (const limitation of sslbl.limitations.slice(0, MAX_LOOKUP_TERMINAL_LIMITATIONS)) {
|
|
471
|
+
certificateWarningLines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
472
|
+
}
|
|
473
|
+
const omitted = Math.max(0, sslbl.limitations.length - MAX_LOOKUP_TERMINAL_LIMITATIONS);
|
|
474
|
+
if (omitted)
|
|
475
|
+
certificateWarningLines.push(`Limitation +${omitted} more retained limitation${omitted === 1 ? '' : 's'}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const websiteLines = [];
|
|
481
|
+
if (document.mode === 'deep' && document.type === 'domain') {
|
|
482
|
+
const http = terminalRecord(availability.http);
|
|
483
|
+
const httpResponse = terminalRecord(http.response);
|
|
484
|
+
const credentialSurface = terminalRecord(availability.credentialSurfaceProfile);
|
|
485
|
+
const structuredIdentity = terminalRecord(availability.structuredDataIdentity);
|
|
486
|
+
const technology = terminalRecord(availability.technologyProfile);
|
|
487
|
+
const browserLibraries = terminalRecord(technology.browserLibraryProfile);
|
|
488
|
+
const posture = terminalRecord(availability.securityPosture);
|
|
489
|
+
const postureSummary = terminalRecord(posture.summary);
|
|
490
|
+
const pageIdentity = terminalRecord(availability.pageIdentity);
|
|
491
|
+
const pageRole = terminalRecord(availability.pageRoleProfile);
|
|
492
|
+
const clientBehavior = terminalRecord(availability.clientBehaviorProfile);
|
|
493
|
+
if (availability.activityStatus)
|
|
494
|
+
websiteLines.push(`Web activity ${titleCase(availability.activityStatus)}`);
|
|
495
|
+
if (detail !== 'summary' && availability.pageTitle)
|
|
496
|
+
websiteLines.push(`Page title ${safeTerminalValue(availability.pageTitle)}`);
|
|
497
|
+
if (http.status) {
|
|
498
|
+
websiteLines.push(`HTTP evidence ${titleCase(http.status)}`);
|
|
499
|
+
const responseDetail = [
|
|
500
|
+
httpResponse.status ? `HTTP ${safeTerminalValue(httpResponse.status)}` : null,
|
|
501
|
+
http.transportSecurity ? safeTerminalValue(http.transportSecurity).toUpperCase() : null,
|
|
502
|
+
].filter(Boolean).join(' · ');
|
|
503
|
+
if (detail !== 'summary' && responseDetail)
|
|
504
|
+
websiteLines.push(`HTTP response ${responseDetail}`);
|
|
505
|
+
if (detail !== 'summary')
|
|
506
|
+
appendDeliveryMetadataLines(websiteLines, httpResponse.deliveryMetadata, detail === 'verbose');
|
|
507
|
+
}
|
|
508
|
+
if (pageIdentity.status || pageIdentity.source === 'html') {
|
|
509
|
+
websiteLines.push(`Page identity ${titleCase(pageIdentity.status)}`);
|
|
510
|
+
const completeness = sourceCompleteness(pageIdentity);
|
|
511
|
+
if (completeness)
|
|
512
|
+
websiteLines.push(`Page coverage ${completeness}`);
|
|
513
|
+
if (positiveSourceStatus(pageIdentity.status) && detail !== 'summary') {
|
|
514
|
+
if (pageIdentity.documentLanguage)
|
|
515
|
+
websiteLines.push(`Language ${safeTerminalValue(pageIdentity.documentLanguage)}`);
|
|
516
|
+
const canonical = retainedUrlRelationship(pageIdentity.canonical, document.registrableDomain || availability.domain || document.query);
|
|
517
|
+
if (canonical)
|
|
518
|
+
websiteLines.push(`Canonical ${canonical}`);
|
|
519
|
+
const openGraph = terminalRecord(pageIdentity.openGraph);
|
|
520
|
+
if (openGraph.title || openGraph.siteName) {
|
|
521
|
+
websiteLines.push(`Open Graph ${safeTerminalValue([openGraph.siteName, openGraph.title].filter(Boolean).join(' · '))}`);
|
|
522
|
+
}
|
|
523
|
+
if (pageIdentity.generator)
|
|
524
|
+
websiteLines.push(`Generator ${safeTerminalValue(pageIdentity.generator)}`);
|
|
525
|
+
const forms = terminalRecord(pageIdentity.forms);
|
|
526
|
+
if (Object.keys(forms).length)
|
|
527
|
+
websiteLines.push(`Page forms ${terminalDisplayCount(forms.count)} total · ${terminalDisplayCount(forms.postCount)} POST · ${terminalDisplayCount(forms.insecureActionCount)} insecure action`);
|
|
528
|
+
const resources = terminalRecord(pageIdentity.resources);
|
|
529
|
+
const resourceTypes = terminalCountSummary(resources.byType, [
|
|
530
|
+
['image', 'image'], ['script', 'script'], ['stylesheet', 'stylesheet'], ['link', 'link'],
|
|
531
|
+
['frame', 'frame'], ['media', 'media'], ['object', 'object'],
|
|
532
|
+
]);
|
|
533
|
+
if (Object.keys(resources).length)
|
|
534
|
+
websiteLines.push(`Resources ${terminalDisplayCount(resources.count)} retained${resourceTypes ? ` · ${resourceTypes}` : ''}${resources.truncated ? ' · truncated' : ''}`);
|
|
535
|
+
const downloads = terminalRecord(pageIdentity.downloads);
|
|
536
|
+
if (Object.keys(downloads).length)
|
|
537
|
+
websiteLines.push(`Downloads ${terminalDisplayCount(downloads.count)} retained · ${terminalDisplayCount(downloads.riskyCount)} review file${Number(downloads.riskyCount) === 1 ? '' : 's'}${downloads.truncated ? ' · truncated' : ''}`);
|
|
538
|
+
if ([pageIdentity.embeddedOrigins, pageIdentity.contactDomains, pageIdentity.trackingIdentifiers].some(Array.isArray)) {
|
|
539
|
+
websiteLines.push(`Relationships ${Array.isArray(pageIdentity.embeddedOrigins) ? pageIdentity.embeddedOrigins.length : 0} embedded retained · `
|
|
540
|
+
+ `${Array.isArray(pageIdentity.contactDomains) ? pageIdentity.contactDomains.length : 0} contact domain${Array.isArray(pageIdentity.contactDomains) && pageIdentity.contactDomains.length === 1 ? '' : 's'} retained · `
|
|
541
|
+
+ `${Array.isArray(pageIdentity.trackingIdentifiers) ? pageIdentity.trackingIdentifiers.length : 0} tracking identifier${Array.isArray(pageIdentity.trackingIdentifiers) && pageIdentity.trackingIdentifiers.length === 1 ? '' : 's'} retained`);
|
|
542
|
+
}
|
|
543
|
+
appendPublicationMetadataLines(websiteLines, pageIdentity.publicationMetadata, detail);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (availability.phishingLanguageMatch && detail !== 'summary') {
|
|
547
|
+
websiteLines.push(`Content cue ${safeTerminalValue(availability.phishingLanguageMatch)} · static review label`);
|
|
548
|
+
}
|
|
549
|
+
if (credentialSurface.status || credentialSurface.source === 'html') {
|
|
550
|
+
const forms = terminalRecord(credentialSurface.forms);
|
|
551
|
+
const inputs = terminalRecord(credentialSurface.inputs);
|
|
552
|
+
const categories = terminalRecord(inputs.categories);
|
|
553
|
+
const actions = terminalRecord(forms.actions);
|
|
554
|
+
const formCount = terminalCount(forms.count);
|
|
555
|
+
const inputCount = terminalCount(inputs.count);
|
|
556
|
+
const externalActionCount = terminalCount(actions.external);
|
|
557
|
+
websiteLines.push(`Credential UI ${titleCase(credentialSurface.status)} · ${safeTerminalValue(inputs.classifiedCount, '0')} classified input${Number(inputs.classifiedCount) === 1 ? '' : 's'}`);
|
|
558
|
+
if (detail !== 'summary')
|
|
559
|
+
websiteLines.push(`Form surface ${safeTerminalValue(formCount)} form${formCount === 1 ? '' : 's'} · ${safeTerminalValue(inputCount)} input${inputCount === 1 ? '' : 's'} · ${safeTerminalValue(externalActionCount)} external action${externalActionCount === 1 ? '' : 's'}`);
|
|
560
|
+
const visible = [
|
|
561
|
+
['password', categories.password],
|
|
562
|
+
['email', categories.email],
|
|
563
|
+
['username', categories.username],
|
|
564
|
+
['one-time code', categories.one_time_code],
|
|
565
|
+
['payment related', categories.payment],
|
|
566
|
+
].filter(([, count]) => Number(count) > 0).map(([label, count]) => `${safeTerminalValue(label)} ${safeTerminalValue(count)}`);
|
|
567
|
+
if (detail !== 'summary' && visible.length)
|
|
568
|
+
websiteLines.push(`Input purposes ${safeTerminalValue(visible.join(' · '))}`);
|
|
569
|
+
}
|
|
570
|
+
if (structuredIdentity.status || structuredIdentity.source === 'html') {
|
|
571
|
+
const entities = Array.isArray(structuredIdentity.entities) ? structuredIdentity.entities : [];
|
|
572
|
+
websiteLines.push(`Structured ID ${titleCase(structuredIdentity.status)} · ${entities.length} declared entit${entities.length === 1 ? 'y' : 'ies'}`);
|
|
573
|
+
const visible = entities.slice(0, 4).map((entity) => {
|
|
574
|
+
const item = terminalRecord(entity);
|
|
575
|
+
const types = Array.isArray(item.types) ? item.types.slice(0, 3).map((value) => safeTerminalValue(value)).join('/') : '';
|
|
576
|
+
return `${safeTerminalValue(item.name, 'Unnamed declaration')}${types ? ` (${types})` : ''}`;
|
|
577
|
+
});
|
|
578
|
+
if (detail !== 'summary' && visible.length)
|
|
579
|
+
websiteLines.push(`Declarations ${safeTerminalValue(visible.join('; '))}`);
|
|
580
|
+
}
|
|
581
|
+
if (technology.status || technology.source === 'derived') {
|
|
582
|
+
const findings = Array.isArray(technology.findings) ? technology.findings : [];
|
|
583
|
+
websiteLines.push(`Technology ${titleCase(technology.status)} · ${findings.length} indicator${findings.length === 1 ? '' : 's'}`);
|
|
584
|
+
const visible = findings.slice(0, 6).map((finding) => {
|
|
585
|
+
const item = terminalRecord(finding);
|
|
586
|
+
const qualifiers = [item.category, item.confidence ? `${item.confidence} signature strength` : null].filter(Boolean).map((value) => safeTerminalValue(value));
|
|
587
|
+
return `${safeTerminalValue(item.name, 'Unnamed indicator')}${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`;
|
|
588
|
+
});
|
|
589
|
+
if (detail !== 'summary' && visible.length) {
|
|
590
|
+
const omitted = findings.length - visible.length;
|
|
591
|
+
websiteLines.push(`Indicators ${safeTerminalValue(`${visible.join('; ')}${omitted > 0 ? `; +${omitted} more` : ''}`)}`);
|
|
592
|
+
}
|
|
593
|
+
if (detail !== 'summary') {
|
|
594
|
+
const nameservers = Array.isArray(availability.nameservers)
|
|
595
|
+
? availability.nameservers.slice(0, MAX_LOOKUP_TERMINAL_NAMES).map((value) => safeTerminalValue(value))
|
|
596
|
+
: [];
|
|
597
|
+
websiteLines.push(`Nameservers ${nameservers.length ? boundedTerminalList(nameservers, Math.max(0, availability.nameservers.length - nameservers.length)) : 'Unavailable'} · identity does not establish operator or web-host ownership`);
|
|
598
|
+
websiteLines.push(`Observed edge ${terminalTechnologyRoleNames(findings, 'observed_edge')}`);
|
|
599
|
+
websiteLines.push(`App platform ${terminalTechnologyRoleNames(findings, 'application_platform')}`);
|
|
600
|
+
websiteLines.push(`Framework/run ${terminalTechnologyRoleNames(findings, 'framework_runtime')}`);
|
|
601
|
+
websiteLines.push(`Embedded deps ${terminalTechnologyRoleNames(findings, 'embedded_dependency')}`);
|
|
602
|
+
websiteLines.push('Origin host Not established from retained evidence');
|
|
603
|
+
}
|
|
604
|
+
if (detail !== 'summary' && (browserLibraries.profileVersion === 1 || browserLibraries.source === 'derived')) {
|
|
605
|
+
const libraries = Array.isArray(browserLibraries.findings) ? browserLibraries.findings : [];
|
|
606
|
+
const advisoryMatches = libraries.filter((finding) => terminalCount(terminalRecord(finding).advisoryCount) > 0).length;
|
|
607
|
+
websiteLines.push(`JS libraries ${titleCase(browserLibraries.status)} · ${libraries.length} apparent · `
|
|
608
|
+
+ `${advisoryMatches} with catalogue advisory match${advisoryMatches === 1 ? '' : 'es'}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (posture.status || posture.source === 'derived') {
|
|
612
|
+
websiteLines.push(`Posture ${titleCase(posture.status)}`);
|
|
613
|
+
if (detail !== 'summary' && Object.keys(postureSummary).length)
|
|
614
|
+
websiteLines.push(`Posture counts ${terminalCount(postureSummary.observed)} observed · `
|
|
615
|
+
+ `${terminalCount(postureSummary.potentialExposure)} potential exposure · `
|
|
616
|
+
+ `${terminalCount(postureSummary.observedAbsence)} observed absence · `
|
|
617
|
+
+ `${terminalCount(postureSummary.unavailable)} unavailable`);
|
|
618
|
+
if (detail === 'verbose' && positiveSourceStatus(posture.status)) {
|
|
619
|
+
const findings = boundedFindingLabels(posture.findings, (finding) => {
|
|
620
|
+
const state = finding.state ? ` (${titleCase(finding.state)})` : '';
|
|
621
|
+
return `${safeTerminalValue(finding.label, 'Unlabelled finding')}${state}`;
|
|
622
|
+
});
|
|
623
|
+
if (findings)
|
|
624
|
+
websiteLines.push(`Posture labels ${findings}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (pageRole.status || pageRole.source === 'derived') {
|
|
628
|
+
websiteLines.push(`Page role ${titleCase(pageRole.status)}`);
|
|
629
|
+
const roleFindings = Array.isArray(pageRole.findings) ? pageRole.findings : [];
|
|
630
|
+
const primary = roleFindings.map(terminalRecord).find((finding) => finding.role === pageRole.primaryRole)
|
|
631
|
+
|| terminalRecord(roleFindings[0]);
|
|
632
|
+
if (positiveSourceStatus(pageRole.status) && detail !== 'summary' && Object.keys(primary).length) {
|
|
633
|
+
websiteLines.push(`Primary role ${safeTerminalValue(primary.label, 'Unclassified')} · ${titleCase(primary.confidence)} indicator strength`);
|
|
634
|
+
}
|
|
635
|
+
if (positiveSourceStatus(pageRole.status) && detail === 'verbose') {
|
|
636
|
+
const findings = boundedFindingLabels(roleFindings, (finding) => `${safeTerminalValue(finding.label, 'Unclassified')} (${titleCase(finding.confidence)} indicator strength)`);
|
|
637
|
+
if (findings)
|
|
638
|
+
websiteLines.push(`Role labels ${findings}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (clientBehavior.status || clientBehavior.source === 'derived') {
|
|
642
|
+
websiteLines.push(`Client signals ${titleCase(clientBehavior.status)}`);
|
|
643
|
+
const scriptSummary = terminalRecord(clientBehavior.scriptSummary);
|
|
644
|
+
if (positiveSourceStatus(clientBehavior.status) && detail !== 'summary' && Object.keys(scriptSummary).length)
|
|
645
|
+
websiteLines.push(`Scripts ${terminalCount(scriptSummary.elementsObserved)} elements · ${terminalCount(scriptSummary.referencedScripts)} referenced · `
|
|
646
|
+
+ `${terminalCount(scriptSummary.inlineScripts)} inline · ${terminalCount(scriptSummary.moduleScripts)} modules`);
|
|
647
|
+
if (positiveSourceStatus(clientBehavior.status) && detail === 'verbose') {
|
|
648
|
+
const indicators = boundedFindingLabels(clientBehavior.indicators, (indicator) => {
|
|
649
|
+
const occurrences = terminalCount(indicator.occurrences);
|
|
650
|
+
return `${safeTerminalValue(indicator.label, 'Static indicator')} (${titleCase(indicator.evidenceClass)}, ${occurrences})`;
|
|
651
|
+
});
|
|
652
|
+
if (indicators)
|
|
653
|
+
websiteLines.push(`Client labels ${indicators}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const networkLines = [];
|
|
658
|
+
if (document.mode === 'deep' && (document.type === 'ipv4' || document.type === 'ipv6')) {
|
|
659
|
+
const reverseDns = terminalRecord(document.reverseDns);
|
|
660
|
+
const reverseDnsRecords = terminalRecord(reverseDns.records);
|
|
661
|
+
const ptrNames = boundedTerminalStrings(reverseDnsRecords.ptr);
|
|
662
|
+
if (reverseDns.status)
|
|
663
|
+
networkLines.push(`Reverse DNS ${titleCase(reverseDns.status)}`);
|
|
664
|
+
const completeness = sourceCompleteness(reverseDns);
|
|
665
|
+
if (completeness)
|
|
666
|
+
networkLines.push(`PTR coverage ${completeness}`);
|
|
667
|
+
if (detail !== 'summary' && ptrNames)
|
|
668
|
+
networkLines.push(`PTR names ${ptrNames}`);
|
|
669
|
+
if (detail !== 'summary' && !ptrNames && reverseDns.status)
|
|
670
|
+
networkLines.push('PTR names No names retained; source state remains inconclusive');
|
|
671
|
+
}
|
|
672
|
+
const network = terminalRecord(document.networkContext);
|
|
673
|
+
if (network.contextVersion === 1) {
|
|
674
|
+
const endpoint = terminalRecord(network.endpoint);
|
|
675
|
+
const networkRecord = terminalRecord(network.network);
|
|
676
|
+
networkLines.push(`Network RDAP ${titleCase(network.status)}`);
|
|
677
|
+
const completeness = sourceCompleteness(network);
|
|
678
|
+
if (completeness)
|
|
679
|
+
networkLines.push(`Completeness ${completeness}`);
|
|
680
|
+
if (detail !== 'summary' && endpoint.address)
|
|
681
|
+
networkLines.push(`Selected IP ${safeTerminalValue(endpoint.address)}`);
|
|
682
|
+
if (detail !== 'summary' && endpoint.selectedFrom)
|
|
683
|
+
networkLines.push(`Selected from ${addressSelectionLabel(endpoint.selectedFrom)}`);
|
|
684
|
+
if (positiveSourceStatus(network.status) && (networkRecord.name || networkRecord.holder)) {
|
|
685
|
+
networkLines.push(`Network ${safeTerminalValue(networkRecord.name || networkRecord.holder)}`);
|
|
686
|
+
}
|
|
687
|
+
if (positiveSourceStatus(network.status) && detail !== 'summary') {
|
|
688
|
+
if (networkRecord.handle)
|
|
689
|
+
networkLines.push(`Network handle ${safeTerminalValue(networkRecord.handle)}`);
|
|
690
|
+
if (networkRecord.holder && networkRecord.holder !== networkRecord.name)
|
|
691
|
+
networkLines.push(`Holder ${safeTerminalValue(networkRecord.holder)}`);
|
|
692
|
+
const cidrs = boundedTerminalStrings(networkRecord.cidrs, MAX_LOOKUP_TERMINAL_CIDRS);
|
|
693
|
+
if (cidrs)
|
|
694
|
+
networkLines.push(`CIDR prefixes ${cidrs}`);
|
|
695
|
+
if (networkRecord.startAddress || networkRecord.endAddress)
|
|
696
|
+
networkLines.push(`Address range ${safeTerminalValue(networkRecord.startAddress)} to ${safeTerminalValue(networkRecord.endAddress)}`);
|
|
697
|
+
if (networkRecord.country)
|
|
698
|
+
networkLines.push(`Country ${safeTerminalValue(networkRecord.country)}`);
|
|
699
|
+
if (networkRecord.networkType)
|
|
700
|
+
networkLines.push(`Allocation ${safeTerminalValue(networkRecord.networkType)}`);
|
|
701
|
+
const allAbuseRoutes = Array.isArray(network.abuseRouting) ? network.abuseRouting : [];
|
|
702
|
+
const inspectedRoutes = Math.min(allAbuseRoutes.length, MAX_LOOKUP_TERMINAL_ABUSE_ROUTES);
|
|
703
|
+
const abuseRoutes = allAbuseRoutes.slice(0, inspectedRoutes).map(terminalRecord);
|
|
704
|
+
if (abuseRoutes.length) {
|
|
705
|
+
const channelCounts = new Map();
|
|
706
|
+
for (const route of abuseRoutes) {
|
|
707
|
+
const channel = ['email', 'phone'].includes(String(route.channel)) ? String(route.channel) : 'other';
|
|
708
|
+
channelCounts.set(channel, (channelCounts.get(channel) || 0) + 1);
|
|
709
|
+
}
|
|
710
|
+
const summary = [...channelCounts].map(([channel, count]) => `${channel} ${count}`).join(' · ');
|
|
711
|
+
const omittedRoutes = Math.max(0, allAbuseRoutes.length - inspectedRoutes);
|
|
712
|
+
networkLines.push(`Published routes ${abuseRoutes.length} retained${omittedRoutes ? ` · +${omittedRoutes} more entries` : ''} (${summary}); values omitted`);
|
|
713
|
+
}
|
|
714
|
+
if (detail === 'verbose' && networkRecord.databaseUpdatedAt)
|
|
715
|
+
networkLines.push(`Registry date ${safeTerminalValue(networkRecord.databaseUpdatedAt)}`);
|
|
716
|
+
if (detail === 'verbose' && Array.isArray(network.limitations)) {
|
|
717
|
+
for (const limitation of network.limitations.slice(0, MAX_LOOKUP_TERMINAL_LIMITATIONS))
|
|
718
|
+
networkLines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
719
|
+
const omitted = Math.max(0, network.limitations.length - MAX_LOOKUP_TERMINAL_LIMITATIONS);
|
|
720
|
+
if (omitted)
|
|
721
|
+
networkLines.push(`Limitation +${omitted} more retained limitation${omitted === 1 ? '' : 's'}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const sourceHealthLines = [];
|
|
726
|
+
const registryAccess = terminalRecord(diagnostics.registryAccess);
|
|
727
|
+
if (Object.keys(registryAccess).length) {
|
|
728
|
+
sourceHealthLines.push(`Registry access .${safeTerminalValue(registryAccess.suffix)}`);
|
|
729
|
+
sourceHealthLines.push(`WHOIS access ${registryAccessProfileLabel(registryAccess.whoisAccessProfile)}`);
|
|
730
|
+
sourceHealthLines.push(`RDAP access ${registryAccessProfileLabel(registryAccess.rdapAccessProfile)}`);
|
|
731
|
+
if (detail !== 'summary' && registryAccess.limitation)
|
|
732
|
+
sourceHealthLines.push(`Access note ${safeTerminalValue(registryAccess.limitation)}`);
|
|
733
|
+
}
|
|
734
|
+
const collectionLines = [];
|
|
735
|
+
if (detail === 'verbose') {
|
|
736
|
+
collectionLines.push(`Generated ${safeTerminalValue(document.generatedAt, 'unknown')}`);
|
|
737
|
+
const timing = terminalRecord(document.timing);
|
|
738
|
+
if (Number(timing.totalMs) >= 0)
|
|
739
|
+
collectionLines.push(`Total time ${safeTerminalValue(timing.totalMs)} ms`);
|
|
740
|
+
const sources = Array.isArray(timing.sources) ? timing.sources.map(terminalRecord) : [];
|
|
741
|
+
for (const source of sources.slice(0, 16)) {
|
|
742
|
+
collectionLines.push(`${safeTerminalValue(source.source, 'source')} ${titleCase(source.outcome)} · ${safeTerminalValue(source.durationMs, '0')} ms`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const lines = [];
|
|
746
|
+
appendSection(lines, 'Target', targetLines);
|
|
747
|
+
appendSection(lines, 'Registration', registrationLines);
|
|
748
|
+
appendSection(lines, 'DNS', dnsLines);
|
|
749
|
+
appendSection(lines, 'Mail', mailLines);
|
|
750
|
+
appendSection(lines, 'Website and security', websiteLines);
|
|
751
|
+
appendSection(lines, 'TLS and certificate', tlsLines);
|
|
752
|
+
appendSection(lines, 'Certificate warning data', certificateWarningLines);
|
|
753
|
+
appendSection(lines, 'Network', networkLines);
|
|
754
|
+
appendSection(lines, 'Source health', sourceHealthLines);
|
|
755
|
+
appendSection(lines, 'Collection', collectionLines);
|
|
756
|
+
return `${lines.join('\n')}\n`;
|
|
757
|
+
}
|
|
758
|
+
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, };
|