@slicedearth/whoisleuth-cli 1.32.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/LICENSE +661 -0
- package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
- package/NOTICE +76 -0
- package/README.md +84 -0
- package/TRADEMARKS.md +41 -0
- package/bin/whoisleuth.mjs +35 -0
- package/cli/archive-inspect.mjs +198 -0
- package/cli/arguments.mjs +1022 -0
- package/cli/artifact-verify.mjs +210 -0
- package/cli/bulk-checkpoint.mjs +218 -0
- package/cli/bulk-command-runner.mjs +109 -0
- package/cli/bulk-output.mjs +112 -0
- package/cli/bulk.mjs +133 -0
- package/cli/compare.mjs +238 -0
- package/cli/completion.mjs +175 -0
- package/cli/csv.mjs +1 -0
- package/cli/discover.mjs +50 -0
- package/cli/discovery-command-runner.mjs +36 -0
- package/cli/discovery-observation-snapshot.mjs +315 -0
- package/cli/discovery-scan-command-runner.mjs +141 -0
- package/cli/discovery-scan.mjs +325 -0
- package/cli/discovery-snapshot.mjs +133 -0
- package/cli/discovery-workflow.mjs +73 -0
- package/cli/doctor.mjs +137 -0
- package/cli/errors.mjs +21 -0
- package/cli/evidence-command-arguments.mjs +145 -0
- package/cli/evidence-command-runner.mjs +106 -0
- package/cli/evidence-signing.mjs +215 -0
- package/cli/exit-codes.mjs +9 -0
- package/cli/export-evidence.mjs +28 -0
- package/cli/formatters/evidence-report.mjs +3 -0
- package/cli/formatters/html.mjs +69 -0
- package/cli/formatters/json.mjs +135 -0
- package/cli/formatters/markdown.mjs +3 -0
- package/cli/formatters/terminal.mjs +658 -0
- package/cli/http.mjs +27 -0
- package/cli/lookup-command-runner.mjs +87 -0
- package/cli/lookup-diff.mjs +171 -0
- package/cli/mail-review.mjs +204 -0
- package/cli/manual.mjs +34 -0
- package/cli/network-command-runner.mjs +88 -0
- package/cli/output-file.mjs +97 -0
- package/cli/page-compare.mjs +123 -0
- package/cli/posture.mjs +20 -0
- package/cli/progress-events.mjs +53 -0
- package/cli/progress.mjs +56 -0
- package/cli/registry-access.mjs +3 -0
- package/cli/registry-support.mjs +120 -0
- package/cli/risk-calibration.mjs +294 -0
- package/cli/runner-types.mjs +1 -0
- package/cli/runner.mjs +726 -0
- package/cli/saved-lookup.mjs +87 -0
- package/cli/source-reliability.mjs +727 -0
- package/cli/strict-exit.mjs +55 -0
- package/cli/terminal-presentation.mjs +147 -0
- package/docs/cli.md +827 -0
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
- package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
- package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
- package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
- package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
- package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
- package/frontend/src/lib/analysis/bulk-sort.js +49 -0
- package/frontend/src/lib/analysis/campaign-model.js +265 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
- package/frontend/src/lib/analysis/case-migration-model.js +248 -0
- package/frontend/src/lib/analysis/case-model.js +5 -0
- package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
- package/frontend/src/lib/analysis/case-record-core.js +232 -0
- package/frontend/src/lib/analysis/case-record-model.js +7 -0
- package/frontend/src/lib/analysis/case-record-operations.js +224 -0
- package/frontend/src/lib/analysis/case-response-model.js +640 -0
- package/frontend/src/lib/analysis/case-response-packet.js +573 -0
- package/frontend/src/lib/analysis/case-storage-model.js +93 -0
- package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
- package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
- package/frontend/src/lib/analysis/http-summary.js +165 -0
- package/frontend/src/lib/analysis/investigation-guide.js +500 -0
- package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
- package/frontend/src/lib/analysis/page-baseline.js +272 -0
- package/frontend/src/lib/analysis/page-similarity.js +179 -0
- package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
- package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
- package/frontend/src/lib/analysis/scoring.js +135 -0
- package/frontend/src/lib/analysis/shortlist-model.js +209 -0
- package/frontend/src/lib/analysis/utils.js +265 -0
- package/frontend/src/lib/analysis/watchlist-history.js +643 -0
- package/frontend/src/lib/analysis/watchlist-store.js +127 -0
- package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
- package/frontend/src/lib/analysis/workspace-archive.js +472 -0
- package/lib/abort.mjs +26 -0
- package/lib/abusech-auth.mjs +19 -0
- package/lib/availability.mjs +716 -0
- package/lib/bounded-contract-normalizers.mjs +61 -0
- package/lib/browser-library-profile.mjs +310 -0
- package/lib/bulk-comparison-evidence.mjs +83 -0
- package/lib/classify.mjs +93 -0
- package/lib/client-behavior-profile.mjs +83 -0
- package/lib/credential-surface-profile.mjs +66 -0
- package/lib/csv.mjs +23 -0
- package/lib/ct-query.mjs +27 -0
- package/lib/ct-search.mjs +393 -0
- package/lib/distributed-operation-budget.mjs +461 -0
- package/lib/dns-delegation-health.mjs +344 -0
- package/lib/dns-intelligence.mjs +739 -0
- package/lib/dns-mx.mjs +24 -0
- package/lib/dns-resolver-selection.mjs +33 -0
- package/lib/domain-posture-analysis.mjs +254 -0
- package/lib/domain-posture-parsers.mjs +358 -0
- package/lib/domain-posture.mjs +745 -0
- package/lib/error-detail.mjs +10 -0
- package/lib/evidence-export.mjs +431 -0
- package/lib/evidence-report-markdown.mjs +71 -0
- package/lib/evidence-report.mjs +391 -0
- package/lib/external-intelligence-risk.mjs +108 -0
- package/lib/favicon.mjs +146 -0
- package/lib/feature-policy.mjs +70 -0
- package/lib/generated/retire-browser-catalog.mjs +8724 -0
- package/lib/generated/unicode-confusables-17.mjs +93 -0
- package/lib/hostname.mjs +38 -0
- package/lib/html-signals.mjs +680 -0
- package/lib/http-intelligence.mjs +232 -0
- package/lib/idn-confusable-policy.mjs +86 -0
- package/lib/idn-confusables.mjs +465 -0
- package/lib/lookup-cache.mjs +130 -0
- package/lib/lookup-progress.mjs +291 -0
- package/lib/lookup-source-progress.mjs +109 -0
- package/lib/lookup-threat-provider-inventory.mjs +13 -0
- package/lib/lookup.mjs +592 -0
- package/lib/observation.mjs +114 -0
- package/lib/observed-network-context.mjs +355 -0
- package/lib/operation-budget.mjs +489 -0
- package/lib/page-fingerprints.mjs +481 -0
- package/lib/page-role-profile.mjs +117 -0
- package/lib/perceptual-hash.mjs +451 -0
- package/lib/rdap-attempts.mjs +34 -0
- package/lib/rdap-bootstrap.mjs +173 -0
- package/lib/rdap-capabilities.mjs +292 -0
- package/lib/rdap-client.mjs +125 -0
- package/lib/rdap-normalization.mjs +691 -0
- package/lib/rdap-registrar.mjs +196 -0
- package/lib/rdap-transport.mjs +39 -0
- package/lib/rdap-types.mjs +1 -0
- package/lib/rdap-validation.mjs +94 -0
- package/lib/rdap.mjs +16 -0
- package/lib/registry-access.mjs +32 -0
- package/lib/registry-capabilities.mjs +117 -0
- package/lib/registry-capability-catalogue.mjs +1206 -0
- package/lib/registry-comparison.mjs +318 -0
- package/lib/registry-dates.mjs +190 -0
- package/lib/registry-insights.mjs +325 -0
- package/lib/response-policy.mjs +256 -0
- package/lib/risk-scoring.mjs +164 -0
- package/lib/safe-fetch.mjs +446 -0
- package/lib/security-txt.mjs +359 -0
- package/lib/service-binding-dns.mjs +608 -0
- package/lib/sslbl-certificates.generated.mjs +143 -0
- package/lib/sslbl-intelligence.mjs +241 -0
- package/lib/static-html-analysis.mjs +340 -0
- package/lib/structured-data-identity.mjs +323 -0
- package/lib/threat-intelligence-contract.mjs +5 -0
- package/lib/threat-intelligence-definition-registry.mjs +34 -0
- package/lib/threat-intelligence-fixtures.mjs +82 -0
- package/lib/threat-intelligence-manifests.mjs +512 -0
- package/lib/threat-intelligence-runtime.mjs +662 -0
- package/lib/threat-intelligence-targets.mjs +168 -0
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +307 -0
- package/lib/tls-intelligence.mjs +814 -0
- package/lib/typosquat-generator.mjs +884 -0
- package/lib/urlhaus-intelligence.mjs +361 -0
- package/lib/urlscan-intelligence.mjs +349 -0
- package/lib/website-security-posture.mjs +325 -0
- package/lib/website-technology.mjs +437 -0
- package/lib/whois-authority.mjs +98 -0
- package/lib/whois-chain.mjs +261 -0
- package/lib/whois-contacts.mjs +163 -0
- package/lib/whois-contracts.mjs +3 -0
- package/lib/whois-normalization.mjs +27 -0
- package/lib/whois-transport.mjs +100 -0
- package/lib/whois-values.mjs +92 -0
- package/lib/whois.mjs +1187 -0
- package/package.json +55 -0
- package/third-party-notices.txt +5571 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { registryAccessProfileLabel } from "../registry-access.mjs";
|
|
2
|
+
import { unicodeDomainFromAscii } from "../../lib/idn-confusables.mjs";
|
|
3
|
+
const MAX_TERMINAL_VALUE_LENGTH = 240;
|
|
4
|
+
const MAX_CT_TERMINAL_MATCHES = 100;
|
|
5
|
+
const MAX_CT_TERMINAL_HOSTNAMES = 5;
|
|
6
|
+
const MAX_DISCOVER_TERMINAL_CANDIDATES = 200;
|
|
7
|
+
const MAX_POSTURE_TERMINAL_RECORDS = 5;
|
|
8
|
+
const MAX_TLS_TERMINAL_ALT_NAMES = 10;
|
|
9
|
+
const MAX_TLS_TERMINAL_PURPOSES = 8;
|
|
10
|
+
const MAX_RISK_CALIBRATION_TERMINAL_RECORDS = 100;
|
|
11
|
+
function safeTerminalValue(value, fallback = '—') {
|
|
12
|
+
if (value === null || value === undefined || value === '')
|
|
13
|
+
return fallback;
|
|
14
|
+
const normalized = String(value)
|
|
15
|
+
.replace(/[\x00-\x1f\x7f]+/g, ' ')
|
|
16
|
+
.replace(/\s+/g, ' ')
|
|
17
|
+
.trim()
|
|
18
|
+
.slice(0, MAX_TERMINAL_VALUE_LENGTH);
|
|
19
|
+
return normalized || fallback;
|
|
20
|
+
}
|
|
21
|
+
function titleCase(value) {
|
|
22
|
+
const text = safeTerminalValue(value, 'unknown').replaceAll('_', ' ');
|
|
23
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
24
|
+
}
|
|
25
|
+
function terminalRecord(value) {
|
|
26
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
27
|
+
? value
|
|
28
|
+
: {};
|
|
29
|
+
}
|
|
30
|
+
function terminalCount(value) {
|
|
31
|
+
const count = Number(value);
|
|
32
|
+
return Number.isSafeInteger(count) && count >= 0 ? Math.min(count, 999) : 0;
|
|
33
|
+
}
|
|
34
|
+
function terminalCountSummary(value, labels) {
|
|
35
|
+
const source = terminalRecord(value);
|
|
36
|
+
return labels
|
|
37
|
+
.map(([key, label]) => [label, terminalCount(source[key])])
|
|
38
|
+
.filter(([, count]) => count > 0)
|
|
39
|
+
.map(([label, count]) => `${label} ${count}`)
|
|
40
|
+
.join(', ');
|
|
41
|
+
}
|
|
42
|
+
function appendSection(lines, label, values) {
|
|
43
|
+
if (!values.length)
|
|
44
|
+
return;
|
|
45
|
+
if (lines.length)
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(`${label}:`, ...values);
|
|
48
|
+
}
|
|
49
|
+
function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
50
|
+
const availability = terminalRecord(document.availability);
|
|
51
|
+
const diagnostics = terminalRecord(document.diagnostics);
|
|
52
|
+
const rdapDiagnostics = terminalRecord(diagnostics.rdap);
|
|
53
|
+
const whoisDiagnostics = terminalRecord(diagnostics.whois);
|
|
54
|
+
const targetLines = [
|
|
55
|
+
`Query ${safeTerminalValue(document.query)}`,
|
|
56
|
+
`Type ${safeTerminalValue(document.type)}`,
|
|
57
|
+
`Mode ${titleCase(document.mode)}`,
|
|
58
|
+
];
|
|
59
|
+
if (document.inputHostname && document.inputHostname !== document.registrableDomain) {
|
|
60
|
+
targetLines.push(`Input host ${safeTerminalValue(document.inputHostname)}`);
|
|
61
|
+
targetLines.push(`Registry query ${safeTerminalValue(document.registrableDomain)}`);
|
|
62
|
+
}
|
|
63
|
+
if (availability.applicable) {
|
|
64
|
+
targetLines.push(`Availability ${titleCase(availability.state)}`);
|
|
65
|
+
targetLines.push(`Confidence ${titleCase(availability.confidence)}`);
|
|
66
|
+
}
|
|
67
|
+
const registrationLines = [
|
|
68
|
+
`RDAP ${titleCase(rdapDiagnostics.status)}`,
|
|
69
|
+
`WHOIS ${titleCase(whoisDiagnostics.status)}`,
|
|
70
|
+
];
|
|
71
|
+
if (detail !== 'summary' && rdapDiagnostics.endpoint) {
|
|
72
|
+
registrationLines.splice(1, 0, `RDAP source ${safeTerminalValue(rdapDiagnostics.endpoint)}`);
|
|
73
|
+
}
|
|
74
|
+
const registrarRdap = terminalRecord(rdapDiagnostics.registrar);
|
|
75
|
+
if (Object.keys(registrarRdap).length) {
|
|
76
|
+
registrationLines.push(`Registrar RDAP ${titleCase(registrarRdap.status)}`);
|
|
77
|
+
if (detail !== 'summary' && registrarRdap.endpoint) {
|
|
78
|
+
registrationLines.push(`Registrar source ${safeTerminalValue(registrarRdap.endpoint)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const registryInsights = terminalRecord(document.registryInsights);
|
|
82
|
+
if (detail !== 'summary' && registryInsights.version === 1) {
|
|
83
|
+
const lifecycle = terminalRecord(registryInsights.lifecycle);
|
|
84
|
+
const disclosure = terminalRecord(registryInsights.contactDisclosure);
|
|
85
|
+
const registryDisclosure = terminalRecord(disclosure.registryRdap);
|
|
86
|
+
const whoisDisclosure = terminalRecord(disclosure.whois);
|
|
87
|
+
const reconciliation = terminalRecord(registryInsights.reconciliation);
|
|
88
|
+
const publications = Array.isArray(registryInsights.publications)
|
|
89
|
+
? registryInsights.publications.map(terminalRecord)
|
|
90
|
+
: [];
|
|
91
|
+
const publicationCounts = {
|
|
92
|
+
complete: publications.filter((item) => item.state === 'complete').length,
|
|
93
|
+
partial: publications.filter((item) => item.state === 'partial').length,
|
|
94
|
+
unavailable: publications.filter((item) => item.state === 'unavailable').length,
|
|
95
|
+
};
|
|
96
|
+
registrationLines.push(`Lifecycle ${titleCase(lifecycle.label)}`);
|
|
97
|
+
registrationLines.push(`Disclosure RDAP ${titleCase(registryDisclosure.state)} · WHOIS ${titleCase(whoisDisclosure.state)}`);
|
|
98
|
+
registrationLines.push(`Reconciliation ${titleCase(reconciliation.state)}`);
|
|
99
|
+
registrationLines.push(`Publications ${publicationCounts.complete} complete · ${publicationCounts.partial} partial · ${publicationCounts.unavailable} unavailable`);
|
|
100
|
+
}
|
|
101
|
+
const websiteLines = [];
|
|
102
|
+
if (document.mode === 'deep' && document.type === 'domain') {
|
|
103
|
+
const dns = terminalRecord(availability.dns);
|
|
104
|
+
const http = terminalRecord(availability.http);
|
|
105
|
+
const httpResponse = terminalRecord(http.response);
|
|
106
|
+
const tls = terminalRecord(availability.tls);
|
|
107
|
+
const credentialSurface = terminalRecord(availability.credentialSurfaceProfile);
|
|
108
|
+
const structuredIdentity = terminalRecord(availability.structuredDataIdentity);
|
|
109
|
+
const technology = terminalRecord(availability.technologyProfile);
|
|
110
|
+
const browserLibraries = terminalRecord(technology.browserLibraryProfile);
|
|
111
|
+
const posture = terminalRecord(availability.securityPosture);
|
|
112
|
+
const postureSummary = terminalRecord(posture.summary);
|
|
113
|
+
if (availability.activityStatus)
|
|
114
|
+
websiteLines.push(`Web activity ${titleCase(availability.activityStatus)}`);
|
|
115
|
+
if (detail !== 'summary' && availability.pageTitle)
|
|
116
|
+
websiteLines.push(`Page title ${safeTerminalValue(availability.pageTitle)}`);
|
|
117
|
+
if (dns.status)
|
|
118
|
+
websiteLines.push(`DNS evidence ${titleCase(dns.status)}`);
|
|
119
|
+
if (http.status) {
|
|
120
|
+
websiteLines.push(`HTTP evidence ${titleCase(http.status)}`);
|
|
121
|
+
const responseDetail = [
|
|
122
|
+
httpResponse.status ? `HTTP ${safeTerminalValue(httpResponse.status)}` : null,
|
|
123
|
+
http.transportSecurity ? safeTerminalValue(http.transportSecurity).toUpperCase() : null,
|
|
124
|
+
].filter(Boolean).join(' · ');
|
|
125
|
+
if (detail !== 'summary' && responseDetail)
|
|
126
|
+
websiteLines.push(`HTTP response ${responseDetail}`);
|
|
127
|
+
}
|
|
128
|
+
if (tls.status) {
|
|
129
|
+
websiteLines.push(`TLS evidence ${titleCase(tls.status)}`);
|
|
130
|
+
if (detail !== 'summary' && tls.protocol)
|
|
131
|
+
websiteLines.push(`TLS protocol ${safeTerminalValue(tls.protocol)}`);
|
|
132
|
+
}
|
|
133
|
+
if (credentialSurface.status || credentialSurface.source === 'html') {
|
|
134
|
+
const forms = terminalRecord(credentialSurface.forms);
|
|
135
|
+
const inputs = terminalRecord(credentialSurface.inputs);
|
|
136
|
+
const categories = terminalRecord(inputs.categories);
|
|
137
|
+
const actions = terminalRecord(forms.actions);
|
|
138
|
+
const formCount = terminalCount(forms.count);
|
|
139
|
+
const inputCount = terminalCount(inputs.count);
|
|
140
|
+
const externalActionCount = terminalCount(actions.external);
|
|
141
|
+
websiteLines.push(`Credential UI ${titleCase(credentialSurface.status)} · ${safeTerminalValue(inputs.classifiedCount, '0')} classified input${Number(inputs.classifiedCount) === 1 ? '' : 's'}`);
|
|
142
|
+
if (detail !== 'summary')
|
|
143
|
+
websiteLines.push(`Form surface ${safeTerminalValue(formCount)} form${formCount === 1 ? '' : 's'} · ${safeTerminalValue(inputCount)} input${inputCount === 1 ? '' : 's'} · ${safeTerminalValue(externalActionCount)} external action${externalActionCount === 1 ? '' : 's'}`);
|
|
144
|
+
const visible = [
|
|
145
|
+
['password', categories.password],
|
|
146
|
+
['email', categories.email],
|
|
147
|
+
['username', categories.username],
|
|
148
|
+
['one-time code', categories.one_time_code],
|
|
149
|
+
['payment related', categories.payment],
|
|
150
|
+
].filter(([, count]) => Number(count) > 0).map(([label, count]) => `${safeTerminalValue(label)} ${safeTerminalValue(count)}`);
|
|
151
|
+
if (detail !== 'summary' && visible.length)
|
|
152
|
+
websiteLines.push(`Input purposes ${safeTerminalValue(visible.join(' · '))}`);
|
|
153
|
+
}
|
|
154
|
+
if (structuredIdentity.status || structuredIdentity.source === 'html') {
|
|
155
|
+
const entities = Array.isArray(structuredIdentity.entities) ? structuredIdentity.entities : [];
|
|
156
|
+
websiteLines.push(`Structured ID ${titleCase(structuredIdentity.status)} · ${entities.length} declared entit${entities.length === 1 ? 'y' : 'ies'}`);
|
|
157
|
+
const visible = entities.slice(0, 4).map((entity) => {
|
|
158
|
+
const item = terminalRecord(entity);
|
|
159
|
+
const types = Array.isArray(item.types) ? item.types.slice(0, 3).map((value) => safeTerminalValue(value)).join('/') : '';
|
|
160
|
+
return `${safeTerminalValue(item.name, 'Unnamed declaration')}${types ? ` (${types})` : ''}`;
|
|
161
|
+
});
|
|
162
|
+
if (detail !== 'summary' && visible.length)
|
|
163
|
+
websiteLines.push(`Declarations ${safeTerminalValue(visible.join('; '))}`);
|
|
164
|
+
}
|
|
165
|
+
if (technology.status || technology.source === 'derived') {
|
|
166
|
+
const findings = Array.isArray(technology.findings) ? technology.findings : [];
|
|
167
|
+
websiteLines.push(`Technology ${titleCase(technology.status)} · ${findings.length} indicator${findings.length === 1 ? '' : 's'}`);
|
|
168
|
+
const visible = findings.slice(0, 6).map((finding) => {
|
|
169
|
+
const item = terminalRecord(finding);
|
|
170
|
+
const qualifiers = [item.category, item.confidence].filter(Boolean).map((value) => safeTerminalValue(value));
|
|
171
|
+
return `${safeTerminalValue(item.name, 'Unnamed indicator')}${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`;
|
|
172
|
+
});
|
|
173
|
+
if (detail !== 'summary' && visible.length) {
|
|
174
|
+
const omitted = findings.length - visible.length;
|
|
175
|
+
websiteLines.push(`Indicators ${safeTerminalValue(`${visible.join('; ')}${omitted > 0 ? `; +${omitted} more` : ''}`)}`);
|
|
176
|
+
}
|
|
177
|
+
if (detail !== 'summary' && (browserLibraries.profileVersion === 1 || browserLibraries.source === 'derived')) {
|
|
178
|
+
const libraries = Array.isArray(browserLibraries.findings) ? browserLibraries.findings : [];
|
|
179
|
+
const advisoryMatches = libraries.filter((finding) => terminalCount(terminalRecord(finding).advisoryCount) > 0).length;
|
|
180
|
+
websiteLines.push(`JS libraries ${titleCase(browserLibraries.status)} · ${libraries.length} apparent · `
|
|
181
|
+
+ `${advisoryMatches} with catalogue advisory match${advisoryMatches === 1 ? '' : 'es'}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (posture.status || posture.source === 'derived') {
|
|
185
|
+
websiteLines.push(`Posture ${titleCase(posture.status)}`);
|
|
186
|
+
if (detail !== 'summary')
|
|
187
|
+
websiteLines.push(`Posture counts ${terminalCount(postureSummary.observed)} observed · `
|
|
188
|
+
+ `${terminalCount(postureSummary.potentialExposure)} potential exposure · `
|
|
189
|
+
+ `${terminalCount(postureSummary.observedAbsence)} observed absence · `
|
|
190
|
+
+ `${terminalCount(postureSummary.unavailable)} unavailable`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const networkLines = [];
|
|
194
|
+
if (document.mode === 'deep' && (document.type === 'ipv4' || document.type === 'ipv6')) {
|
|
195
|
+
const reverseDns = terminalRecord(document.reverseDns);
|
|
196
|
+
const reverseDnsRecords = terminalRecord(reverseDns.records);
|
|
197
|
+
const ptrNames = Array.isArray(reverseDnsRecords.ptr)
|
|
198
|
+
? reverseDnsRecords.ptr.slice(0, 5).map((value) => safeTerminalValue(value))
|
|
199
|
+
: [];
|
|
200
|
+
if (reverseDns.status)
|
|
201
|
+
networkLines.push(`Reverse DNS ${titleCase(reverseDns.status)}`);
|
|
202
|
+
if (detail !== 'summary' && ptrNames.length)
|
|
203
|
+
networkLines.push(`PTR names ${safeTerminalValue(ptrNames.join(', '))}`);
|
|
204
|
+
}
|
|
205
|
+
const network = terminalRecord(document.networkContext);
|
|
206
|
+
if (network.contextVersion === 1) {
|
|
207
|
+
const endpoint = terminalRecord(network.endpoint);
|
|
208
|
+
const networkRecord = terminalRecord(network.network);
|
|
209
|
+
networkLines.push(`Network RDAP ${titleCase(network.status)}`);
|
|
210
|
+
if (detail !== 'summary' && endpoint.address)
|
|
211
|
+
networkLines.push(`Selected IP ${safeTerminalValue(endpoint.address)}`);
|
|
212
|
+
if (networkRecord.name || networkRecord.holder) {
|
|
213
|
+
networkLines.push(`Network ${safeTerminalValue(networkRecord.name || networkRecord.holder)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const sourceHealthLines = [];
|
|
217
|
+
const registryAccess = terminalRecord(diagnostics.registryAccess);
|
|
218
|
+
if (Object.keys(registryAccess).length) {
|
|
219
|
+
sourceHealthLines.push(`Registry access .${safeTerminalValue(registryAccess.suffix)}`);
|
|
220
|
+
sourceHealthLines.push(`WHOIS access ${registryAccessProfileLabel(registryAccess.whoisAccessProfile)}`);
|
|
221
|
+
sourceHealthLines.push(`RDAP access ${registryAccessProfileLabel(registryAccess.rdapAccessProfile)}`);
|
|
222
|
+
if (detail !== 'summary' && registryAccess.limitation)
|
|
223
|
+
sourceHealthLines.push(`Access note ${safeTerminalValue(registryAccess.limitation)}`);
|
|
224
|
+
}
|
|
225
|
+
const collectionLines = [];
|
|
226
|
+
if (detail === 'verbose') {
|
|
227
|
+
collectionLines.push(`Generated ${safeTerminalValue(document.generatedAt, 'unknown')}`);
|
|
228
|
+
const timing = terminalRecord(document.timing);
|
|
229
|
+
if (Number(timing.totalMs) >= 0)
|
|
230
|
+
collectionLines.push(`Total time ${safeTerminalValue(timing.totalMs)} ms`);
|
|
231
|
+
const sources = Array.isArray(timing.sources) ? timing.sources.map(terminalRecord) : [];
|
|
232
|
+
for (const source of sources.slice(0, 16)) {
|
|
233
|
+
collectionLines.push(`${safeTerminalValue(source.source, 'source')} ${titleCase(source.outcome)} · ${safeTerminalValue(source.durationMs, '0')} ms`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const lines = [];
|
|
237
|
+
appendSection(lines, 'Target', targetLines);
|
|
238
|
+
appendSection(lines, 'Registration', registrationLines);
|
|
239
|
+
appendSection(lines, 'Website and security', websiteLines);
|
|
240
|
+
appendSection(lines, 'Network', networkLines);
|
|
241
|
+
appendSection(lines, 'Source health', sourceHealthLines);
|
|
242
|
+
appendSection(lines, 'Collection', collectionLines);
|
|
243
|
+
return `${lines.join('\n')}\n`;
|
|
244
|
+
}
|
|
245
|
+
function formatTerminalRegistrySupport(document) {
|
|
246
|
+
const profile = terminalRecord(document.profile);
|
|
247
|
+
const rdap = terminalRecord(profile.rdap);
|
|
248
|
+
const whois = terminalRecord(profile.whois);
|
|
249
|
+
const verification = terminalRecord(document.verification);
|
|
250
|
+
const fixtures = Array.isArray(verification.fixtureScenarios) ? verification.fixtureScenarios : [];
|
|
251
|
+
const files = Array.isArray(verification.files) ? verification.files : [];
|
|
252
|
+
const documentation = Array.isArray(verification.documentationUrls) ? verification.documentationUrls : [];
|
|
253
|
+
const standards = terminalRecord(document.standardsCoverage);
|
|
254
|
+
const genericCoverage = terminalRecord(standards.genericAndRestricted);
|
|
255
|
+
const supportLabel = (value) => titleCase(safeTerminalValue(value, 'unknown').replaceAll('-', '_'));
|
|
256
|
+
const lines = [
|
|
257
|
+
`Input ${safeTerminalValue(document.requestedInput)}`,
|
|
258
|
+
`Suffix .${safeTerminalValue(document.suffix)}`,
|
|
259
|
+
`Catalogue Version ${safeTerminalValue(document.catalogueVersion, '0')}`,
|
|
260
|
+
`gTLD RDAP ${safeTerminalValue(genericCoverage.rdapCovered, '0')} / ${safeTerminalValue(genericCoverage.total, '0')}`,
|
|
261
|
+
`Coverage date ${safeTerminalValue(standards.verifiedAt, 'unknown')}`,
|
|
262
|
+
`Profile ${profile.explicitSuffixProfile ? 'Explicit suffix profile' : 'Generic IANA discovery profile'}`,
|
|
263
|
+
`Profile ID ${safeTerminalValue(profile.id)}`,
|
|
264
|
+
`Registry class ${supportLabel(profile.registryClass)}`,
|
|
265
|
+
`Coverage ${supportLabel(profile.coverageState)}`,
|
|
266
|
+
`RDAP discovery ${registryAccessProfileLabel(rdap.discovery)}`,
|
|
267
|
+
`RDAP access ${registryAccessProfileLabel(rdap.accessProfile)}`,
|
|
268
|
+
`WHOIS discovery ${registryAccessProfileLabel(whois.discovery)}`,
|
|
269
|
+
`WHOIS access ${registryAccessProfileLabel(whois.accessProfile)}`,
|
|
270
|
+
`WHOIS query ${supportLabel(whois.queryProfile)}`,
|
|
271
|
+
`WHOIS scope ${supportLabel(whois.queryScope)}`,
|
|
272
|
+
`WHOIS encoding ${supportLabel(whois.encodingProfile)}`,
|
|
273
|
+
`WHOIS parser ${supportLabel(whois.parserProfile)}`,
|
|
274
|
+
`Fallback ${profile.fallbackProfile ? supportLabel(profile.fallbackProfile) : 'None'}`,
|
|
275
|
+
`Fixture states ${fixtures.length ? fixtures.map((value) => supportLabel(value)).join(', ') : 'None documented'}`,
|
|
276
|
+
];
|
|
277
|
+
for (const file of files)
|
|
278
|
+
lines.push(`Verified by ${safeTerminalValue(file)}`);
|
|
279
|
+
for (const url of documentation)
|
|
280
|
+
lines.push(`Documentation ${safeTerminalValue(url)}`);
|
|
281
|
+
lines.push(`Limitation ${safeTerminalValue(document.limitation)}`, '', safeTerminalValue(terminalRecord(document.interpretation).statement));
|
|
282
|
+
return `${lines.join('\n')}\n`;
|
|
283
|
+
}
|
|
284
|
+
function formatTerminalBulk(items, metadata) {
|
|
285
|
+
const lines = items.map((item) => {
|
|
286
|
+
if (!item.ok)
|
|
287
|
+
return `! ${safeTerminalValue(item.query)} — ${safeTerminalValue(item.error, 'Lookup failed')}`;
|
|
288
|
+
const result = terminalRecord(item.result);
|
|
289
|
+
const availability = terminalRecord(result.availability);
|
|
290
|
+
const state = titleCase(availability.state);
|
|
291
|
+
const confidence = titleCase(availability.confidence);
|
|
292
|
+
return `✓ ${safeTerminalValue(item.query)} — ${state} (${confidence} confidence)`;
|
|
293
|
+
});
|
|
294
|
+
const succeeded = items.filter((item) => item.ok).length;
|
|
295
|
+
lines.push('');
|
|
296
|
+
const collected = metadata.collectedTotal ?? items.length;
|
|
297
|
+
const filter = metadata.filter && metadata.filter !== 'all'
|
|
298
|
+
? ` · ${items.length} matched ${metadata.filter}`
|
|
299
|
+
: '';
|
|
300
|
+
lines.push(`${collected} collected${filter} · ${succeeded} succeeded · ${items.length - succeeded} failed in output · ${metadata.duplicates || 0} duplicates removed`);
|
|
301
|
+
return `${lines.join('\n')}\n`;
|
|
302
|
+
}
|
|
303
|
+
function formatTerminalCtSearch(document) {
|
|
304
|
+
const matches = Array.isArray(document.matches) ? document.matches : [];
|
|
305
|
+
const visible = matches.slice(0, MAX_CT_TERMINAL_MATCHES);
|
|
306
|
+
const observation = terminalRecord(document.observation);
|
|
307
|
+
const lines = [
|
|
308
|
+
`Keyword ${safeTerminalValue(document.keyword)}`,
|
|
309
|
+
`CT status ${titleCase(observation.status || (document.truncated ? 'partial' : 'success'))}`,
|
|
310
|
+
`Certificates ${safeTerminalValue(document.certCount, '0')}`,
|
|
311
|
+
`Observed hosts ${safeTerminalValue(Array.isArray(document.domains) ? document.domains.length : 0, '0')}`,
|
|
312
|
+
`Matches ${safeTerminalValue(matches.length, '0')}`,
|
|
313
|
+
`Truncated ${document.truncated ? 'Yes' : 'No'}`,
|
|
314
|
+
'',
|
|
315
|
+
];
|
|
316
|
+
if (!visible.length) {
|
|
317
|
+
lines.push('No structured registrable-domain matches.');
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
for (const value of visible) {
|
|
321
|
+
const match = terminalRecord(value);
|
|
322
|
+
const hostnames = Array.isArray(match.hostnames) ? match.hostnames : [];
|
|
323
|
+
const shownHosts = hostnames.slice(0, MAX_CT_TERMINAL_HOSTNAMES).map((value) => safeTerminalValue(value));
|
|
324
|
+
const omitted = hostnames.length - shownHosts.length;
|
|
325
|
+
lines.push(safeTerminalValue(match.domain));
|
|
326
|
+
lines.push(` Certificates ${safeTerminalValue(match.certificateCount, '0')}`);
|
|
327
|
+
lines.push(` Hostnames ${shownHosts.join(', ')}${omitted > 0 ? ` (+${omitted} more)` : ''}`);
|
|
328
|
+
lines.push(` Observed ${safeTerminalValue(match.firstObservedAt)} → ${safeTerminalValue(match.lastObservedAt)}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (matches.length > visible.length) {
|
|
332
|
+
lines.push('', `Showing ${visible.length} of ${matches.length} structured matches in terminal output; use --json for the complete bounded result.`);
|
|
333
|
+
}
|
|
334
|
+
return `${lines.join('\n')}\n`;
|
|
335
|
+
}
|
|
336
|
+
function formatTerminalDiscover(document, mutationLabels = {}) {
|
|
337
|
+
const candidates = Array.isArray(document.candidates) ? document.candidates : [];
|
|
338
|
+
const visible = candidates.slice(0, MAX_DISCOVER_TERMINAL_CANDIDATES);
|
|
339
|
+
const advanced = terminalRecord(document.advancedConfusable);
|
|
340
|
+
const snapshot = terminalRecord(document.snapshot);
|
|
341
|
+
const lines = [
|
|
342
|
+
`Seed ${safeTerminalValue(document.seed)}`,
|
|
343
|
+
`Preset ${safeTerminalValue(document.preset)}`,
|
|
344
|
+
`Keyboard ${safeTerminalValue(document.keyboardLayout)}`,
|
|
345
|
+
`TLDs ${(Array.isArray(document.tlds) ? document.tlds : []).map((value) => safeTerminalValue(value)).join(', ')}`,
|
|
346
|
+
`Families ${(Array.isArray(document.mutationFamilies) && document.mutationFamilies.length) ? document.mutationFamilies.map((value) => safeTerminalValue(value)).join(', ') : 'Preset defaults'}`,
|
|
347
|
+
`Dictionary ${safeTerminalValue(document.dictionaryTermCount, '0')} accepted custom terms${Number(document.rejectedDictionaryTermCount) > 0 ? `, ${safeTerminalValue(document.rejectedDictionaryTermCount)} rejected` : ''}`,
|
|
348
|
+
`Candidates ${safeTerminalValue(candidates.length, '0')}`,
|
|
349
|
+
`Truncated ${document.truncated ? 'Yes' : 'No'}`,
|
|
350
|
+
];
|
|
351
|
+
if (Object.keys(advanced).length) {
|
|
352
|
+
lines.push(`Advanced IDN ${safeTerminalValue(advanced.generated, '0')} generated, ${safeTerminalValue(advanced.omittedByPolicy, '0')} policy-omitted, ${safeTerminalValue(advanced.omittedByBudget, '0')} budget-omitted`);
|
|
353
|
+
}
|
|
354
|
+
if (Object.keys(snapshot).length) {
|
|
355
|
+
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`}`);
|
|
356
|
+
}
|
|
357
|
+
lines.push('');
|
|
358
|
+
for (const value of visible) {
|
|
359
|
+
const candidate = terminalRecord(value);
|
|
360
|
+
const labels = (Array.isArray(candidate.mutationTypes) ? candidate.mutationTypes : [])
|
|
361
|
+
.map((mutationType) => {
|
|
362
|
+
const value = safeTerminalValue(mutationType);
|
|
363
|
+
return safeTerminalValue(mutationLabels[value] || value);
|
|
364
|
+
});
|
|
365
|
+
const candidateDomain = safeTerminalValue(candidate.domain, '');
|
|
366
|
+
const unicodeDomain = unicodeDomainFromAscii(candidateDomain);
|
|
367
|
+
const unicodeDetail = unicodeDomain && unicodeDomain !== candidateDomain
|
|
368
|
+
? ` [Unicode: ${safeTerminalValue(unicodeDomain)}]`
|
|
369
|
+
: '';
|
|
370
|
+
lines.push(`${safeTerminalValue(candidateDomain)}${unicodeDetail} — ${labels.join(', ') || 'Generated variant'}`);
|
|
371
|
+
}
|
|
372
|
+
if (!visible.length)
|
|
373
|
+
lines.push('No candidates were generated.');
|
|
374
|
+
if (candidates.length > visible.length) {
|
|
375
|
+
lines.push('', `Showing ${visible.length} of ${candidates.length} candidates in terminal output; use --json or --jsonl for the complete bounded result.`);
|
|
376
|
+
}
|
|
377
|
+
return `${lines.join('\n')}\n`;
|
|
378
|
+
}
|
|
379
|
+
function formatTerminalPosture(document) {
|
|
380
|
+
const summary = terminalRecord(document.summary);
|
|
381
|
+
const spfExpansion = terminalRecord(document.spfExpansion);
|
|
382
|
+
const selectors = Array.isArray(document.dkimSelectors) ? document.dkimSelectors : [];
|
|
383
|
+
const checks = Array.isArray(document.checks) ? document.checks : [];
|
|
384
|
+
const dependencies = Array.isArray(document.externalDependencies) ? document.externalDependencies : [];
|
|
385
|
+
const dmarcAuthorizations = Array.isArray(document.dmarcAuthorizations) ? document.dmarcAuthorizations : [];
|
|
386
|
+
const lines = [
|
|
387
|
+
`Domain ${safeTerminalValue(document.domain)}`,
|
|
388
|
+
`Checked ${safeTerminalValue(document.checkedAt)}`,
|
|
389
|
+
`DKIM selectors ${selectors.length ? selectors.map((value) => safeTerminalValue(value)).join(', ') : 'None supplied'}`,
|
|
390
|
+
`Summary ${safeTerminalValue(summary.danger, '0')} action · ${safeTerminalValue(summary.warning, '0')} review · ${safeTerminalValue(summary.pass, '0')} pass · ${safeTerminalValue(summary.info, '0')} info`,
|
|
391
|
+
...(Object.keys(spfExpansion).length ? [
|
|
392
|
+
`SPF expansion ${safeTerminalValue(spfExpansion.state)} · ${safeTerminalValue(spfExpansion.lookupsUsed, '0')}/${safeTerminalValue(spfExpansion.lookupLimit, '0')} policy queries · ${safeTerminalValue(spfExpansion.dnsLookupTerms, '0')} DNS terms`,
|
|
393
|
+
] : []),
|
|
394
|
+
`Dependencies ${dependencies.length} observed · ${dmarcAuthorizations.length} DMARC reporting destination${dmarcAuthorizations.length === 1 ? '' : 's'} checked`,
|
|
395
|
+
'',
|
|
396
|
+
];
|
|
397
|
+
for (const value of checks) {
|
|
398
|
+
const item = terminalRecord(value);
|
|
399
|
+
lines.push(`[${safeTerminalValue(item.status, 'info').toUpperCase()}] ${safeTerminalValue(item.label)} — ${safeTerminalValue(item.summary)}`);
|
|
400
|
+
if (item.detail)
|
|
401
|
+
lines.push(` Detail ${safeTerminalValue(item.detail)}`);
|
|
402
|
+
if (item.remediation)
|
|
403
|
+
lines.push(` Next ${safeTerminalValue(item.remediation)}`);
|
|
404
|
+
const records = Array.isArray(item.records) ? item.records : [];
|
|
405
|
+
for (const record of records.slice(0, MAX_POSTURE_TERMINAL_RECORDS)) {
|
|
406
|
+
lines.push(` Record ${safeTerminalValue(record)}`);
|
|
407
|
+
}
|
|
408
|
+
if (records.length > MAX_POSTURE_TERMINAL_RECORDS) {
|
|
409
|
+
lines.push(` Records ${records.length - MAX_POSTURE_TERMINAL_RECORDS} more omitted from terminal output; use --json for the complete bounded report.`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!checks.length)
|
|
413
|
+
lines.push('No posture checks were returned.');
|
|
414
|
+
return `${lines.join('\n')}\n`;
|
|
415
|
+
}
|
|
416
|
+
function formatTerminalHttp(document) {
|
|
417
|
+
const http = terminalRecord(document.http);
|
|
418
|
+
const response = terminalRecord(http.response);
|
|
419
|
+
const attempts = Array.isArray(http.attempts) ? http.attempts : [];
|
|
420
|
+
const limitations = Array.isArray(http.limitations) ? http.limitations : [];
|
|
421
|
+
const securityHeadersRecord = terminalRecord(response.securityHeaders);
|
|
422
|
+
const securityHeaders = Object.keys(securityHeadersRecord).length
|
|
423
|
+
? Object.entries(securityHeadersRecord).filter(([, value]) => Boolean(value)).map(([name]) => name)
|
|
424
|
+
: [];
|
|
425
|
+
const lines = [
|
|
426
|
+
`Domain ${safeTerminalValue(document.domain)}`,
|
|
427
|
+
`Probe ${titleCase(document.probeStatus)}`,
|
|
428
|
+
`Activity ${titleCase(document.activityStatus)}`,
|
|
429
|
+
`Evidence ${titleCase(http.status)}`,
|
|
430
|
+
`Final URL ${safeTerminalValue(http.finalUrl)}`,
|
|
431
|
+
`HTTP status ${safeTerminalValue(response.status)}`,
|
|
432
|
+
`Transport ${safeTerminalValue(http.transportSecurity)}`,
|
|
433
|
+
`Redirects ${safeTerminalValue(http.redirectCount, '0')}`,
|
|
434
|
+
`Content type ${safeTerminalValue(response.contentType)}`,
|
|
435
|
+
`Body inspected ${response.bodyInspected === true ? 'Yes' : response.bodyInspected === false ? 'No' : '—'}`,
|
|
436
|
+
`Security ${securityHeaders.length ? securityHeaders.join(', ') : 'No selected headers observed'}`,
|
|
437
|
+
];
|
|
438
|
+
if (document.detail)
|
|
439
|
+
lines.push(`Detail ${safeTerminalValue(document.detail)}`);
|
|
440
|
+
const bodyHash = terminalRecord(response.bodyHash);
|
|
441
|
+
if (bodyHash.value) {
|
|
442
|
+
lines.push(`Body hash ${safeTerminalValue(`${bodyHash.algorithm}:${bodyHash.value} (${bodyHash.scope})`)}`);
|
|
443
|
+
}
|
|
444
|
+
for (const value of attempts) {
|
|
445
|
+
const attempt = terminalRecord(value);
|
|
446
|
+
const outcome = attempt.httpStatus ? `HTTP ${attempt.httpStatus}` : attempt.error || attempt.outcome;
|
|
447
|
+
lines.push(`Attempt ${safeTerminalValue(attempt.url)} — ${safeTerminalValue(outcome)}`);
|
|
448
|
+
}
|
|
449
|
+
for (const limitation of limitations)
|
|
450
|
+
lines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
451
|
+
return `${lines.join('\n')}\n`;
|
|
452
|
+
}
|
|
453
|
+
function formatTerminalTls(document) {
|
|
454
|
+
const certificate = terminalRecord(document.certificate);
|
|
455
|
+
const subject = terminalRecord(certificate.subject);
|
|
456
|
+
const issuer = terminalRecord(certificate.issuer);
|
|
457
|
+
const altNames = terminalRecord(certificate.subjectAltNames);
|
|
458
|
+
const dnsNames = Array.isArray(altNames.dnsNames) ? altNames.dnsNames : [];
|
|
459
|
+
const ipAddresses = Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses : [];
|
|
460
|
+
const visibleAltNames = [...dnsNames, ...ipAddresses].slice(0, MAX_TLS_TERMINAL_ALT_NAMES);
|
|
461
|
+
const omittedAltNames = dnsNames.length + ipAddresses.length - visibleAltNames.length;
|
|
462
|
+
const cipher = terminalRecord(document.cipher);
|
|
463
|
+
const publicKey = terminalRecord(certificate.publicKey);
|
|
464
|
+
const signature = terminalRecord(certificate.signature);
|
|
465
|
+
const purposes = terminalRecord(certificate.extendedKeyUsage);
|
|
466
|
+
const purposeValues = Array.isArray(purposes.values)
|
|
467
|
+
? purposes.values.slice(0, MAX_TLS_TERMINAL_PURPOSES).map(terminalRecord)
|
|
468
|
+
: [];
|
|
469
|
+
const sanClasses = terminalCountSummary(altNames.classes, [
|
|
470
|
+
['dns', 'DNS'],
|
|
471
|
+
['ip', 'IP'],
|
|
472
|
+
['email', 'email'],
|
|
473
|
+
['uri', 'URI'],
|
|
474
|
+
['directoryName', 'directory name'],
|
|
475
|
+
['registeredId', 'registered ID'],
|
|
476
|
+
['otherName', 'other name'],
|
|
477
|
+
['unclassified', 'other'],
|
|
478
|
+
]);
|
|
479
|
+
const aia = terminalRecord(certificate.authorityInformationAccess);
|
|
480
|
+
const ocsp = terminalRecord(aia.ocsp);
|
|
481
|
+
const caIssuers = terminalRecord(aia.caIssuers);
|
|
482
|
+
const authorization = terminalRecord(document.authorization);
|
|
483
|
+
const hostname = terminalRecord(document.hostname);
|
|
484
|
+
const validity = terminalRecord(document.validity);
|
|
485
|
+
const diagnostics = terminalRecord(document.diagnostics);
|
|
486
|
+
const findings = Array.isArray(document.findings) ? document.findings : [];
|
|
487
|
+
const limitations = Array.isArray(document.limitations) ? document.limitations : [];
|
|
488
|
+
const lines = [
|
|
489
|
+
`Hostname ${safeTerminalValue(document.sniHost)}`,
|
|
490
|
+
`Evidence ${titleCase(document.status)}`,
|
|
491
|
+
`Observed ${safeTerminalValue(document.observedAt)}`,
|
|
492
|
+
`Address ${safeTerminalValue(document.connectedAddress)}`,
|
|
493
|
+
`Protocol ${safeTerminalValue(document.protocol)}`,
|
|
494
|
+
`ALPN ${safeTerminalValue(document.alpnProtocol)}`,
|
|
495
|
+
`Cipher ${safeTerminalValue(cipher.standardName || cipher.name)}`,
|
|
496
|
+
`Authorized ${authorization.authorized === true ? 'Yes' : authorization.authorized === false ? 'No' : 'Unknown'}`,
|
|
497
|
+
`Hostname match ${hostname.matches === true ? 'Yes' : hostname.matches === false ? 'No' : 'Unknown'}`,
|
|
498
|
+
`Validity ${titleCase(validity.status)}`,
|
|
499
|
+
`Subject ${safeTerminalValue(Array.isArray(subject.commonNames) ? subject.commonNames.join(', ') : null)}`,
|
|
500
|
+
`Issuer ${safeTerminalValue(Array.isArray(issuer.commonNames) ? issuer.commonNames.join(', ') : null)}`,
|
|
501
|
+
`Valid from ${safeTerminalValue(certificate.validFrom)}`,
|
|
502
|
+
`Valid to ${safeTerminalValue(certificate.validTo)}`,
|
|
503
|
+
`Fingerprint ${safeTerminalValue(certificate.fingerprintSha256)}`,
|
|
504
|
+
`Public key ${safeTerminalValue([publicKey.type, publicKey.bits ? `${publicKey.bits} bits` : null, publicKey.curve].filter(Boolean).join(' '))}`,
|
|
505
|
+
...(signature.algorithm || signature.oid
|
|
506
|
+
? [`Signature ${safeTerminalValue([signature.algorithm, signature.oid ? `(${signature.oid})` : null].filter(Boolean).join(' '))}`]
|
|
507
|
+
: []),
|
|
508
|
+
...(Object.keys(purposes).length
|
|
509
|
+
? [`Purposes ${purposeValues.length
|
|
510
|
+
? purposeValues.map((purpose) => safeTerminalValue(`${purpose.name || 'Unrecognized purpose'} (${purpose.oid || 'unknown OID'})`)).join(', ')
|
|
511
|
+
: 'None declared'}${Array.isArray(purposes.values) && purposes.values.length > purposeValues.length ? ` (+${purposes.values.length - purposeValues.length} more)` : ''}${purposes.truncated ? ' (source truncated)' : ''}`]
|
|
512
|
+
: []),
|
|
513
|
+
`Alt names ${visibleAltNames.length ? visibleAltNames.map((value) => safeTerminalValue(value)).join(', ') : '—'}${omittedAltNames > 0 ? ` (+${omittedAltNames} more)` : ''}`,
|
|
514
|
+
...(Object.keys(terminalRecord(altNames.classes)).length
|
|
515
|
+
? [`SAN classes ${sanClasses || 'None observed'}${altNames.truncated ? ' (truncated)' : ''}`]
|
|
516
|
+
: []),
|
|
517
|
+
...(Object.keys(aia).length
|
|
518
|
+
? [`AIA presence ${[
|
|
519
|
+
terminalCount(ocsp.total) > 0
|
|
520
|
+
? `OCSP ${terminalCount(ocsp.total)} (${terminalCount(ocsp.https)} HTTPS, ${terminalCount(ocsp.http)} HTTP, ${terminalCount(ocsp.other)} other)`
|
|
521
|
+
: null,
|
|
522
|
+
terminalCount(caIssuers.total) > 0
|
|
523
|
+
? `CA issuers ${terminalCount(caIssuers.total)} (${terminalCount(caIssuers.https)} HTTPS, ${terminalCount(caIssuers.http)} HTTP, ${terminalCount(caIssuers.other)} other)`
|
|
524
|
+
: null,
|
|
525
|
+
terminalCount(aia.unknownMethods) > 0 ? `unknown methods ${terminalCount(aia.unknownMethods)}` : null,
|
|
526
|
+
].filter(Boolean).join(' · ') || 'None declared'}${aia.truncated ? ' (truncated)' : ''}`]
|
|
527
|
+
: []),
|
|
528
|
+
`Chain ${safeTerminalValue(Array.isArray(document.chain) ? document.chain.length : 0, '0')} certificate${Array.isArray(document.chain) && document.chain.length === 1 ? '' : 's'}${document.chainTruncated ? ' (truncated)' : ''}`,
|
|
529
|
+
];
|
|
530
|
+
if (authorization.error)
|
|
531
|
+
lines.push(`Trust detail ${safeTerminalValue(authorization.error)}`);
|
|
532
|
+
if (hostname.error)
|
|
533
|
+
lines.push(`Name detail ${safeTerminalValue(hostname.error)}`);
|
|
534
|
+
if (diagnostics.error)
|
|
535
|
+
lines.push(`Error ${safeTerminalValue(diagnostics.error)}`);
|
|
536
|
+
for (const value of findings) {
|
|
537
|
+
const finding = terminalRecord(value);
|
|
538
|
+
lines.push(`Finding ${safeTerminalValue(finding.label)} — ${safeTerminalValue(finding.detail)}`);
|
|
539
|
+
}
|
|
540
|
+
for (const limitation of limitations)
|
|
541
|
+
lines.push(`Limitation ${safeTerminalValue(limitation)}`);
|
|
542
|
+
return `${lines.join('\n')}\n`;
|
|
543
|
+
}
|
|
544
|
+
function comparisonStatusLabel(status) {
|
|
545
|
+
const labels = {
|
|
546
|
+
equivalent: 'Equivalent',
|
|
547
|
+
conflict: 'Conflict',
|
|
548
|
+
rdap_only: 'RDAP only',
|
|
549
|
+
whois_only: 'WHOIS only',
|
|
550
|
+
rdap_redacted: 'RDAP redacted',
|
|
551
|
+
whois_redacted: 'WHOIS redacted',
|
|
552
|
+
rdap_unavailable: 'RDAP unavailable',
|
|
553
|
+
whois_unavailable: 'WHOIS unavailable',
|
|
554
|
+
rdap_incomplete: 'RDAP incomplete',
|
|
555
|
+
whois_incomplete: 'WHOIS incomplete',
|
|
556
|
+
registry_only: 'Registry only',
|
|
557
|
+
registrar_only: 'Registrar only',
|
|
558
|
+
registry_redacted: 'Registry redacted',
|
|
559
|
+
registrar_redacted: 'Registrar redacted',
|
|
560
|
+
registry_unavailable: 'Registry unavailable',
|
|
561
|
+
registrar_unavailable: 'Registrar unavailable',
|
|
562
|
+
registry_incomplete: 'Registry incomplete',
|
|
563
|
+
registrar_incomplete: 'Registrar incomplete',
|
|
564
|
+
};
|
|
565
|
+
return labels[String(status)] || titleCase(status);
|
|
566
|
+
}
|
|
567
|
+
function formatTerminalCompare(document) {
|
|
568
|
+
const fields = Array.isArray(document.fields) ? document.fields : [];
|
|
569
|
+
const counts = terminalRecord(document.counts);
|
|
570
|
+
const sourceHealth = terminalRecord(document.sourceHealth);
|
|
571
|
+
const rdapHealth = terminalRecord(sourceHealth.rdap);
|
|
572
|
+
const whoisHealth = terminalRecord(sourceHealth.whois);
|
|
573
|
+
const registryAccess = terminalRecord(document.registryAccess);
|
|
574
|
+
const differenceCount = fields.length - (Number(counts.equivalent) || 0);
|
|
575
|
+
const lines = [
|
|
576
|
+
`Query ${safeTerminalValue(document.query || document.registrableDomain)}`,
|
|
577
|
+
`Lookup mode ${titleCase(document.lookupMode)}`,
|
|
578
|
+
`Lookup saved ${safeTerminalValue(document.lookupGeneratedAt)}`,
|
|
579
|
+
`RDAP source ${comparisonStatusLabel(rdapHealth.status)}`,
|
|
580
|
+
`WHOIS source ${comparisonStatusLabel(whoisHealth.status)}`,
|
|
581
|
+
`Compared ${safeTerminalValue(fields.length, '0')} field${fields.length === 1 ? '' : 's'}`,
|
|
582
|
+
`Equivalent ${safeTerminalValue(counts.equivalent, '0')}`,
|
|
583
|
+
`Differences ${safeTerminalValue(differenceCount, '0')}`,
|
|
584
|
+
];
|
|
585
|
+
if (Object.keys(registryAccess).length) {
|
|
586
|
+
lines.push(`Registry access .${safeTerminalValue(registryAccess.suffix)}`, `WHOIS access ${registryAccessProfileLabel(registryAccess.whoisAccessProfile)}`, `RDAP access ${registryAccessProfileLabel(registryAccess.rdapAccessProfile)}`, `Access note ${safeTerminalValue(registryAccess.limitation)}`);
|
|
587
|
+
}
|
|
588
|
+
lines.push('');
|
|
589
|
+
if (!fields.length) {
|
|
590
|
+
lines.push('Neither source published a comparable normalized field.');
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
for (const value of fields) {
|
|
594
|
+
const field = terminalRecord(value);
|
|
595
|
+
lines.push(`[${comparisonStatusLabel(field.status).toUpperCase()}] ${safeTerminalValue(field.label)}`);
|
|
596
|
+
lines.push(` RDAP ${safeTerminalValue(field.rdapDisplay)}`);
|
|
597
|
+
lines.push(` WHOIS ${safeTerminalValue(field.whoisDisplay)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const registrarComparison = terminalRecord(document.registrarPublicationComparison);
|
|
601
|
+
if (Object.keys(registrarComparison).length) {
|
|
602
|
+
const publicationFields = Array.isArray(registrarComparison.fields) ? registrarComparison.fields : [];
|
|
603
|
+
const publicationCounts = terminalRecord(registrarComparison.counts);
|
|
604
|
+
const publicationHealth = terminalRecord(registrarComparison.sourceHealth);
|
|
605
|
+
const registryHealth = terminalRecord(publicationHealth.registry);
|
|
606
|
+
const registrarHealth = terminalRecord(publicationHealth.registrar);
|
|
607
|
+
const publicationDifferences = publicationFields.length - (Number(publicationCounts.equivalent) || 0);
|
|
608
|
+
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')}`, '');
|
|
609
|
+
if (!publicationFields.length) {
|
|
610
|
+
lines.push('Neither RDAP publication exposed a comparable normalized field.');
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
for (const value of publicationFields) {
|
|
614
|
+
const field = terminalRecord(value);
|
|
615
|
+
lines.push(`[${comparisonStatusLabel(field.status).toUpperCase()}] ${safeTerminalValue(field.label)}`);
|
|
616
|
+
lines.push(` Registry ${safeTerminalValue(field.registryDisplay)}`);
|
|
617
|
+
lines.push(` Registrar ${safeTerminalValue(field.registrarDisplay)}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
lines.push('', 'Comparison is source reconciliation, not an availability or ownership decision.');
|
|
622
|
+
if (Object.keys(registryAccess).length) {
|
|
623
|
+
lines.push('Registry access describes collection reachability only; it does not decide registration, availability, ownership, safety, or maliciousness.');
|
|
624
|
+
}
|
|
625
|
+
return `${lines.join('\n')}\n`;
|
|
626
|
+
}
|
|
627
|
+
function formatTerminalRiskCalibration(document) {
|
|
628
|
+
const summary = terminalRecord(document.summary);
|
|
629
|
+
const bands = terminalRecord(summary.scoreBands);
|
|
630
|
+
const thresholds = Array.isArray(document.thresholds) ? document.thresholds : [];
|
|
631
|
+
const records = Array.isArray(document.records) ? document.records : [];
|
|
632
|
+
const visible = records.slice(0, MAX_RISK_CALIBRATION_TERMINAL_RECORDS);
|
|
633
|
+
const lines = [
|
|
634
|
+
`Risk model v${safeTerminalValue(document.riskModelVersion)}`,
|
|
635
|
+
`Review band ${safeTerminalValue(document.currentReviewThreshold)}+`,
|
|
636
|
+
`Records ${safeTerminalValue(summary.total, '0')}`,
|
|
637
|
+
`Metric labels ${safeTerminalValue((Number(summary.positive) || 0) + (Number(summary.negative) || 0), '0')}`,
|
|
638
|
+
`Excluded ${safeTerminalValue(summary.excluded, '0')}`,
|
|
639
|
+
`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')}`,
|
|
640
|
+
'',
|
|
641
|
+
'Threshold replay',
|
|
642
|
+
];
|
|
643
|
+
for (const value of thresholds) {
|
|
644
|
+
const threshold = terminalRecord(value);
|
|
645
|
+
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)}`);
|
|
646
|
+
}
|
|
647
|
+
lines.push('', 'Records');
|
|
648
|
+
for (const value of visible) {
|
|
649
|
+
const record = terminalRecord(value);
|
|
650
|
+
const score = record.score === null ? 'not scored' : String(record.score);
|
|
651
|
+
lines.push(`${safeTerminalValue(record.id)} ${safeTerminalValue(record.domain)} ${titleCase(record.analystDisposition)} ${score}`);
|
|
652
|
+
}
|
|
653
|
+
if (records.length > visible.length)
|
|
654
|
+
lines.push(`… ${records.length - visible.length} additional records omitted from terminal output; use --json for the complete bounded report.`);
|
|
655
|
+
lines.push('', safeTerminalValue(terminalRecord(document.interpretation).statement));
|
|
656
|
+
return `${lines.join('\n')}\n`;
|
|
657
|
+
}
|
|
658
|
+
export { MAX_CT_TERMINAL_HOSTNAMES, MAX_CT_TERMINAL_MATCHES, MAX_DISCOVER_TERMINAL_CANDIDATES, 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, };
|