@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -6,11 +6,28 @@
|
|
|
6
6
|
// retained.
|
|
7
7
|
import { analyzeBrowserLibraries } from "./browser-library-profile.mjs";
|
|
8
8
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { attributeValue, createTechnologyMarkupContext, elementMarker, hasAttributes, navigationMarker, resourceMarker, } from "./technology-markup.mjs";
|
|
10
|
+
import { MAX_STATIC_HTML_CHARS, MAX_STATIC_HTML_TAGS as MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
10
11
|
import { TECHNOLOGY_EVIDENCE_ROLE_ORDER, } from "./technology-evidence-role.mjs";
|
|
11
12
|
import { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, TECHNOLOGY_PROFILE_VERSION, } from "./lookup-child-profile-contract.mjs";
|
|
13
|
+
const TECHNOLOGY_CATEGORIES = Object.freeze([
|
|
14
|
+
'application runtime',
|
|
15
|
+
'content management',
|
|
16
|
+
'commerce',
|
|
17
|
+
'site builder',
|
|
18
|
+
'web framework',
|
|
19
|
+
'static site generator',
|
|
20
|
+
'web server',
|
|
21
|
+
'delivery platform',
|
|
22
|
+
]);
|
|
23
|
+
const TECHNOLOGY_EVIDENCE_SOURCES = Object.freeze([
|
|
24
|
+
'generator metadata',
|
|
25
|
+
'static HTML',
|
|
26
|
+
'resource origin',
|
|
27
|
+
'HTTP server header',
|
|
28
|
+
'passive response header',
|
|
29
|
+
]);
|
|
12
30
|
const MAX_TECHNOLOGY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
|
|
13
|
-
const MAX_TECHNOLOGY_TAG_LENGTH = MAX_TAG_LENGTH;
|
|
14
31
|
const MAX_RESOURCE_ORIGINS = 30;
|
|
15
32
|
const MAX_GENERATOR_INPUT = 160;
|
|
16
33
|
const MAX_SERVER_INPUT = 240;
|
|
@@ -98,12 +115,16 @@ function generatorEvidence(pattern, description) {
|
|
|
98
115
|
matches: ({ generator }) => pattern.test(generator),
|
|
99
116
|
};
|
|
100
117
|
}
|
|
101
|
-
function htmlEvidence(
|
|
118
|
+
function htmlEvidence(markupRules, description, confidence = 'high') {
|
|
102
119
|
return {
|
|
103
120
|
source: 'static HTML',
|
|
104
121
|
description,
|
|
105
122
|
confidence,
|
|
106
|
-
|
|
123
|
+
markupRules,
|
|
124
|
+
matches: (context) => markupRules.some((rule) => rule.matches(context)),
|
|
125
|
+
roleFor: (context) => markupRules.some((rule) => rule.matches(context) && !rule.embedded?.(context))
|
|
126
|
+
? undefined
|
|
127
|
+
: 'embedded_dependency',
|
|
107
128
|
};
|
|
108
129
|
}
|
|
109
130
|
function resourceEvidence(hosts, description) {
|
|
@@ -133,7 +154,7 @@ function serverEvidence(pattern, description, confidence = 'high') {
|
|
|
133
154
|
function responseHeaderEvidence(name, pattern, description, confidence = 'high') {
|
|
134
155
|
return {
|
|
135
156
|
source: 'passive response header',
|
|
136
|
-
description
|
|
157
|
+
description: `${name}: ${description}`,
|
|
137
158
|
confidence,
|
|
138
159
|
matches: ({ responseHeaders }) => {
|
|
139
160
|
if (!responseHeaders.has(name))
|
|
@@ -142,7 +163,10 @@ function responseHeaderEvidence(name, pattern, description, confidence = 'high')
|
|
|
142
163
|
},
|
|
143
164
|
};
|
|
144
165
|
}
|
|
145
|
-
function evidenceRole(signature, evidence) {
|
|
166
|
+
function evidenceRole(signature, evidence, context) {
|
|
167
|
+
const contextualRole = context ? evidence.roleFor?.(context) : undefined;
|
|
168
|
+
if (contextualRole)
|
|
169
|
+
return contextualRole;
|
|
146
170
|
if (evidence.role)
|
|
147
171
|
return evidence.role;
|
|
148
172
|
if (evidence.source === 'resource origin')
|
|
@@ -159,14 +183,20 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
159
183
|
id: 'wordpress', name: 'WordPress', category: 'content management',
|
|
160
184
|
evidence: [
|
|
161
185
|
generatorEvidence(/^wordpress(?:\s|$)/i, 'Generator metadata identifies WordPress.'),
|
|
162
|
-
htmlEvidence([
|
|
186
|
+
htmlEvidence([
|
|
187
|
+
resourceMarker((url) => /\/wp-content\//iu.test(url.pathname), '/wp-content/fixture.css'),
|
|
188
|
+
resourceMarker((url) => /\/wp-includes\//iu.test(url.pathname), '/wp-includes/fixture.js'),
|
|
189
|
+
], 'Static resource paths use WordPress conventions.', 'medium'),
|
|
163
190
|
],
|
|
164
191
|
},
|
|
165
192
|
{
|
|
166
193
|
id: 'drupal', name: 'Drupal', category: 'content management',
|
|
167
194
|
evidence: [
|
|
168
195
|
generatorEvidence(/^drupal(?:\s|$)/i, 'Generator metadata identifies Drupal.'),
|
|
169
|
-
htmlEvidence([
|
|
196
|
+
htmlEvidence([
|
|
197
|
+
elementMarker((element) => hasAttributes(element, 'data-drupal-selector'), '<main data-drupal-selector="fixture"></main>'),
|
|
198
|
+
elementMarker((element) => hasAttributes(element, 'data-drupal-link-system-path'), '<main data-drupal-link-system-path="fixture"></main>'),
|
|
199
|
+
], 'Static markup contains Drupal-specific attributes.'),
|
|
170
200
|
responseHeaderEvidence('x-drupal-cache', null, 'A Drupal-specific cache response header was observed.', 'medium'),
|
|
171
201
|
],
|
|
172
202
|
},
|
|
@@ -178,7 +208,10 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
178
208
|
id: 'ghost', name: 'Ghost', category: 'content management',
|
|
179
209
|
evidence: [
|
|
180
210
|
generatorEvidence(/^ghost(?:\s|$)/i, 'Generator metadata identifies Ghost.'),
|
|
181
|
-
htmlEvidence([
|
|
211
|
+
htmlEvidence([
|
|
212
|
+
elementMarker((element) => hasAttributes(element, 'data-ghost-search'), '<main data-ghost-search></main>'),
|
|
213
|
+
resourceMarker((url) => /\/ghost\/api\/content\//iu.test(url.pathname), '/ghost/api/content/'),
|
|
214
|
+
], 'Static markup contains Ghost-specific integration markers.'),
|
|
182
215
|
],
|
|
183
216
|
},
|
|
184
217
|
{
|
|
@@ -195,7 +228,10 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
195
228
|
id: 'shopify', name: 'Shopify', category: 'commerce',
|
|
196
229
|
requiresNonResourceEvidence: true,
|
|
197
230
|
evidence: [
|
|
198
|
-
htmlEvidence([
|
|
231
|
+
htmlEvidence([
|
|
232
|
+
elementMarker((element) => attributeValue(element, 'class').split(/\s/u).includes('shopify-section')
|
|
233
|
+
|| /^shopify-section(?:-|$)/u.test(attributeValue(element, 'id')), '<section class="shopify-section"></section>'),
|
|
234
|
+
], 'Static markup contains Shopify-specific storefront markers.'),
|
|
199
235
|
resourceEvidence(['cdn.shopify.com'], 'A retained resource origin uses the Shopify content network.'),
|
|
200
236
|
responseHeaderEvidence('x-shopify-stage', null, 'A Shopify-specific platform response header was observed.', 'medium'),
|
|
201
237
|
responseHeaderEvidence('x-sorting-hat-podid', null, 'A Shopify-specific routing response header was observed.', 'medium'),
|
|
@@ -216,33 +252,43 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
216
252
|
{
|
|
217
253
|
id: 'adobe-commerce-magento', name: 'Adobe Commerce / Magento Open Source', category: 'commerce',
|
|
218
254
|
evidence: [
|
|
219
|
-
htmlEvidence([
|
|
255
|
+
htmlEvidence([
|
|
256
|
+
elementMarker((element) => hasAttributes(element, 'data-mage-init'), '<main data-mage-init="{}"></main>'),
|
|
257
|
+
elementMarker((element) => element.name === 'script' && attributeValue(element, 'type') === 'text/x-magento-init', '<script type="text/x-magento-init">{}</script>'),
|
|
258
|
+
], 'Static markup contains Commerce frontend initialisation markers.'),
|
|
220
259
|
],
|
|
221
260
|
},
|
|
222
261
|
{
|
|
223
262
|
id: 'bigcommerce', name: 'BigCommerce', category: 'commerce',
|
|
224
263
|
minimumEvidenceMatches: 2,
|
|
264
|
+
allowEmbeddedOnly: true,
|
|
225
265
|
evidence: [
|
|
226
|
-
htmlEvidence([
|
|
266
|
+
htmlEvidence([
|
|
267
|
+
resourceMarker((url) => /(?:^|\/)stencil-utils(?:[./-]|$)/iu.test(url.pathname), '/assets/stencil-utils.js'),
|
|
268
|
+
resourceMarker((url) => /^cdn\d+\.bigcommerce\.com$/iu.test(url.hostname) && /^\/s-[^/]+\//iu.test(url.pathname), 'https://cdn11.bigcommerce.com/s-fixture/theme.css'),
|
|
269
|
+
], 'Static markup contains BigCommerce storefront asset markers.', 'medium'),
|
|
227
270
|
resourcePatternEvidence(/^cdn\d+\.bigcommerce\.com$/i, 'A retained resource origin uses BigCommerce storefront delivery infrastructure.'),
|
|
228
271
|
],
|
|
229
272
|
},
|
|
230
273
|
{
|
|
231
274
|
id: 'woocommerce', name: 'WooCommerce', category: 'commerce',
|
|
232
275
|
evidence: [
|
|
233
|
-
htmlEvidence(['/wp-content/plugins/woocommerce/'], 'Static resource paths identify the WooCommerce plugin.'),
|
|
276
|
+
htmlEvidence([resourceMarker((url) => /\/wp-content\/plugins\/woocommerce\//iu.test(url.pathname), '/wp-content/plugins/woocommerce/fixture.css')], 'Static resource paths identify the WooCommerce plugin.'),
|
|
234
277
|
],
|
|
235
278
|
},
|
|
236
279
|
{
|
|
237
280
|
id: 'opencart', name: 'OpenCart', category: 'commerce',
|
|
238
281
|
evidence: [
|
|
239
|
-
htmlEvidence([
|
|
282
|
+
htmlEvidence([
|
|
283
|
+
navigationMarker((url) => /\/index\.php$/iu.test(url.pathname) && url.searchParams.getAll('route').length === 1 && url.searchParams.get('route')?.toLowerCase() === 'common/home', 'index.php?route=common/home'),
|
|
284
|
+
resourceMarker((url) => /\/image\/catalog\/opencart-logo\.png$/iu.test(url.pathname), '/image/catalog/opencart-logo.png'),
|
|
285
|
+
], 'Static markup contains OpenCart routing or default asset conventions.'),
|
|
240
286
|
],
|
|
241
287
|
},
|
|
242
288
|
{
|
|
243
289
|
id: 'prestashop', name: 'PrestaShop', category: 'commerce',
|
|
244
290
|
evidence: [
|
|
245
|
-
htmlEvidence(['/modules/
|
|
291
|
+
htmlEvidence([resourceMarker((url) => /\/modules\/ps_[^/]+\//iu.test(url.pathname), '/modules/ps_fixture/fixture.css')], 'Static resource paths use PrestaShop module conventions.'),
|
|
246
292
|
],
|
|
247
293
|
},
|
|
248
294
|
{
|
|
@@ -250,24 +296,29 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
250
296
|
requiresNonResourceEvidence: true,
|
|
251
297
|
evidence: [
|
|
252
298
|
generatorEvidence(/^wix(?:\.com)?(?:\s|$)/i, 'Generator metadata identifies Wix.'),
|
|
253
|
-
htmlEvidence(['data-mesh-id='], 'Static markup contains a Wix-specific document attribute.', 'medium'),
|
|
299
|
+
htmlEvidence([elementMarker((element) => hasAttributes(element, 'data-mesh-id'), '<main data-mesh-id="fixture"></main>')], 'Static markup contains a Wix-specific document attribute.', 'medium'),
|
|
254
300
|
resourceEvidence(['static.parastorage.com', 'wixstatic.com'], 'A retained resource origin uses Wix delivery infrastructure.'),
|
|
255
301
|
],
|
|
256
302
|
},
|
|
257
303
|
{
|
|
258
304
|
id: 'squarespace', name: 'Squarespace', category: 'site builder',
|
|
259
305
|
requiresNonResourceEvidence: true,
|
|
306
|
+
allowEmbeddedOnly: true,
|
|
260
307
|
evidence: [
|
|
261
308
|
generatorEvidence(/^squarespace(?:\s|$)/i, 'Generator metadata identifies Squarespace.'),
|
|
262
|
-
htmlEvidence([
|
|
263
|
-
|
|
309
|
+
htmlEvidence([
|
|
310
|
+
resourceMarker((url) => url.hostname === 'assets.squarespace.com' && /^\/universal\/scripts-compressed\/[a-z0-9][a-z0-9.-]*\.js$/iu.test(url.pathname), 'https://assets.squarespace.com/universal/scripts-compressed/fixture.js'),
|
|
311
|
+
], 'Static resource paths use Squarespace platform bundle conventions.', 'medium'),
|
|
264
312
|
],
|
|
265
313
|
},
|
|
266
314
|
{
|
|
267
315
|
id: 'webflow', name: 'Webflow', category: 'site builder',
|
|
268
316
|
evidence: [
|
|
269
317
|
generatorEvidence(/^webflow(?:\s|$)/i, 'Generator metadata identifies Webflow.'),
|
|
270
|
-
htmlEvidence([
|
|
318
|
+
htmlEvidence([
|
|
319
|
+
elementMarker((element) => hasAttributes(element, 'data-wf-page'), '<main data-wf-page="fixture"></main>'),
|
|
320
|
+
elementMarker((element) => hasAttributes(element, 'data-wf-site'), '<main data-wf-site="fixture"></main>'),
|
|
321
|
+
], 'Static markup contains Webflow-specific document attributes.'),
|
|
271
322
|
],
|
|
272
323
|
},
|
|
273
324
|
{
|
|
@@ -275,7 +326,7 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
275
326
|
requiresNonResourceEvidence: true,
|
|
276
327
|
evidence: [
|
|
277
328
|
generatorEvidence(/^framer(?:\s|$)/i, 'Generator metadata identifies Framer.'),
|
|
278
|
-
htmlEvidence(['data-framer-name='], 'Static markup contains Framer-specific component attributes.'),
|
|
329
|
+
htmlEvidence([elementMarker((element) => hasAttributes(element, 'data-framer-name'), '<main data-framer-name="fixture"></main>')], 'Static markup contains Framer-specific component attributes.'),
|
|
279
330
|
resourceEvidence(['framerusercontent.com'], 'A retained resource origin uses Framer delivery infrastructure.'),
|
|
280
331
|
],
|
|
281
332
|
},
|
|
@@ -283,42 +334,57 @@ const TECHNOLOGY_SIGNATURES = [
|
|
|
283
334
|
id: 'weebly', name: 'Weebly', category: 'site builder',
|
|
284
335
|
requiresNonResourceEvidence: true,
|
|
285
336
|
evidence: [
|
|
286
|
-
htmlEvidence(['
|
|
337
|
+
htmlEvidence([elementMarker((element) => element.name === 'link' && (attributeValue(element, 'id') === 'wsite-base-style' || attributeValue(element, 'title') === 'wsite-theme-css'), '<link id="wsite-base-style" href="/fixture.css">')], 'Static markup contains Weebly-specific theme attributes.'),
|
|
287
338
|
resourceEvidence(['editmysite.com'], 'A retained resource origin uses Weebly delivery infrastructure.'),
|
|
288
339
|
],
|
|
289
340
|
},
|
|
290
341
|
{
|
|
291
342
|
id: 'angular', name: 'Angular', category: 'web framework',
|
|
292
|
-
evidence: [htmlEvidence([' ng-version='], 'Static markup contains Angular version metadata.')],
|
|
343
|
+
evidence: [htmlEvidence([elementMarker((element) => hasAttributes(element, 'ng-version'), '<main ng-version="fixture"></main>')], 'Static markup contains Angular version metadata.')],
|
|
293
344
|
},
|
|
294
345
|
{
|
|
295
346
|
id: 'aspnet-web-forms', name: 'ASP.NET Web Forms', category: 'web framework',
|
|
296
|
-
evidence: [htmlEvidence([' name
|
|
347
|
+
evidence: [htmlEvidence([elementMarker((element) => element.name === 'input' && (attributeValue(element, 'name') === '__viewstate' || attributeValue(element, 'id') === '__viewstate'), '<input name="__VIEWSTATE">')], 'Static form markup contains the ASP.NET Web Forms view-state field.')],
|
|
297
348
|
},
|
|
298
349
|
{
|
|
299
350
|
id: 'nextjs', name: 'Next.js', category: 'web framework',
|
|
300
|
-
evidence: [htmlEvidence([
|
|
351
|
+
evidence: [htmlEvidence([
|
|
352
|
+
elementMarker((element) => element.name === 'script' && attributeValue(element, 'id') === '__next_data__', '<script id="__NEXT_DATA__"></script>'),
|
|
353
|
+
resourceMarker((url) => /\/_next\/static\//iu.test(url.pathname), '/_next/static/fixture.js'),
|
|
354
|
+
], 'Static markup contains Next.js bootstrap or asset markers.')],
|
|
301
355
|
},
|
|
302
356
|
{
|
|
303
357
|
id: 'nuxt', name: 'Nuxt', category: 'web framework',
|
|
304
|
-
evidence: [htmlEvidence([
|
|
358
|
+
evidence: [htmlEvidence([
|
|
359
|
+
elementMarker((element) => attributeValue(element, 'id') === '__nuxt', '<main id="__nuxt"></main>'),
|
|
360
|
+
resourceMarker((url) => /\/_nuxt\//iu.test(url.pathname), '/_nuxt/fixture.js'),
|
|
361
|
+
], 'Static markup contains Nuxt bootstrap or asset markers.')],
|
|
305
362
|
},
|
|
306
363
|
{
|
|
307
364
|
id: 'gatsby', name: 'Gatsby', category: 'web framework',
|
|
308
|
-
evidence: [htmlEvidence([
|
|
365
|
+
evidence: [htmlEvidence([
|
|
366
|
+
elementMarker((element) => attributeValue(element, 'id') === '___gatsby', '<main id="___gatsby"></main>'),
|
|
367
|
+
resourceMarker((url) => /\/page-data\/app-data\.json$/iu.test(url.pathname), '/page-data/app-data.json'),
|
|
368
|
+
], 'Static markup contains Gatsby bootstrap or page-data markers.')],
|
|
309
369
|
},
|
|
310
370
|
{
|
|
311
371
|
id: 'sveltekit', name: 'SvelteKit', category: 'web framework',
|
|
312
372
|
evidence: [
|
|
313
|
-
htmlEvidence([
|
|
314
|
-
|
|
373
|
+
htmlEvidence([
|
|
374
|
+
elementMarker((element) => hasAttributes(element, 'data-sveltekit-preload-data'), '<a data-sveltekit-preload-data="hover"></a>'),
|
|
375
|
+
elementMarker((element) => hasAttributes(element, 'data-sveltekit-reload'), '<a data-sveltekit-reload></a>'),
|
|
376
|
+
], 'Static markup contains SvelteKit-specific navigation attributes.'),
|
|
377
|
+
htmlEvidence([resourceMarker((url) => /\/_app\/immutable\//iu.test(url.pathname), '/_app/immutable/fixture.css')], 'Static asset paths use SvelteKit build conventions.', 'medium'),
|
|
315
378
|
],
|
|
316
379
|
},
|
|
317
380
|
{
|
|
318
381
|
id: 'astro', name: 'Astro', category: 'web framework',
|
|
319
382
|
evidence: [
|
|
320
|
-
htmlEvidence([
|
|
321
|
-
|
|
383
|
+
htmlEvidence([
|
|
384
|
+
elementMarker((element) => element.name === 'astro-island', '<astro-island></astro-island>'),
|
|
385
|
+
elementMarker((element) => element.name === 'astro-slot', '<astro-slot></astro-slot>'),
|
|
386
|
+
], 'Static markup contains Astro component-island elements.'),
|
|
387
|
+
htmlEvidence([resourceMarker((url) => /\/_astro\//iu.test(url.pathname), '/_astro/fixture.css')], 'Static asset paths use Astro build conventions.', 'medium'),
|
|
322
388
|
],
|
|
323
389
|
},
|
|
324
390
|
{
|
|
@@ -406,43 +472,85 @@ const TECHNOLOGY_SIGNATURE_CATALOGUE = Object.freeze(TECHNOLOGY_SIGNATURES.map((
|
|
|
406
472
|
confidence: evidence.confidence,
|
|
407
473
|
}))),
|
|
408
474
|
})));
|
|
409
|
-
function
|
|
475
|
+
function technologyHtmlAnalysis(input) {
|
|
476
|
+
return input.htmlAnalysis ?? analyzeStaticHtml(input.html, {
|
|
477
|
+
...(typeof input.documentOrigin === 'string' ? { baseUrl: input.documentOrigin } : {}),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
// These are fixed semantic reconstructions, not copies of observed markup.
|
|
481
|
+
// Keeping them beside the predicates removes a second keyword catalogue from
|
|
482
|
+
// the contribution tools. Independent structural fixtures test the predicates.
|
|
483
|
+
function minimiseTechnologyMarkup(input) {
|
|
484
|
+
const analysis = technologyHtmlAnalysis(input);
|
|
485
|
+
if (analysis.inputLimitReached || analysis.tagLimitReached) {
|
|
486
|
+
throw new TypeError('Technology review markup exceeded the complete structural-analysis boundary.');
|
|
487
|
+
}
|
|
488
|
+
const context = createTechnologyMarkupContext(analysis, input.effectiveBaseUrl, input.documentOrigin);
|
|
489
|
+
const fragments = TECHNOLOGY_SIGNATURES.flatMap((signature) => signature.evidence.flatMap((evidence) => ((evidence.markupRules ?? []).flatMap((rule) => rule.matches(context)
|
|
490
|
+
? [rule.embedded?.(context) ? rule.embeddedReviewMarkup : rule.reviewMarkup]
|
|
491
|
+
: []))));
|
|
492
|
+
return [...new Set(fragments)].join('');
|
|
493
|
+
}
|
|
494
|
+
function reconstructTechnologyHtmlEvidence(id, description, role) {
|
|
495
|
+
const rules = TECHNOLOGY_SIGNATURES.find((signature) => signature.id === id)?.evidence
|
|
496
|
+
.find((evidence) => evidence.source === 'static HTML' && evidence.description === description)?.markupRules;
|
|
497
|
+
const markup = role === 'embedded_dependency'
|
|
498
|
+
? rules?.find((rule) => rule.embeddedReviewMarkup)?.embeddedReviewMarkup
|
|
499
|
+
: rules?.[0]?.reviewMarkup;
|
|
500
|
+
if (!markup)
|
|
501
|
+
throw new TypeError('The retained static clue has no current structural reconstruction.');
|
|
502
|
+
return markup;
|
|
503
|
+
}
|
|
504
|
+
async function analyzeWebsiteTechnology(input = {}) {
|
|
505
|
+
input.signal?.throwIfAborted();
|
|
410
506
|
// Existing direct callers pass minimised, already-derived page evidence rather
|
|
411
507
|
// than the page body. The real collector declares false for header-only
|
|
412
508
|
// responses, while an explicit malformed declaration also fails closed.
|
|
413
509
|
const htmlAvailable = input.htmlAvailable === undefined
|
|
414
510
|
? true
|
|
415
511
|
: input.htmlAvailable === true;
|
|
416
|
-
const htmlAnalysis = input
|
|
417
|
-
const browserLibraryProfile = htmlAvailable ? analyzeBrowserLibraries({
|
|
512
|
+
const htmlAnalysis = htmlAvailable ? technologyHtmlAnalysis(input) : analyzeStaticHtml('');
|
|
513
|
+
const browserLibraryProfile = htmlAvailable ? await analyzeBrowserLibraries({
|
|
418
514
|
htmlAnalysis,
|
|
419
515
|
observedAt: input.observedAt,
|
|
420
516
|
sourceTruncated: input.sourceTruncated,
|
|
517
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
421
518
|
}) : null;
|
|
422
519
|
const context = {
|
|
423
|
-
|
|
424
|
-
generator: boundedLowercase(input.generator, MAX_GENERATOR_INPUT),
|
|
520
|
+
...createTechnologyMarkupContext(htmlAnalysis, input.effectiveBaseUrl, input.documentOrigin),
|
|
521
|
+
generator: htmlAvailable ? boundedLowercase(input.generator, MAX_GENERATOR_INPUT) : '',
|
|
425
522
|
httpServer: boundedLowercase(input.httpServer, MAX_SERVER_INPUT),
|
|
426
|
-
resourceHosts: normalizedResourceHosts(input.resourceOrigins),
|
|
523
|
+
resourceHosts: htmlAvailable ? normalizedResourceHosts(input.resourceOrigins) : new Set(),
|
|
427
524
|
responseHeaders: normalizedResponseHeaders(input.responseHeaders),
|
|
428
525
|
};
|
|
429
526
|
const findings = [];
|
|
430
527
|
for (const signature of TECHNOLOGY_SIGNATURES) {
|
|
431
|
-
const matched = signature.evidence
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
528
|
+
const matched = signature.evidence
|
|
529
|
+
.filter((evidence) => evidence.matches(context))
|
|
530
|
+
.map((evidence) => ({ evidence, role: evidenceRole(signature, evidence, context) }));
|
|
531
|
+
const nonEmbeddedMatches = matched.filter((entry) => entry.role !== 'embedded_dependency');
|
|
532
|
+
const minimumEvidenceMatches = signature.minimumEvidenceMatches || 1;
|
|
533
|
+
const qualifiesForPlatform = matched.length >= minimumEvidenceMatches
|
|
534
|
+
&& (minimumEvidenceMatches === 1 || nonEmbeddedMatches.length > 0)
|
|
535
|
+
&& (!signature.requiresNonResourceEvidence || nonEmbeddedMatches.length > 0);
|
|
536
|
+
const contextualEmbeddedEvidence = matched.filter((entry) => (entry.role === 'embedded_dependency' && entry.evidence.roleFor !== undefined));
|
|
537
|
+
const selected = qualifiesForPlatform
|
|
538
|
+
? matched
|
|
539
|
+
: signature.allowEmbeddedOnly
|
|
540
|
+
? contextualEmbeddedEvidence.slice(0, 1)
|
|
541
|
+
: [];
|
|
542
|
+
if (selected.length === 0)
|
|
435
543
|
continue;
|
|
436
|
-
const matchedEvidence =
|
|
544
|
+
const matchedEvidence = selected.slice(0, MAX_EVIDENCE_PER_TECHNOLOGY).map(({ evidence, role }) => ({
|
|
437
545
|
source: evidence.source,
|
|
438
|
-
role
|
|
546
|
+
role,
|
|
439
547
|
description: evidence.description,
|
|
440
548
|
}));
|
|
441
549
|
findings.push({
|
|
442
550
|
id: signature.id,
|
|
443
551
|
name: signature.name,
|
|
444
552
|
category: signature.category,
|
|
445
|
-
confidence:
|
|
553
|
+
confidence: selected.some(({ evidence }) => evidence.confidence === 'high') ? 'high' : 'medium',
|
|
446
554
|
roles: TECHNOLOGY_EVIDENCE_ROLE_ORDER.filter((role) => matchedEvidence.some((evidence) => evidence.role === role)),
|
|
447
555
|
evidence: matchedEvidence,
|
|
448
556
|
});
|
|
@@ -466,7 +574,7 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
466
574
|
if (htmlAnalysis.inputLimitReached)
|
|
467
575
|
limitations.push(`Only the first ${MAX_TECHNOLOGY_HTML_CHARS} HTML characters were evaluated.`);
|
|
468
576
|
if (htmlAnalysis.tagLimitReached)
|
|
469
|
-
limitations.push(
|
|
577
|
+
limitations.push('Static HTML construction or attribute projection reached a bound; later technology indicators may be missing.');
|
|
470
578
|
if (findingLimitReached)
|
|
471
579
|
limitations.push(`Only the first ${MAX_TECHNOLOGY_FINDINGS} technology findings were retained.`);
|
|
472
580
|
return {
|
|
@@ -493,4 +601,4 @@ function analyzeWebsiteTechnology(input = {}) {
|
|
|
493
601
|
browserLibraryProfile,
|
|
494
602
|
};
|
|
495
603
|
}
|
|
496
|
-
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, PASSIVE_TECHNOLOGY_HEADER_NAMES, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, captureTechnologyResponseHeaders, };
|
|
604
|
+
export { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH, MAX_TECHNOLOGY_FINDINGS, MAX_TECHNOLOGY_HTML_CHARS, MAX_TECHNOLOGY_TAGS, PASSIVE_TECHNOLOGY_HEADER_NAMES, TECHNOLOGY_CATEGORIES, TECHNOLOGY_EVIDENCE_SOURCES, TECHNOLOGY_PROFILE_VERSION, TECHNOLOGY_SIGNATURE_CATALOGUE, analyzeWebsiteTechnology, captureTechnologyResponseHeaders, minimiseTechnologyMarkup, reconstructTechnologyHtmlEvidence, };
|
package/lib/whois-authority.mjs
CHANGED
|
@@ -29,7 +29,10 @@ const LINE_NOT_FOUND_PATTERNS = Object.freeze([
|
|
|
29
29
|
const NZ_NOT_FOUND_RE = /^[ \t]*query_status[ \t]*:[ \t]*220(?:\s|$)/im;
|
|
30
30
|
const NZ_POSITIVE_RE = /^[ \t]*query_status[ \t]*:[ \t]*(?:200|210)(?:\s|$)/im;
|
|
31
31
|
const NZ_TEMPORARY_FAILURE_RE = /^[ \t]*query_status[ \t]*:[ \t]*4\d{2}(?:\s|$)/im;
|
|
32
|
-
|
|
32
|
+
// A refusal describes this response; a footer describing permitted query
|
|
33
|
+
// frequency does not. Keep explicit refusals ahead of echoed domain fields.
|
|
34
|
+
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(?:ed|[ \t]+(?:exceeded|reached))|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)\b[^\r\n]{0,240}$/im;
|
|
35
|
+
const RETRY_LINE_RE = /^[ \t]*(?:[%#*;>-]+[ \t]*)?(?:(?:error|status)[ \t:.-]+)?(?:rate[ \t-]*limit|(?:please[ \t]+)?try[ \t]+again[ \t]+later|please[ \t]+wait(?:[ \t]+\d{1,6}[ \t]+(?:seconds?|minutes?|hours?))?(?:[ \t]+(?:and[ \t]+)?(?:retry|try[ \t]+again)(?:[ \t]+later)?)?)[.!]?[ \t]*$/im;
|
|
33
36
|
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;
|
|
34
37
|
const POSITIVE_BRACKET_RE = /\[(?:Domain Name|Registrant|Name Server)\][ \t]*\S/i;
|
|
35
38
|
function hasSectionedRegistrationEvidence(text) {
|
|
@@ -53,7 +56,7 @@ function classifyHopEvidence(hop, index) {
|
|
|
53
56
|
const text = hop.response || '';
|
|
54
57
|
if (!text.trim())
|
|
55
58
|
return 'inconclusive';
|
|
56
|
-
if (RATE_LIMIT_LINE_RE.test(text))
|
|
59
|
+
if (RATE_LIMIT_LINE_RE.test(text) || RETRY_LINE_RE.test(text))
|
|
57
60
|
return 'rate_limited';
|
|
58
61
|
if (NZ_TEMPORARY_FAILURE_RE.test(text))
|
|
59
62
|
return 'rate_limited';
|
|
@@ -131,3 +134,12 @@ export function analyzeWhoisChainAuthority(chain) {
|
|
|
131
134
|
: 'partial',
|
|
132
135
|
};
|
|
133
136
|
}
|
|
137
|
+
/** Collection status shared by final and incremental Lookup presentation. */
|
|
138
|
+
export function whoisCollectionStatus(chain) {
|
|
139
|
+
const source = normalizeWhoisChain(chain);
|
|
140
|
+
if (!source.length || source[0]?.error)
|
|
141
|
+
return 'error';
|
|
142
|
+
if (source.length === 1)
|
|
143
|
+
return 'unsupported';
|
|
144
|
+
return analyzeWhoisChainAuthority(source).chainStatus;
|
|
145
|
+
}
|
package/lib/whois-chain.mjs
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// bounded referrals.
|
|
4
4
|
import { domainToUnicode } from 'node:url';
|
|
5
5
|
import { cached } from "./lookup-cache.mjs";
|
|
6
|
+
import { MAX_WHOIS_QUERY_HOPS } from "./whois-contracts.mjs";
|
|
6
7
|
import { registryCapabilityFor, registryServiceAdmissionFor, } from "./registry-capabilities.mjs";
|
|
7
8
|
import { whoisQuery } from "./whois-transport.mjs";
|
|
8
9
|
const IANA_WHOIS = 'whois.iana.org';
|
|
9
10
|
const WHOIS_HOP_DEADLINE_MS = 12_000;
|
|
10
11
|
const WHOIS_CHAIN_DEADLINE_MS = 25_000;
|
|
11
|
-
const MAX_WHOIS_QUERY_HOPS = 6;
|
|
12
12
|
function incompleteReferralHop(server, reason) {
|
|
13
13
|
return {
|
|
14
14
|
server,
|
|
@@ -58,6 +58,7 @@ function extractReferral(whoisText) {
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
export async function buildWhoisChainUncached(query, options = {}) {
|
|
61
|
+
options.signal?.throwIfAborted();
|
|
61
62
|
const admission = registryServiceAdmissionFor(query, 'whois');
|
|
62
63
|
if (admission && !admission.allowed) {
|
|
63
64
|
return [{
|
|
@@ -77,6 +78,7 @@ export async function buildWhoisChainUncached(query, options = {}) {
|
|
|
77
78
|
let currentServer = IANA_WHOIS;
|
|
78
79
|
const startedAt = now();
|
|
79
80
|
for (let hop = 0; hop < MAX_WHOIS_QUERY_HOPS; hop += 1) {
|
|
81
|
+
options.signal?.throwIfAborted();
|
|
80
82
|
if (visited.has(currentServer.toLowerCase())) {
|
|
81
83
|
chain.push(incompleteReferralHop(currentServer, 'referral_loop'));
|
|
82
84
|
break;
|
|
@@ -101,12 +103,15 @@ export async function buildWhoisChainUncached(query, options = {}) {
|
|
|
101
103
|
responseText = await queryWhois(currentServer, transport.query, {
|
|
102
104
|
timeoutMs: Math.min(10_000, remainingMs),
|
|
103
105
|
totalDeadlineMs: Math.min(WHOIS_HOP_DEADLINE_MS, remainingMs),
|
|
106
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
104
107
|
onAddressSelected: (selected) => {
|
|
105
108
|
address = selected;
|
|
106
109
|
},
|
|
107
110
|
});
|
|
111
|
+
options.signal?.throwIfAborted();
|
|
108
112
|
}
|
|
109
113
|
catch (cause) {
|
|
114
|
+
options.signal?.throwIfAborted();
|
|
110
115
|
chain.push({
|
|
111
116
|
server: currentServer,
|
|
112
117
|
queriedAt,
|
|
@@ -140,6 +145,8 @@ export async function buildWhoisChainUncached(query, options = {}) {
|
|
|
140
145
|
}
|
|
141
146
|
return chain;
|
|
142
147
|
}
|
|
143
|
-
export async function buildWhoisChain(query) {
|
|
148
|
+
export async function buildWhoisChain(query, options = {}) {
|
|
149
|
+
if (options.signal)
|
|
150
|
+
return buildWhoisChainUncached(query, options);
|
|
144
151
|
return cached(`whois:${query.toLowerCase()}`, () => buildWhoisChainUncached(query));
|
|
145
152
|
}
|
|
@@ -176,7 +176,7 @@ function applyWhoisCommonFormats(text, isRootHop, dialects, context) {
|
|
|
176
176
|
}) === 'capped')
|
|
177
177
|
break;
|
|
178
178
|
}
|
|
179
|
-
for (const m of text.matchAll(/\[Name Server\][ \t]*([
|
|
179
|
+
for (const m of text.matchAll(/\[Name Server\][ \t]*([^\s]+)/gi)) {
|
|
180
180
|
if (addBoundedWhoisSetValue(nameservers, m[1], {
|
|
181
181
|
maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
|
|
182
182
|
field: 'nameservers', truncatedFields,
|
|
@@ -188,16 +188,16 @@ function applyWhoisCommonFormats(text, isRootHop, dialects, context) {
|
|
|
188
188
|
// on every hop; "nserver:"/"Host Name:"/"DNS:" are real per-domain
|
|
189
189
|
// labels on some registries (e.g. .ru, .kr, .mx) but only once we're
|
|
190
190
|
// past the root hop, for the same reason as the field patterns above.
|
|
191
|
-
const nsLinePatterns = [/^[ \t*]*Name Server[ \t.]*:[ \t]*([
|
|
191
|
+
const nsLinePatterns = [/^[ \t*]*Name Server[ \t.]*:[ \t]*([^\s]+)/gim];
|
|
192
192
|
if (!isRootHop) {
|
|
193
|
-
nsLinePatterns.push(/^[ \t*]*nserver[ \t.]*:[ \t]*([
|
|
193
|
+
nsLinePatterns.push(/^[ \t*]*nserver[ \t.]*:[ \t]*([^\s]+)/gim, /^[ \t*]*Nameserver[ \t.]*:[ \t]*([^\s]+)/gim, /^[ \t*]*Host Name[ \t.]*:[ \t]*([^\s]+)/gim, /^[ \t]*ns_name_\d{2}[ \t]*:[ \t]*([^\s]+)/gim);
|
|
194
194
|
// Punktum dk uses `DNS: example.dk` for the queried domain, then
|
|
195
195
|
// `Hostname:` inside its nameserver section. Other supported
|
|
196
196
|
// registries use `DNS:` for an actual nameserver, so switch aliases
|
|
197
197
|
// only when the full .dk marker set is present.
|
|
198
198
|
nsLinePatterns.push(isPunktum
|
|
199
|
-
? /^[ \t*]*Hostname[ \t.]*:[ \t]*([
|
|
200
|
-
: /^[ \t*]*DNS[ \t.]*:[ \t]*([
|
|
199
|
+
? /^[ \t*]*Hostname[ \t.]*:[ \t]*([^\s]+)/gim
|
|
200
|
+
: /^[ \t*]*DNS[ \t.]*:[ \t]*([^\s]+)/gim);
|
|
201
201
|
}
|
|
202
202
|
for (const re of nsLinePatterns) {
|
|
203
203
|
for (const m of text.matchAll(re)) {
|
package/lib/whois-contracts.mjs
CHANGED
package/lib/whois-dialects.mjs
CHANGED
|
@@ -263,8 +263,8 @@ function applyWhoisDialects(text, isRootHop, context) {
|
|
|
263
263
|
assignBoundedWhoisMatch(text, fields, 'updatedDate', /^[ \t]*Last modified[ \t.]*:[ \t]*(.+)$/im, truncatedFields);
|
|
264
264
|
assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Current Registar[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
265
265
|
for (const pattern of [
|
|
266
|
-
/^[ \t]*Primary server[ \t.]*:[ \t]*([
|
|
267
|
-
/^[ \t]*Secondary server[ \t.]*:[ \t]*([
|
|
266
|
+
/^[ \t]*Primary server[ \t.]*:[ \t]*([^\s]+)/gim,
|
|
267
|
+
/^[ \t]*Secondary server[ \t.]*:[ \t]*([^\s]+)/gim,
|
|
268
268
|
]) {
|
|
269
269
|
for (const match of text.matchAll(pattern)) {
|
|
270
270
|
const result = addBoundedWhoisSetValue(nameservers, match[1], {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Bounded primitives and generic field patterns shared by the WHOIS parser.
|
|
2
2
|
import { boundedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
|
|
3
|
+
import { MAX_WHOIS_STATUSES } from "./whois-contracts.mjs";
|
|
3
4
|
const MAX_WHOIS_NAMESERVERS = 200;
|
|
4
|
-
const MAX_WHOIS_STATUSES = 100;
|
|
5
5
|
const WHOIS_DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// WHOIS response parsing (merges the referral chain into readable fields)
|
|
@@ -21,6 +21,13 @@ function assignBoundedWhoisMatch(text, fields, key, pattern, truncatedFields) {
|
|
|
21
21
|
}
|
|
22
22
|
function addBoundedWhoisSetValue(set, rawValue, { maxEntries, maxLength, field, truncatedFields, }) {
|
|
23
23
|
const bounded = boundedWhoisValue(rawValue, maxLength);
|
|
24
|
+
if (bounded.truncated)
|
|
25
|
+
truncatedFields.add(field);
|
|
26
|
+
if (field === 'nameservers' && (bounded.truncated || !bounded.value
|
|
27
|
+
|| !WHOIS_DOMAIN_RE.test(bounded.value.replace(/\.$/u, '')))) {
|
|
28
|
+
truncatedFields.add(field);
|
|
29
|
+
return 'ignored';
|
|
30
|
+
}
|
|
24
31
|
if (!bounded.value || set.has(bounded.value))
|
|
25
32
|
return 'ignored';
|
|
26
33
|
if (set.size >= maxEntries) {
|