@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,437 @@
|
|
|
1
|
+
// Versioned, bounded technology indicators derived from the HTTP and static
|
|
2
|
+
// HTML evidence already collected by a deep lookup. A standards-compliant
|
|
3
|
+
// parser identifies live elements and attributes without executing scripts.
|
|
4
|
+
// Signatures emit only curated labels and fixed evidence descriptions; matched
|
|
5
|
+
// markup, header values, URL paths, and arbitrary upstream strings are never
|
|
6
|
+
// retained.
|
|
7
|
+
import { analyzeBrowserLibraries } from "./browser-library-profile.mjs";
|
|
8
|
+
import { createObservation } from "./observation.mjs";
|
|
9
|
+
import { MAX_STATIC_HTML_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
10
|
+
const TECHNOLOGY_PROFILE_VERSION = 6;
|
|
11
|
+
const MAX_TECHNOLOGY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
12
|
+
const MAX_TECHNOLOGY_TAG_LENGTH = MAX_TAG_LENGTH;
|
|
13
|
+
const MAX_TECHNOLOGY_FINDINGS = 24;
|
|
14
|
+
const MAX_EVIDENCE_PER_TECHNOLOGY = 4;
|
|
15
|
+
const MAX_RESOURCE_ORIGINS = 30;
|
|
16
|
+
const MAX_GENERATOR_INPUT = 160;
|
|
17
|
+
const MAX_SERVER_INPUT = 240;
|
|
18
|
+
const MAX_PASSIVE_RESPONSE_HEADERS = 8;
|
|
19
|
+
const MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH = 180;
|
|
20
|
+
const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/;
|
|
21
|
+
function boundedLowercase(value, maxLength) {
|
|
22
|
+
if (typeof value !== 'string' || value.length > maxLength || CONTROL_CHARACTER_RE.test(value))
|
|
23
|
+
return '';
|
|
24
|
+
return value.trim().toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function normalizedResourceHosts(value) {
|
|
27
|
+
const hosts = new Set();
|
|
28
|
+
for (const item of (Array.isArray(value) ? value : []).slice(0, MAX_RESOURCE_ORIGINS)) {
|
|
29
|
+
if (typeof item !== 'string' || item.length > 2048 || CONTROL_CHARACTER_RE.test(item))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(item);
|
|
33
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname)
|
|
34
|
+
continue;
|
|
35
|
+
hosts.add(parsed.hostname.toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// An invalid retained origin cannot contribute to a technology finding.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return hosts;
|
|
42
|
+
}
|
|
43
|
+
const PASSIVE_HEADER_NAMES = new Set([
|
|
44
|
+
'cf-ray',
|
|
45
|
+
'x-drupal-cache',
|
|
46
|
+
'x-fastly-request-id',
|
|
47
|
+
'x-powered-by',
|
|
48
|
+
'x-shopify-stage',
|
|
49
|
+
'x-sorting-hat-podid',
|
|
50
|
+
'x-vercel-id',
|
|
51
|
+
]);
|
|
52
|
+
function normalizedResponseHeaders(value) {
|
|
53
|
+
const input = value && typeof value === 'object' && !Array.isArray(value)
|
|
54
|
+
? value
|
|
55
|
+
: {};
|
|
56
|
+
const output = new Map();
|
|
57
|
+
for (const [rawName, rawValue] of Object.entries(input).slice(0, MAX_PASSIVE_RESPONSE_HEADERS * 4)) {
|
|
58
|
+
const name = rawName.trim().toLowerCase();
|
|
59
|
+
if (!PASSIVE_HEADER_NAMES.has(name) || typeof rawValue !== 'string')
|
|
60
|
+
continue;
|
|
61
|
+
const normalized = boundedLowercase(rawValue, 240);
|
|
62
|
+
if (!normalized)
|
|
63
|
+
continue;
|
|
64
|
+
output.set(name, normalized);
|
|
65
|
+
if (output.size >= MAX_PASSIVE_RESPONSE_HEADERS)
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
function generatorEvidence(pattern, description) {
|
|
71
|
+
return {
|
|
72
|
+
source: 'generator metadata',
|
|
73
|
+
description,
|
|
74
|
+
confidence: 'high',
|
|
75
|
+
matches: ({ generator }) => pattern.test(generator),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function htmlEvidence(markers, description, confidence = 'high') {
|
|
79
|
+
return {
|
|
80
|
+
source: 'static HTML',
|
|
81
|
+
description,
|
|
82
|
+
confidence,
|
|
83
|
+
matches: ({ html }) => markers.some((marker) => html.includes(marker)),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function resourceEvidence(hosts, description) {
|
|
87
|
+
return {
|
|
88
|
+
source: 'resource origin',
|
|
89
|
+
description,
|
|
90
|
+
confidence: 'medium',
|
|
91
|
+
matches: ({ resourceHosts }) => hosts.some((host) => resourceHosts.has(host) || [...resourceHosts].some((value) => value.endsWith(`.${host}`))),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function resourcePatternEvidence(pattern, description) {
|
|
95
|
+
return {
|
|
96
|
+
source: 'resource origin',
|
|
97
|
+
description,
|
|
98
|
+
confidence: 'medium',
|
|
99
|
+
matches: ({ resourceHosts }) => [...resourceHosts].some((host) => pattern.test(host)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function serverEvidence(pattern, description, confidence = 'high') {
|
|
103
|
+
return {
|
|
104
|
+
source: 'HTTP server header',
|
|
105
|
+
description,
|
|
106
|
+
confidence,
|
|
107
|
+
matches: ({ httpServer }) => pattern.test(httpServer),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function responseHeaderEvidence(name, pattern, description, confidence = 'high') {
|
|
111
|
+
return {
|
|
112
|
+
source: 'passive response header',
|
|
113
|
+
description,
|
|
114
|
+
confidence,
|
|
115
|
+
matches: ({ responseHeaders }) => {
|
|
116
|
+
if (!responseHeaders.has(name))
|
|
117
|
+
return false;
|
|
118
|
+
return pattern ? pattern.test(responseHeaders.get(name) ?? '') : true;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const TECHNOLOGY_SIGNATURES = [
|
|
123
|
+
{
|
|
124
|
+
id: 'wordpress', name: 'WordPress', category: 'content management',
|
|
125
|
+
evidence: [
|
|
126
|
+
generatorEvidence(/^wordpress(?:\s|$)/i, 'Generator metadata identifies WordPress.'),
|
|
127
|
+
htmlEvidence(['/wp-content/', '/wp-includes/'], 'Static resource paths use WordPress conventions.', 'medium'),
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'drupal', name: 'Drupal', category: 'content management',
|
|
132
|
+
evidence: [
|
|
133
|
+
generatorEvidence(/^drupal(?:\s|$)/i, 'Generator metadata identifies Drupal.'),
|
|
134
|
+
htmlEvidence(['data-drupal-selector=', 'data-drupal-link-system-path=', 'drupal-settings-json'], 'Static markup contains Drupal-specific attributes.'),
|
|
135
|
+
responseHeaderEvidence('x-drupal-cache', null, 'A Drupal-specific cache response header was observed.', 'medium'),
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'joomla', name: 'Joomla', category: 'content management',
|
|
140
|
+
evidence: [generatorEvidence(/^joomla!?\b/i, 'Generator metadata identifies Joomla.')],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'ghost', name: 'Ghost', category: 'content management',
|
|
144
|
+
evidence: [
|
|
145
|
+
generatorEvidence(/^ghost(?:\s|$)/i, 'Generator metadata identifies Ghost.'),
|
|
146
|
+
htmlEvidence(['ghost/api/content/', 'data-ghost-search'], 'Static markup contains Ghost-specific integration markers.'),
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'craft-cms', name: 'Craft CMS', category: 'content management',
|
|
151
|
+
evidence: [generatorEvidence(/^craft cms(?:\s|$)/i, 'Generator metadata identifies Craft CMS.')],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'typo3', name: 'TYPO3 CMS', category: 'content management',
|
|
155
|
+
evidence: [generatorEvidence(/^typo3(?:\s+cms)?(?:\s|$)/i, 'Generator metadata identifies TYPO3 CMS.')],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'shopify', name: 'Shopify', category: 'commerce',
|
|
159
|
+
requiresNonResourceEvidence: true,
|
|
160
|
+
evidence: [
|
|
161
|
+
htmlEvidence(['shopify-section', 'shopify.theme'], 'Static markup contains Shopify-specific storefront markers.'),
|
|
162
|
+
resourceEvidence(['cdn.shopify.com'], 'A retained resource origin uses the Shopify content network.'),
|
|
163
|
+
responseHeaderEvidence('x-shopify-stage', null, 'A Shopify-specific platform response header was observed.', 'medium'),
|
|
164
|
+
responseHeaderEvidence('x-sorting-hat-podid', null, 'A Shopify-specific routing response header was observed.', 'medium'),
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'php', name: 'PHP', category: 'application runtime',
|
|
169
|
+
evidence: [responseHeaderEvidence('x-powered-by', /^php(?:\s|$|\/)/i, 'The passive X-Powered-By response header identifies PHP.')],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'aspnet', name: 'ASP.NET', category: 'web framework',
|
|
173
|
+
evidence: [responseHeaderEvidence('x-powered-by', /^asp\.net(?:\s|$|\/)/i, 'The passive X-Powered-By response header identifies ASP.NET.')],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'express', name: 'Express', category: 'web framework',
|
|
177
|
+
evidence: [responseHeaderEvidence('x-powered-by', /^express(?:\s|$|\/)/i, 'The passive X-Powered-By response header identifies Express.')],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'adobe-commerce-magento', name: 'Adobe Commerce / Magento Open Source', category: 'commerce',
|
|
181
|
+
evidence: [
|
|
182
|
+
htmlEvidence(['data-mage-init=', 'type="text/x-magento-init"', "type='text/x-magento-init'"], 'Static markup contains Commerce frontend initialization markers.'),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'bigcommerce', name: 'BigCommerce', category: 'commerce',
|
|
187
|
+
minimumEvidenceMatches: 2,
|
|
188
|
+
evidence: [
|
|
189
|
+
htmlEvidence(['cdn11.bigcommerce.com/s-', 'stencil-utils'], 'Static markup contains BigCommerce storefront asset markers.', 'medium'),
|
|
190
|
+
resourcePatternEvidence(/^cdn\d+\.bigcommerce\.com$/i, 'A retained resource origin uses BigCommerce storefront delivery infrastructure.'),
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'woocommerce', name: 'WooCommerce', category: 'commerce',
|
|
195
|
+
evidence: [
|
|
196
|
+
htmlEvidence(['/wp-content/plugins/woocommerce/'], 'Static resource paths identify the WooCommerce plugin.'),
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'opencart', name: 'OpenCart', category: 'commerce',
|
|
201
|
+
evidence: [generatorEvidence(/^opencart(?:\s|$)/i, 'Generator metadata identifies OpenCart.')],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'prestashop', name: 'PrestaShop', category: 'commerce',
|
|
205
|
+
evidence: [generatorEvidence(/^prestashop(?:\s|$)/i, 'Generator metadata identifies PrestaShop.')],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'wix', name: 'Wix', category: 'site builder',
|
|
209
|
+
requiresNonResourceEvidence: true,
|
|
210
|
+
evidence: [
|
|
211
|
+
generatorEvidence(/^wix(?:\s|$)/i, 'Generator metadata identifies Wix.'),
|
|
212
|
+
htmlEvidence(['data-mesh-id='], 'Static markup contains a Wix-specific document attribute.', 'medium'),
|
|
213
|
+
resourceEvidence(['static.parastorage.com', 'wixstatic.com'], 'A retained resource origin uses Wix delivery infrastructure.'),
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'squarespace', name: 'Squarespace', category: 'site builder',
|
|
218
|
+
requiresNonResourceEvidence: true,
|
|
219
|
+
evidence: [
|
|
220
|
+
generatorEvidence(/^squarespace(?:\s|$)/i, 'Generator metadata identifies Squarespace.'),
|
|
221
|
+
htmlEvidence(['squarespace-context'], 'Static markup contains a Squarespace-specific document marker.', 'medium'),
|
|
222
|
+
resourceEvidence(['static.squarespace.com', 'static1.squarespace.com'], 'A retained resource origin uses Squarespace delivery infrastructure.'),
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'webflow', name: 'Webflow', category: 'site builder',
|
|
227
|
+
evidence: [
|
|
228
|
+
generatorEvidence(/^webflow(?:\s|$)/i, 'Generator metadata identifies Webflow.'),
|
|
229
|
+
htmlEvidence(['data-wf-page=', 'data-wf-site='], 'Static markup contains Webflow-specific document attributes.'),
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'framer', name: 'Framer', category: 'site builder',
|
|
234
|
+
requiresNonResourceEvidence: true,
|
|
235
|
+
evidence: [
|
|
236
|
+
generatorEvidence(/^framer(?:\s|$)/i, 'Generator metadata identifies Framer.'),
|
|
237
|
+
htmlEvidence(['data-framer-name='], 'Static markup contains Framer-specific component attributes.'),
|
|
238
|
+
resourceEvidence(['framerusercontent.com'], 'A retained resource origin uses Framer delivery infrastructure.'),
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: 'weebly', name: 'Weebly', category: 'site builder',
|
|
243
|
+
requiresNonResourceEvidence: true,
|
|
244
|
+
evidence: [
|
|
245
|
+
generatorEvidence(/^weebly(?:\s|$)/i, 'Generator metadata identifies Weebly.'),
|
|
246
|
+
resourceEvidence(['editmysite.com'], 'A retained resource origin uses Weebly delivery infrastructure.'),
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'angular', name: 'Angular', category: 'web framework',
|
|
251
|
+
evidence: [htmlEvidence([' ng-version='], 'Static markup contains Angular version metadata.')],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'aspnet-web-forms', name: 'ASP.NET Web Forms', category: 'web framework',
|
|
255
|
+
evidence: [htmlEvidence([' name="__viewstate"', ' id="__viewstate"'], 'Static form markup contains the ASP.NET Web Forms view-state field.')],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: 'nextjs', name: 'Next.js', category: 'web framework',
|
|
259
|
+
evidence: [htmlEvidence(['id="__next_data__"', "id='__next_data__'", '/_next/static/'], 'Static markup contains Next.js bootstrap or asset markers.')],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'nuxt', name: 'Nuxt', category: 'web framework',
|
|
263
|
+
evidence: [htmlEvidence(['id="__nuxt"', "id='__nuxt'", '/_nuxt/'], 'Static markup contains Nuxt bootstrap or asset markers.')],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'gatsby', name: 'Gatsby', category: 'web framework',
|
|
267
|
+
evidence: [htmlEvidence(['id="___gatsby"', "id='___gatsby'", '/page-data/app-data.json'], 'Static markup contains Gatsby bootstrap or page-data markers.')],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'sveltekit', name: 'SvelteKit', category: 'web framework',
|
|
271
|
+
evidence: [
|
|
272
|
+
htmlEvidence(['data-sveltekit-preload-data=', 'data-sveltekit-reload='], 'Static markup contains SvelteKit-specific navigation attributes.'),
|
|
273
|
+
htmlEvidence(['href="/_app/immutable/', 'src="/_app/immutable/'], 'Static asset paths use SvelteKit build conventions.', 'medium'),
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: 'astro', name: 'Astro', category: 'web framework',
|
|
278
|
+
evidence: [
|
|
279
|
+
htmlEvidence(['<astro-island', '<astro-slot'], 'Static markup contains Astro component-island elements.'),
|
|
280
|
+
htmlEvidence(['href="/_astro/', 'src="/_astro/'], 'Static asset paths use Astro build conventions.', 'medium'),
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'hugo', name: 'Hugo', category: 'static site generator',
|
|
285
|
+
evidence: [generatorEvidence(/^hugo(?:\s|$)/i, 'Generator metadata identifies Hugo.')],
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 'jekyll', name: 'Jekyll', category: 'static site generator',
|
|
289
|
+
evidence: [generatorEvidence(/^jekyll(?:\s|$)/i, 'Generator metadata identifies Jekyll.')],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: 'docusaurus', name: 'Docusaurus', category: 'static site generator',
|
|
293
|
+
evidence: [generatorEvidence(/^docusaurus(?:\s|$)/i, 'Generator metadata identifies Docusaurus.')],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: 'eleventy', name: 'Eleventy', category: 'static site generator',
|
|
297
|
+
evidence: [generatorEvidence(/^(?:eleventy|11ty)(?:\s|$)/i, 'Generator metadata identifies Eleventy.')],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: 'hexo', name: 'Hexo', category: 'static site generator',
|
|
301
|
+
evidence: [generatorEvidence(/^hexo(?:\s|$)/i, 'Generator metadata identifies Hexo.')],
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: 'cloudflare', name: 'Cloudflare', category: 'delivery platform',
|
|
305
|
+
evidence: [
|
|
306
|
+
serverEvidence(/^cloudflare(?:\s|$|\/)/i, 'The selected response server header identifies Cloudflare.'),
|
|
307
|
+
responseHeaderEvidence('cf-ray', null, 'A Cloudflare request-trace response header was observed.', 'medium'),
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'cloudfront', name: 'Amazon CloudFront', category: 'delivery platform',
|
|
312
|
+
evidence: [
|
|
313
|
+
serverEvidence(/^cloudfront(?:\s|$|\/)/i, 'The selected response server header identifies Amazon CloudFront.'),
|
|
314
|
+
resourceEvidence(['cloudfront.net'], 'A retained resource origin uses Amazon CloudFront delivery infrastructure.'),
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'netlify', name: 'Netlify', category: 'delivery platform',
|
|
319
|
+
evidence: [serverEvidence(/^netlify(?:\s|$|\/)/i, 'The selected response server header identifies Netlify.')],
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: 'vercel', name: 'Vercel', category: 'delivery platform',
|
|
323
|
+
evidence: [
|
|
324
|
+
serverEvidence(/^vercel(?:\s|$|\/)/i, 'The selected response server header identifies Vercel.'),
|
|
325
|
+
responseHeaderEvidence('x-vercel-id', null, 'A Vercel request-trace response header was observed.', 'medium'),
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: 'fastly', name: 'Fastly', category: 'delivery platform',
|
|
330
|
+
evidence: [responseHeaderEvidence('x-fastly-request-id', null, 'A Fastly request identifier response header was observed.', 'medium')],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: 'nginx', name: 'nginx', category: 'web server',
|
|
334
|
+
evidence: [serverEvidence(/^nginx(?:\s|$|\/)/i, 'The selected response server header identifies nginx.')],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: 'apache-http-server', name: 'Apache HTTP Server', category: 'web server',
|
|
338
|
+
evidence: [serverEvidence(/^apache(?:\s|$|\/)/i, 'The selected response server header identifies Apache HTTP Server.')],
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: 'microsoft-iis', name: 'Microsoft IIS', category: 'web server',
|
|
342
|
+
evidence: [serverEvidence(/^microsoft-iis(?:\s|$|\/)/i, 'The selected response server header identifies Microsoft IIS.')],
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: 'litespeed', name: 'LiteSpeed', category: 'web server',
|
|
346
|
+
evidence: [serverEvidence(/^(?:open)?litespeed(?:\s|$|\/)/i, 'The selected response server header identifies LiteSpeed.')],
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: 'caddy', name: 'Caddy', category: 'web server',
|
|
350
|
+
evidence: [serverEvidence(/^caddy(?:\s|$|\/)/i, 'The selected response server header identifies Caddy.')],
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
const TECHNOLOGY_SIGNATURE_CATALOGUE = Object.freeze(TECHNOLOGY_SIGNATURES.map((signature) => Object.freeze({
|
|
354
|
+
id: signature.id,
|
|
355
|
+
name: signature.name,
|
|
356
|
+
category: signature.category,
|
|
357
|
+
minimumEvidenceMatches: signature.minimumEvidenceMatches || 1,
|
|
358
|
+
requiresNonResourceEvidence: signature.requiresNonResourceEvidence === true,
|
|
359
|
+
evidence: Object.freeze(signature.evidence.map(({ source, description, confidence }) => Object.freeze({
|
|
360
|
+
source,
|
|
361
|
+
description,
|
|
362
|
+
confidence,
|
|
363
|
+
}))),
|
|
364
|
+
})));
|
|
365
|
+
function analyzeWebsiteTechnology(input = {}) {
|
|
366
|
+
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
|
|
367
|
+
const browserLibraryProfile = analyzeBrowserLibraries({
|
|
368
|
+
htmlAnalysis,
|
|
369
|
+
observedAt: input.observedAt,
|
|
370
|
+
sourceTruncated: input.sourceTruncated,
|
|
371
|
+
});
|
|
372
|
+
const context = {
|
|
373
|
+
html: htmlAnalysis.markup,
|
|
374
|
+
generator: boundedLowercase(input.generator, MAX_GENERATOR_INPUT),
|
|
375
|
+
httpServer: boundedLowercase(input.httpServer, MAX_SERVER_INPUT),
|
|
376
|
+
resourceHosts: normalizedResourceHosts(input.resourceOrigins),
|
|
377
|
+
responseHeaders: normalizedResponseHeaders(input.responseHeaders),
|
|
378
|
+
};
|
|
379
|
+
const findings = [];
|
|
380
|
+
for (const signature of TECHNOLOGY_SIGNATURES) {
|
|
381
|
+
const matched = signature.evidence.filter((evidence) => evidence.matches(context));
|
|
382
|
+
if (matched.length < (signature.minimumEvidenceMatches || 1))
|
|
383
|
+
continue;
|
|
384
|
+
if (signature.requiresNonResourceEvidence && matched.every((evidence) => evidence.source === 'resource origin'))
|
|
385
|
+
continue;
|
|
386
|
+
findings.push({
|
|
387
|
+
id: signature.id,
|
|
388
|
+
name: signature.name,
|
|
389
|
+
category: signature.category,
|
|
390
|
+
confidence: matched.some((evidence) => evidence.confidence === 'high') ? 'high' : 'medium',
|
|
391
|
+
evidence: matched.slice(0, MAX_EVIDENCE_PER_TECHNOLOGY).map(({ source, description }) => ({ source, description })),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
findings.sort((left, right) => left.category.localeCompare(right.category) || left.name.localeCompare(right.name));
|
|
395
|
+
const findingLimitReached = findings.length > MAX_TECHNOLOGY_FINDINGS;
|
|
396
|
+
const truncated = input.sourceTruncated === true
|
|
397
|
+
|| htmlAnalysis.inputLimitReached
|
|
398
|
+
|| htmlAnalysis.tagLimitReached
|
|
399
|
+
|| findingLimitReached;
|
|
400
|
+
const limitations = [
|
|
401
|
+
'Curated signature matching is selective; an unmatched technology may still be present.',
|
|
402
|
+
'Static response evidence cannot identify JavaScript-rendered or deliberately concealed technologies.',
|
|
403
|
+
'Technology indicators describe observed implementation clues, not ownership, safety, or maliciousness.',
|
|
404
|
+
];
|
|
405
|
+
if (input.sourceTruncated === true)
|
|
406
|
+
limitations.push('The captured homepage body was truncated, so technology indicators may be incomplete.');
|
|
407
|
+
if (htmlAnalysis.inputLimitReached)
|
|
408
|
+
limitations.push(`Only the first ${MAX_TECHNOLOGY_HTML_CHARS} HTML characters were evaluated.`);
|
|
409
|
+
if (htmlAnalysis.tagLimitReached)
|
|
410
|
+
limitations.push(`Technology matching reached the ${MAX_TECHNOLOGY_TAGS}-tag or ${MAX_TECHNOLOGY_TAG_LENGTH}-character tag boundary.`);
|
|
411
|
+
if (findingLimitReached)
|
|
412
|
+
limitations.push(`Only the first ${MAX_TECHNOLOGY_FINDINGS} technology findings were retained.`);
|
|
413
|
+
return {
|
|
414
|
+
profileVersion: TECHNOLOGY_PROFILE_VERSION,
|
|
415
|
+
...createObservation({
|
|
416
|
+
status: truncated ? 'partial' : 'success',
|
|
417
|
+
observedAt: input.observedAt,
|
|
418
|
+
scanMode: 'deep',
|
|
419
|
+
source: 'derived',
|
|
420
|
+
complete: !truncated,
|
|
421
|
+
truncated,
|
|
422
|
+
limitations,
|
|
423
|
+
diagnostics: {
|
|
424
|
+
findings: findings.length,
|
|
425
|
+
htmlEvaluated: Boolean(context.html),
|
|
426
|
+
generatorEvaluated: Boolean(context.generator),
|
|
427
|
+
serverEvaluated: Boolean(context.httpServer),
|
|
428
|
+
resourceOriginsEvaluated: context.resourceHosts.size,
|
|
429
|
+
passiveHeadersEvaluated: context.responseHeaders.size,
|
|
430
|
+
tagLimitReached: htmlAnalysis.tagLimitReached,
|
|
431
|
+
},
|
|
432
|
+
}),
|
|
433
|
+
findings: findings.slice(0, MAX_TECHNOLOGY_FINDINGS),
|
|
434
|
+
browserLibraryProfile,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Authority-aware interpretation of WHOIS referral chains. The first
|
|
2
|
+
// definitive non-root response decides existence; later registrar output is
|
|
3
|
+
// diagnostic and cannot override the registry.
|
|
4
|
+
import { normalizeWhoisChain } from "./whois-normalization.mjs";
|
|
5
|
+
import { parseIndentedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
|
|
6
|
+
const NOT_FOUND_RE = /no match for|no match\b|not found|no entries found|domain not found|no object found|not registered|status\s*:\s*(?:available|free)\b|registered\s*:\s*(?:no|false)\b|is available for registration/i;
|
|
7
|
+
const LINE_NOT_FOUND_PATTERNS = Object.freeze([
|
|
8
|
+
/^[ \t]*%[ \t]*nothing found[ \t]*$/im,
|
|
9
|
+
/^[ \t]*[a-z0-9](?:[a-z0-9.-]{0,252})[ \t]+is free[ \t]*$/im,
|
|
10
|
+
/^[ \t]*el dominio no se encuentra registrado en nic argentina[ \t]*$/im,
|
|
11
|
+
/^[ \t]*the domain has not been registered\.?[ \t]*$/im,
|
|
12
|
+
/^[ \t]*the queried object does not exist:[ \t]*no matching objects found[ \t]*$/im,
|
|
13
|
+
/^[ \t]*no record found for[ \t]+'[a-z0-9.-]{1,253}'\.?[ \t]*$/im,
|
|
14
|
+
/^[ \t]*no data found[ \t]*$/im,
|
|
15
|
+
/^[ \t]*>>[ \t]*no data found for domain[ \t]*:[ \t]*[a-z0-9.-]{1,253}[ \t]*$/im,
|
|
16
|
+
/^[ \t]*domain[ \t]+[a-z0-9.-]{1,253}[ \t]+is available for purchase[ \t]*$/im,
|
|
17
|
+
]);
|
|
18
|
+
const NZ_NOT_FOUND_RE = /^[ \t]*query_status[ \t]*:[ \t]*220(?:\s|$)/im;
|
|
19
|
+
const NZ_POSITIVE_RE = /^[ \t]*query_status[ \t]*:[ \t]*(?:200|210)(?:\s|$)/im;
|
|
20
|
+
const NZ_TEMPORARY_FAILURE_RE = /^[ \t]*query_status[ \t]*:[ \t]*4\d{2}(?:\s|$)/im;
|
|
21
|
+
const RATE_LIMIT_LINE_RE = /^[ \t]*(?:[%#*;>-]+[ \t]*)?(?:(?:error|status)[ \t:.-]+)?(?:whois[ \t]+limit[ \t]+exceeded|query[ \t]+(?:rate[ \t-]*)?limit[ \t]+exceeded|(?:request|query)[ \t]+limit[ \t]+(?:exceeded|reached)|rate[ \t-]*limit(?:[ \t]+exceeded)?|too[ \t]+many[ \t]+(?:requests|queries)|quota[ \t]+exceeded|number[ \t]+of[^\r\n]{0,120}[ \t]+exceeded|(?:requests?|queries?)[ \t]+(?:are[ \t]+)?throttled|throttled|(?:service[ \t]+)?temporarily[ \t]+unavailable|(?:please[ \t]+)?try[ \t]+again[ \t]+later|please[ \t]+wait)\b[^\r\n]{0,240}$/im;
|
|
22
|
+
const POSITIVE_REGISTRATION_RE = /^[ \t*]*(?:Domain(?:[ \t]+Name)?|domainname|Registrar|Registrar WHOIS Server|Creation Date|Created(?: On)?|Registry Expiry Date|Registered(?: On)?|Name Server|nserver|Sponsoring Registrar)[ \t.]*:[ \t]*\S/im;
|
|
23
|
+
const POSITIVE_BRACKET_RE = /\[(?:Domain Name|Registrant|Name Server)\][ \t]*\S/i;
|
|
24
|
+
function hasSectionedRegistrationEvidence(text) {
|
|
25
|
+
if (!/^[ \t]*Relevant dates[ \t]*:[ \t]*$/im.test(text)
|
|
26
|
+
|| !/^[ \t]*Registration status[ \t]*:[ \t]*$/im.test(text)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const domain = parseIndentedWhoisValue(text, /^[ \t]*Domain(?: name)?[ \t]*:[ \t]*$/im, whoisFieldLimit('domainName'));
|
|
30
|
+
const status = parseIndentedWhoisValue(text, /^[ \t]*Registration status[ \t]*:[ \t]*$/im, 160);
|
|
31
|
+
return Boolean(domain?.value && status?.value);
|
|
32
|
+
}
|
|
33
|
+
export function hasNicKgRegistrationEvidence(text) {
|
|
34
|
+
return /^% This is the \.kg ccTLD Whois server[ \t]*$/im.test(text)
|
|
35
|
+
&& /^[ \t]*Domain[ \t]+[a-z0-9.-]+[ \t]+\([A-Z][A-Z0-9_-]*\)[ \t]*$/im.test(text)
|
|
36
|
+
&& /^[ \t]*Record created[ \t]*:[ \t]*\S/im.test(text)
|
|
37
|
+
&& /^[ \t]*Name servers in the listed order[ \t]*:[ \t]*$/im.test(text);
|
|
38
|
+
}
|
|
39
|
+
function classifyHopEvidence(hop, index) {
|
|
40
|
+
if (hop.error)
|
|
41
|
+
return 'error';
|
|
42
|
+
const text = hop.response || '';
|
|
43
|
+
if (!text.trim())
|
|
44
|
+
return 'inconclusive';
|
|
45
|
+
if (RATE_LIMIT_LINE_RE.test(text))
|
|
46
|
+
return 'rate_limited';
|
|
47
|
+
if (NZ_TEMPORARY_FAILURE_RE.test(text))
|
|
48
|
+
return 'rate_limited';
|
|
49
|
+
if (NZ_NOT_FOUND_RE.test(text))
|
|
50
|
+
return 'negative';
|
|
51
|
+
if (NOT_FOUND_RE.test(text)
|
|
52
|
+
|| LINE_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
53
|
+
return 'negative';
|
|
54
|
+
}
|
|
55
|
+
if (index > 0
|
|
56
|
+
&& (POSITIVE_REGISTRATION_RE.test(text)
|
|
57
|
+
|| POSITIVE_BRACKET_RE.test(text)
|
|
58
|
+
|| NZ_POSITIVE_RE.test(text)
|
|
59
|
+
|| hasSectionedRegistrationEvidence(text)
|
|
60
|
+
|| hasNicKgRegistrationEvidence(text))) {
|
|
61
|
+
return 'positive';
|
|
62
|
+
}
|
|
63
|
+
return 'inconclusive';
|
|
64
|
+
}
|
|
65
|
+
export function analyzeWhoisChainAuthority(chain) {
|
|
66
|
+
const source = normalizeWhoisChain(chain);
|
|
67
|
+
const evidence = source.map((hop, index) => ({
|
|
68
|
+
server: hop.server,
|
|
69
|
+
index,
|
|
70
|
+
kind: classifyHopEvidence(hop, index),
|
|
71
|
+
}));
|
|
72
|
+
const failed = evidence.filter((item) => item.kind === 'error' || item.kind === 'rate_limited');
|
|
73
|
+
const authoritative = evidence.find((item) => item.index > 0
|
|
74
|
+
&& (item.kind === 'positive' || item.kind === 'negative'));
|
|
75
|
+
const conflict = authoritative
|
|
76
|
+
? evidence.find((item) => item.index > authoritative.index
|
|
77
|
+
&& (item.kind === 'positive' || item.kind === 'negative')
|
|
78
|
+
&& item.kind !== authoritative.kind)
|
|
79
|
+
: null;
|
|
80
|
+
const registrationStatus = !authoritative
|
|
81
|
+
? 'inconclusive'
|
|
82
|
+
: authoritative.kind === 'positive'
|
|
83
|
+
? 'registered'
|
|
84
|
+
: 'not_found';
|
|
85
|
+
return {
|
|
86
|
+
registrationStatus,
|
|
87
|
+
notFound: registrationStatus === 'not_found',
|
|
88
|
+
notFoundSource: registrationStatus === 'not_found' && authoritative
|
|
89
|
+
? authoritative.server
|
|
90
|
+
: null,
|
|
91
|
+
authoritativeHop: authoritative?.server ?? null,
|
|
92
|
+
failedHop: failed[0]?.server ?? null,
|
|
93
|
+
conflictingHop: conflict?.server ?? null,
|
|
94
|
+
chainStatus: authoritative && failed.length === 0 && !conflict
|
|
95
|
+
? 'complete'
|
|
96
|
+
: 'partial',
|
|
97
|
+
};
|
|
98
|
+
}
|