@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1
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/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/project-metadata.mjs +1 -1
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +13 -5
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
- package/third-party-notices.txt +14 -14
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -255
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -13,6 +13,9 @@ const QUALIFYING_CATEGORIES = new Set(['phishing', 'malware']);
|
|
|
13
13
|
// Only built-in provider IDs can affect the built-in score. The two community
|
|
14
14
|
// malware datasets share one publisher family and therefore cannot corroborate
|
|
15
15
|
// one another by themselves.
|
|
16
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
17
|
+
import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
|
|
18
|
+
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
|
|
16
19
|
const PUBLISHER_FAMILIES = Object.freeze({
|
|
17
20
|
urlscan_search: 'archived-scan-publisher',
|
|
18
21
|
urlhaus_host: 'community-malware-publisher',
|
|
@@ -22,20 +25,24 @@ function record(value) {
|
|
|
22
25
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
23
26
|
}
|
|
24
27
|
function timestamp(value) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const parsed = Date.parse(value);
|
|
28
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
28
|
+
const normalized = normalizeExplicitIsoTimestamp(value);
|
|
29
|
+
return normalized ? Date.parse(normalized) : null;
|
|
29
30
|
}
|
|
30
|
-
function providerEvidence(value) {
|
|
31
|
+
function providerEvidence(value, expectedDomain) {
|
|
31
32
|
const provider = record(value);
|
|
32
33
|
const identity = record(provider?.provider);
|
|
33
34
|
const observation = record(provider?.observation);
|
|
35
|
+
const target = record(provider?.target);
|
|
34
36
|
const providerId = typeof identity?.id === 'string' ? identity.id : '';
|
|
35
37
|
const publisherFamily = Object.hasOwn(PUBLISHER_FAMILIES, providerId)
|
|
36
38
|
? PUBLISHER_FAMILIES[providerId]
|
|
37
39
|
: null;
|
|
38
|
-
if (
|
|
40
|
+
if (provider?.schema !== THREAT_INTELLIGENCE_SCHEMA
|
|
41
|
+
|| provider.version !== THREAT_INTELLIGENCE_CONTRACT_VERSION
|
|
42
|
+
|| target?.type !== 'domain'
|
|
43
|
+
|| target.exposure !== 'registrable_domain'
|
|
44
|
+
|| target.value !== expectedDomain
|
|
45
|
+
|| !publisherFamily
|
|
39
46
|
|| typeof provider?.state !== 'string'
|
|
40
47
|
|| !POSITIVE_STATES.has(provider.state)
|
|
41
48
|
|| !Array.isArray(provider.findings))
|
|
@@ -47,8 +54,12 @@ function providerEvidence(value) {
|
|
|
47
54
|
const finding = record(item);
|
|
48
55
|
if (!finding || typeof finding.category !== 'string' || !QUALIFYING_CATEGORIES.has(finding.category))
|
|
49
56
|
continue;
|
|
57
|
+
const firstObservedAt = timestamp(finding.firstObservedAt);
|
|
58
|
+
const lastObservedAt = timestamp(finding.lastObservedAt);
|
|
59
|
+
if (firstObservedAt !== null && lastObservedAt !== null && firstObservedAt > lastObservedAt)
|
|
60
|
+
continue;
|
|
50
61
|
qualifyingFindings += 1;
|
|
51
|
-
const candidate =
|
|
62
|
+
const candidate = lastObservedAt ?? firstObservedAt;
|
|
52
63
|
if (candidate !== null && (latestFindingAt === null || candidate > latestFindingAt))
|
|
53
64
|
latestFindingAt = candidate;
|
|
54
65
|
}
|
|
@@ -67,12 +78,17 @@ function providerEvidence(value) {
|
|
|
67
78
|
recent: ageDays !== null && ageDays <= EXTERNAL_INTELLIGENCE_RECENT_DAYS,
|
|
68
79
|
};
|
|
69
80
|
}
|
|
70
|
-
export function calibrateExternalIntelligenceRisk(value) {
|
|
81
|
+
export function calibrateExternalIntelligenceRisk(value, expectedDomainValue) {
|
|
71
82
|
const envelope = record(value);
|
|
72
|
-
const
|
|
83
|
+
const expectedDomain = canonicalRegistrableDomain(expectedDomainValue);
|
|
84
|
+
const providers = expectedDomain
|
|
85
|
+
&& envelope?.version === THREAT_INTELLIGENCE_ENVELOPE_VERSION
|
|
86
|
+
&& Array.isArray(envelope.providers)
|
|
87
|
+
? envelope.providers.slice(0, MAX_PROVIDERS)
|
|
88
|
+
: [];
|
|
73
89
|
const byProvider = new Map();
|
|
74
90
|
for (const item of providers) {
|
|
75
|
-
const evidence = providerEvidence(item);
|
|
91
|
+
const evidence = providerEvidence(item, expectedDomain ?? '');
|
|
76
92
|
if (!evidence || byProvider.has(evidence.providerId))
|
|
77
93
|
continue;
|
|
78
94
|
byProvider.set(evidence.providerId, evidence);
|
package/lib/feature-policy.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const NETWORK_FEATURE_DEFINITIONS = Object.freeze({
|
|
|
14
14
|
availability: Object.freeze({ env: 'WHOISLEUTH_DISABLE_AVAILABILITY', label: 'availability analysis' }),
|
|
15
15
|
dns_intelligence: Object.freeze({ env: 'WHOISLEUTH_DISABLE_DNS_INTELLIGENCE', label: 'DNS intelligence' }),
|
|
16
16
|
website_probe: Object.freeze({ env: 'WHOISLEUTH_DISABLE_WEBSITE_PROBE', label: 'website probing' }),
|
|
17
|
-
tls_intelligence: Object.freeze({ env: 'WHOISLEUTH_DISABLE_TLS_INTELLIGENCE', label: 'TLS
|
|
17
|
+
tls_intelligence: Object.freeze({ env: 'WHOISLEUTH_DISABLE_TLS_INTELLIGENCE', label: 'TLS evidence collection' }),
|
|
18
18
|
certificate_transparency: Object.freeze({ env: 'WHOISLEUTH_DISABLE_CERTIFICATE_TRANSPARENCY', label: 'Certificate Transparency search' }),
|
|
19
19
|
domain_posture: Object.freeze({ env: 'WHOISLEUTH_DISABLE_DOMAIN_POSTURE', label: 'domain-posture auditing' }),
|
|
20
20
|
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Exact bounded contracts for metadata derived from the already-captured Deep
|
|
2
|
+
// homepage response. These validators perform no collection and accept only
|
|
3
|
+
// fixed enums, booleans, counts, and reviewed limitation text.
|
|
4
|
+
const PAGE_PUBLICATION_METADATA_VERSION = 1;
|
|
5
|
+
const HTTP_DELIVERY_METADATA_VERSION = 1;
|
|
6
|
+
const MAX_PAGE_PUBLICATION_TAG_COUNT = 8_192;
|
|
7
|
+
const MAX_PAGE_PUBLICATION_META_ELEMENTS = 128;
|
|
8
|
+
const MAX_PAGE_PUBLICATION_DECLARATIONS = 64;
|
|
9
|
+
const MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES = 64;
|
|
10
|
+
const MAX_HTTP_DELIVERY_TOKENS = 32;
|
|
11
|
+
const MAX_HTTP_DELIVERY_HEADER_BYTES = 1_024;
|
|
12
|
+
const MAX_HTTP_CACHE_SECONDS = 315_360_000;
|
|
13
|
+
const MAX_HOMEPAGE_METADATA_LIMITATIONS = 4;
|
|
14
|
+
const MAX_HOMEPAGE_METADATA_LIMITATION_LENGTH = 300;
|
|
15
|
+
const PAGE_PUBLICATION_LIMITATIONS = Object.freeze({
|
|
16
|
+
scope: 'Counts and declarations describe only the captured static homepage HTML; they are not a full accessibility, indexing, or performance audit.',
|
|
17
|
+
body: 'Homepage body capture reached its byte limit; later declarations and elements may be absent.',
|
|
18
|
+
bounds: 'Static HTML token or attribute bounds were reached; publication metadata is partial.',
|
|
19
|
+
malformed: 'At least one publication declaration could not be interpreted; raw declaration values were not retained.',
|
|
20
|
+
});
|
|
21
|
+
const HTTP_DELIVERY_LIMITATIONS = Object.freeze({
|
|
22
|
+
scope: 'Selected-response headers are point-in-time declarations and do not prove intermediary caching, compression effectiveness, or page performance.',
|
|
23
|
+
bounds: 'At least one delivery header exceeded its byte or item bound and was not partially parsed.',
|
|
24
|
+
malformed: 'At least one delivery header could not be interpreted; raw header values were not retained.',
|
|
25
|
+
});
|
|
26
|
+
const COMPONENT_STATES = new Set(['observed', 'not_observed', 'partial', 'malformed']);
|
|
27
|
+
const ROBOTS_DIRECTIVES = new Set([
|
|
28
|
+
'all', 'follow', 'index', 'max-image-preview', 'max-snippet', 'max-video-preview',
|
|
29
|
+
'noarchive', 'nocache', 'nofollow', 'noimageindex', 'noindex', 'none', 'nositelinkssearchbox',
|
|
30
|
+
'nosnippet', 'notranslate', 'unavailable_after',
|
|
31
|
+
]);
|
|
32
|
+
const TWITTER_CARD_TYPES = new Set(['summary', 'summary_large_image', 'player', 'app', 'other']);
|
|
33
|
+
const CONTENT_CODINGS = new Set(['gzip', 'br', 'deflate', 'zstd', 'identity', 'other']);
|
|
34
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/u;
|
|
35
|
+
function record(value) {
|
|
36
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
37
|
+
? value
|
|
38
|
+
: null;
|
|
39
|
+
}
|
|
40
|
+
function exactKeys(value, keys) {
|
|
41
|
+
const actual = Object.keys(value).sort();
|
|
42
|
+
const expected = [...keys].sort();
|
|
43
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
44
|
+
}
|
|
45
|
+
function boundedCount(value, maximum) {
|
|
46
|
+
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum;
|
|
47
|
+
}
|
|
48
|
+
function fixedSortedStrings(value, allowed, maximum) {
|
|
49
|
+
if (!Array.isArray(value) || value.length > maximum || !value.every((item) => typeof item === 'string' && allowed.has(item)))
|
|
50
|
+
return false;
|
|
51
|
+
return value.every((item, index) => index === 0 || String(value[index - 1]) < item);
|
|
52
|
+
}
|
|
53
|
+
function fixedLimitations(value, allowed) {
|
|
54
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > MAX_HOMEPAGE_METADATA_LIMITATIONS)
|
|
55
|
+
return false;
|
|
56
|
+
return value.every((item, index) => typeof item === 'string'
|
|
57
|
+
&& item.length <= MAX_HOMEPAGE_METADATA_LIMITATION_LENGTH
|
|
58
|
+
&& !CONTROL_RE.test(item)
|
|
59
|
+
&& allowed.includes(item)
|
|
60
|
+
&& value.indexOf(item) === index);
|
|
61
|
+
}
|
|
62
|
+
function validComponentState(value) {
|
|
63
|
+
return typeof value === 'string' && COMPONENT_STATES.has(value);
|
|
64
|
+
}
|
|
65
|
+
function validPagePublicationMetadata(value) {
|
|
66
|
+
const root = record(value);
|
|
67
|
+
if (!root || !exactKeys(root, [
|
|
68
|
+
'version', 'status', 'complete', 'truncated', 'limitations', 'robots', 'twitterCard',
|
|
69
|
+
'headings', 'images', 'renderBlockingCandidates',
|
|
70
|
+
])
|
|
71
|
+
|| root.version !== PAGE_PUBLICATION_METADATA_VERSION
|
|
72
|
+
|| !['success', 'partial'].includes(String(root.status))
|
|
73
|
+
|| typeof root.complete !== 'boolean'
|
|
74
|
+
|| typeof root.truncated !== 'boolean'
|
|
75
|
+
|| !fixedLimitations(root.limitations, Object.values(PAGE_PUBLICATION_LIMITATIONS)))
|
|
76
|
+
return false;
|
|
77
|
+
const robots = record(root.robots);
|
|
78
|
+
const twitter = record(root.twitterCard);
|
|
79
|
+
const headings = record(root.headings);
|
|
80
|
+
const images = record(root.images);
|
|
81
|
+
const blocking = record(root.renderBlockingCandidates);
|
|
82
|
+
if (!robots || !twitter || !headings || !images || !blocking)
|
|
83
|
+
return false;
|
|
84
|
+
if (!exactKeys(robots, [
|
|
85
|
+
'status', 'complete', 'truncated', 'directives', 'recognizedDirectiveCount', 'unknownDirectiveCount', 'conflicting',
|
|
86
|
+
])
|
|
87
|
+
|| !validComponentState(robots.status)
|
|
88
|
+
|| typeof robots.complete !== 'boolean'
|
|
89
|
+
|| typeof robots.truncated !== 'boolean'
|
|
90
|
+
|| !fixedSortedStrings(robots.directives, ROBOTS_DIRECTIVES, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES)
|
|
91
|
+
|| !boundedCount(robots.recognizedDirectiveCount, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES)
|
|
92
|
+
|| !boundedCount(robots.unknownDirectiveCount, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES)
|
|
93
|
+
|| Number(robots.recognizedDirectiveCount) + Number(robots.unknownDirectiveCount) > MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES
|
|
94
|
+
|| Number(robots.recognizedDirectiveCount) < robots.directives.length
|
|
95
|
+
|| typeof robots.conflicting !== 'boolean')
|
|
96
|
+
return false;
|
|
97
|
+
if (robots.status === 'partial'
|
|
98
|
+
? robots.complete !== false || robots.truncated !== true
|
|
99
|
+
: robots.status === 'malformed'
|
|
100
|
+
? robots.complete !== false || robots.truncated !== false
|
|
101
|
+
: robots.complete !== true || robots.truncated !== false)
|
|
102
|
+
return false;
|
|
103
|
+
const robotValues = new Set(robots.directives);
|
|
104
|
+
const conflict = (robotValues.has('index') || robotValues.has('all'))
|
|
105
|
+
&& (robotValues.has('noindex') || robotValues.has('none'))
|
|
106
|
+
|| (robotValues.has('follow') || robotValues.has('all'))
|
|
107
|
+
&& (robotValues.has('nofollow') || robotValues.has('none'));
|
|
108
|
+
if (robots.conflicting !== conflict)
|
|
109
|
+
return false;
|
|
110
|
+
if (robots.status === 'observed' && (Number(robots.recognizedDirectiveCount) + Number(robots.unknownDirectiveCount) === 0
|
|
111
|
+
|| (Number(robots.recognizedDirectiveCount) === 0) !== (robots.directives.length === 0)))
|
|
112
|
+
return false;
|
|
113
|
+
if (robots.status === 'not_observed' && (robots.directives.length || robots.recognizedDirectiveCount !== 0
|
|
114
|
+
|| robots.unknownDirectiveCount !== 0 || robots.conflicting !== false))
|
|
115
|
+
return false;
|
|
116
|
+
if (!exactKeys(twitter, [
|
|
117
|
+
'status', 'complete', 'truncated', 'cardType', 'declarationCount', 'titlePresent', 'descriptionPresent', 'imagePresent',
|
|
118
|
+
'imageAltPresent', 'sitePresent', 'creatorPresent', 'playerPresent', 'appPresent',
|
|
119
|
+
])
|
|
120
|
+
|| !validComponentState(twitter.status)
|
|
121
|
+
|| typeof twitter.complete !== 'boolean'
|
|
122
|
+
|| typeof twitter.truncated !== 'boolean'
|
|
123
|
+
|| !(twitter.cardType === null || typeof twitter.cardType === 'string' && TWITTER_CARD_TYPES.has(twitter.cardType))
|
|
124
|
+
|| !boundedCount(twitter.declarationCount, MAX_PAGE_PUBLICATION_DECLARATIONS)
|
|
125
|
+
|| !['titlePresent', 'descriptionPresent', 'imagePresent', 'imageAltPresent', 'sitePresent', 'creatorPresent', 'playerPresent', 'appPresent']
|
|
126
|
+
.every((key) => typeof twitter[key] === 'boolean'))
|
|
127
|
+
return false;
|
|
128
|
+
if (twitter.status === 'partial'
|
|
129
|
+
? twitter.complete !== false || twitter.truncated !== true
|
|
130
|
+
: twitter.status === 'malformed'
|
|
131
|
+
? twitter.complete !== false || twitter.truncated !== false
|
|
132
|
+
: twitter.complete !== true || twitter.truncated !== false)
|
|
133
|
+
return false;
|
|
134
|
+
if (twitter.status === 'not_observed' && (twitter.cardType !== null || twitter.declarationCount !== 0
|
|
135
|
+
|| ['titlePresent', 'descriptionPresent', 'imagePresent', 'imageAltPresent', 'sitePresent', 'creatorPresent', 'playerPresent', 'appPresent']
|
|
136
|
+
.some((key) => twitter[key] === true)))
|
|
137
|
+
return false;
|
|
138
|
+
const twitterValuePresent = twitter.cardType !== null
|
|
139
|
+
|| ['titlePresent', 'descriptionPresent', 'imagePresent', 'imageAltPresent', 'sitePresent', 'creatorPresent', 'playerPresent', 'appPresent']
|
|
140
|
+
.some((key) => twitter[key] === true);
|
|
141
|
+
if (twitterValuePresent && twitter.declarationCount === 0)
|
|
142
|
+
return false;
|
|
143
|
+
if (twitter.status === 'observed' && (twitter.declarationCount === 0 || !twitterValuePresent))
|
|
144
|
+
return false;
|
|
145
|
+
if (!exactKeys(headings, ['complete', 'truncated', 'total', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
|
146
|
+
|| typeof headings.complete !== 'boolean'
|
|
147
|
+
|| typeof headings.truncated !== 'boolean'
|
|
148
|
+
|| headings.complete === headings.truncated
|
|
149
|
+
|| !['total', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].every((key) => boundedCount(headings[key], MAX_PAGE_PUBLICATION_TAG_COUNT))
|
|
150
|
+
|| headings.total !== ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].reduce((sum, key) => sum + Number(headings[key]), 0))
|
|
151
|
+
return false;
|
|
152
|
+
if (!exactKeys(images, [
|
|
153
|
+
'totalComplete', 'classificationComplete', 'truncated',
|
|
154
|
+
'total', 'altMissing', 'altEmpty', 'altNonEmpty', 'altUnclassified',
|
|
155
|
+
])
|
|
156
|
+
|| typeof images.totalComplete !== 'boolean'
|
|
157
|
+
|| typeof images.classificationComplete !== 'boolean'
|
|
158
|
+
|| typeof images.truncated !== 'boolean'
|
|
159
|
+
|| images.truncated !== (images.totalComplete !== true || images.classificationComplete !== true)
|
|
160
|
+
|| images.classificationComplete === true && (images.totalComplete !== true || images.altUnclassified !== 0)
|
|
161
|
+
|| !['total', 'altMissing', 'altEmpty', 'altNonEmpty', 'altUnclassified'].every((key) => boundedCount(images[key], MAX_PAGE_PUBLICATION_TAG_COUNT))
|
|
162
|
+
|| images.total !== ['altMissing', 'altEmpty', 'altNonEmpty', 'altUnclassified'].reduce((sum, key) => sum + Number(images[key]), 0))
|
|
163
|
+
return false;
|
|
164
|
+
if (!exactKeys(blocking, ['complete', 'truncated', 'script', 'stylesheet', 'total', 'scope'])
|
|
165
|
+
|| typeof blocking.complete !== 'boolean'
|
|
166
|
+
|| typeof blocking.truncated !== 'boolean'
|
|
167
|
+
|| blocking.complete === blocking.truncated
|
|
168
|
+
|| !['script', 'stylesheet', 'total'].every((key) => boundedCount(blocking[key], MAX_PAGE_PUBLICATION_TAG_COUNT))
|
|
169
|
+
|| blocking.total !== Number(blocking.script) + Number(blocking.stylesheet)
|
|
170
|
+
|| blocking.scope !== 'explicit-head-static-v1')
|
|
171
|
+
return false;
|
|
172
|
+
const componentTruncated = robots.truncated === true || twitter.truncated === true
|
|
173
|
+
|| headings.truncated === true || images.truncated === true || blocking.truncated === true;
|
|
174
|
+
const componentIncomplete = robots.complete !== true || twitter.complete !== true
|
|
175
|
+
|| headings.complete !== true || images.totalComplete !== true
|
|
176
|
+
|| images.classificationComplete !== true || blocking.complete !== true;
|
|
177
|
+
if (root.truncated !== componentTruncated)
|
|
178
|
+
return false;
|
|
179
|
+
if (root.status === 'success' ? root.complete !== true || componentIncomplete : root.complete !== false || !componentIncomplete)
|
|
180
|
+
return false;
|
|
181
|
+
const limitations = root.limitations;
|
|
182
|
+
const hasTruncationLimitation = limitations.some((item) => (item === PAGE_PUBLICATION_LIMITATIONS.body || item === PAGE_PUBLICATION_LIMITATIONS.bounds));
|
|
183
|
+
const hasMalformedLimitation = limitations.includes(PAGE_PUBLICATION_LIMITATIONS.malformed);
|
|
184
|
+
const malformedComponent = [robots.status, twitter.status].includes('malformed');
|
|
185
|
+
if (root.truncated !== hasTruncationLimitation
|
|
186
|
+
|| malformedComponent && !hasMalformedLimitation
|
|
187
|
+
|| root.status === 'success' && hasMalformedLimitation)
|
|
188
|
+
return false;
|
|
189
|
+
return limitations.includes(PAGE_PUBLICATION_LIMITATIONS.scope);
|
|
190
|
+
}
|
|
191
|
+
function validPresenceValidity(value) {
|
|
192
|
+
const item = record(value);
|
|
193
|
+
return Boolean(item && exactKeys(item, ['present', 'valid'])
|
|
194
|
+
&& typeof item.present === 'boolean'
|
|
195
|
+
&& (item.valid === null || typeof item.valid === 'boolean')
|
|
196
|
+
&& (item.present === true || item.valid === null));
|
|
197
|
+
}
|
|
198
|
+
function validHttpDeliveryMetadata(value) {
|
|
199
|
+
const root = record(value);
|
|
200
|
+
if (!root || !exactKeys(root, ['version', 'status', 'complete', 'truncated', 'limitations', 'contentEncoding', 'cachePolicy'])
|
|
201
|
+
|| root.version !== HTTP_DELIVERY_METADATA_VERSION
|
|
202
|
+
|| !['success', 'partial'].includes(String(root.status))
|
|
203
|
+
|| typeof root.complete !== 'boolean'
|
|
204
|
+
|| typeof root.truncated !== 'boolean'
|
|
205
|
+
|| !fixedLimitations(root.limitations, Object.values(HTTP_DELIVERY_LIMITATIONS)))
|
|
206
|
+
return false;
|
|
207
|
+
const encoding = record(root.contentEncoding);
|
|
208
|
+
const cache = record(root.cachePolicy);
|
|
209
|
+
if (!encoding || !cache)
|
|
210
|
+
return false;
|
|
211
|
+
if (!exactKeys(encoding, ['status', 'codings', 'encoded', 'unknownCodingCount'])
|
|
212
|
+
|| !validComponentState(encoding.status)
|
|
213
|
+
|| !fixedSortedStrings(encoding.codings, CONTENT_CODINGS, MAX_HTTP_DELIVERY_TOKENS)
|
|
214
|
+
|| !(encoding.encoded === null || typeof encoding.encoded === 'boolean')
|
|
215
|
+
|| !boundedCount(encoding.unknownCodingCount, MAX_HTTP_DELIVERY_TOKENS))
|
|
216
|
+
return false;
|
|
217
|
+
const codings = encoding.codings;
|
|
218
|
+
if (encoding.status === 'observed') {
|
|
219
|
+
if (!codings.length || typeof encoding.encoded !== 'boolean')
|
|
220
|
+
return false;
|
|
221
|
+
if (encoding.encoded !== codings.some((coding) => coding !== 'identity'))
|
|
222
|
+
return false;
|
|
223
|
+
if ((encoding.unknownCodingCount === 0) !== !codings.includes('other'))
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
else if (codings.length || encoding.encoded !== null || encoding.unknownCodingCount !== 0)
|
|
227
|
+
return false;
|
|
228
|
+
if (!exactKeys(cache, [
|
|
229
|
+
'status', 'noStore', 'noCache', 'mustRevalidate', 'public', 'private', 'immutable',
|
|
230
|
+
'maxAgeSeconds', 'sMaxAgeSeconds', 'ageSeconds', 'maxAgePresent', 'sMaxAgePresent',
|
|
231
|
+
'agePresent', 'unknownDirectiveCount', 'etag',
|
|
232
|
+
'lastModified', 'expires',
|
|
233
|
+
])
|
|
234
|
+
|| !validComponentState(cache.status)
|
|
235
|
+
|| !['noStore', 'noCache', 'mustRevalidate', 'public', 'private', 'immutable'].every((key) => typeof cache[key] === 'boolean')
|
|
236
|
+
|| !['maxAgeSeconds', 'sMaxAgeSeconds', 'ageSeconds'].every((key) => cache[key] === null || boundedCount(cache[key], MAX_HTTP_CACHE_SECONDS))
|
|
237
|
+
|| !['maxAgePresent', 'sMaxAgePresent', 'agePresent'].every((key) => typeof cache[key] === 'boolean')
|
|
238
|
+
|| cache.maxAgePresent === false && cache.maxAgeSeconds !== null
|
|
239
|
+
|| cache.sMaxAgePresent === false && cache.sMaxAgeSeconds !== null
|
|
240
|
+
|| cache.agePresent === false && cache.ageSeconds !== null
|
|
241
|
+
|| !boundedCount(cache.unknownDirectiveCount, MAX_HTTP_DELIVERY_TOKENS)
|
|
242
|
+
|| !validPresenceValidity(cache.etag)
|
|
243
|
+
|| !validPresenceValidity(cache.lastModified)
|
|
244
|
+
|| !validPresenceValidity(cache.expires))
|
|
245
|
+
return false;
|
|
246
|
+
if (cache.status === 'not_observed' && (['noStore', 'noCache', 'mustRevalidate', 'public', 'private', 'immutable'].some((key) => cache[key] === true)
|
|
247
|
+
|| ['maxAgeSeconds', 'sMaxAgeSeconds', 'ageSeconds'].some((key) => cache[key] !== null)
|
|
248
|
+
|| ['maxAgePresent', 'sMaxAgePresent', 'agePresent'].some((key) => cache[key] === true)
|
|
249
|
+
|| cache.unknownDirectiveCount !== 0
|
|
250
|
+
|| [cache.etag, cache.lastModified, cache.expires].some((item) => item.present === true)))
|
|
251
|
+
return false;
|
|
252
|
+
const numericCacheEvidence = [
|
|
253
|
+
[cache.maxAgePresent, cache.maxAgeSeconds],
|
|
254
|
+
[cache.sMaxAgePresent, cache.sMaxAgeSeconds],
|
|
255
|
+
[cache.agePresent, cache.ageSeconds],
|
|
256
|
+
];
|
|
257
|
+
const validatorEvidence = [cache.etag, cache.lastModified, cache.expires]
|
|
258
|
+
.map((item) => item);
|
|
259
|
+
const cacheEvidencePresent = ['noStore', 'noCache', 'mustRevalidate', 'public', 'private', 'immutable']
|
|
260
|
+
.some((key) => cache[key] === true)
|
|
261
|
+
|| numericCacheEvidence.some(([present]) => present === true)
|
|
262
|
+
|| cache.unknownDirectiveCount !== 0
|
|
263
|
+
|| validatorEvidence.some((item) => item.present === true);
|
|
264
|
+
if (cache.status === 'observed' && (!cacheEvidencePresent
|
|
265
|
+
|| numericCacheEvidence.some(([present, result]) => present === true && result === null)
|
|
266
|
+
|| validatorEvidence.some((item) => item.present === true && item.valid !== true)))
|
|
267
|
+
return false;
|
|
268
|
+
const uncertainValidator = [cache.etag, cache.lastModified, cache.expires]
|
|
269
|
+
.some((item) => item.present === true
|
|
270
|
+
&& item.valid === null);
|
|
271
|
+
if (uncertainValidator && !['partial', 'malformed'].includes(String(cache.status)))
|
|
272
|
+
return false;
|
|
273
|
+
const componentIncomplete = [encoding.status, cache.status].some((state) => state === 'partial' || state === 'malformed');
|
|
274
|
+
if (root.status === 'success' ? root.complete !== true || componentIncomplete : root.complete !== false || !componentIncomplete)
|
|
275
|
+
return false;
|
|
276
|
+
if (root.truncated !== [encoding.status, cache.status].includes('partial'))
|
|
277
|
+
return false;
|
|
278
|
+
const limitations = root.limitations;
|
|
279
|
+
const hasBoundsLimitation = limitations.includes(HTTP_DELIVERY_LIMITATIONS.bounds);
|
|
280
|
+
const hasMalformedLimitation = limitations.includes(HTTP_DELIVERY_LIMITATIONS.malformed);
|
|
281
|
+
const definitelyMalformed = [encoding.status, cache.status].includes('malformed')
|
|
282
|
+
|| numericCacheEvidence.some(([present, result]) => present === true && result === null)
|
|
283
|
+
|| validatorEvidence.some((item) => item.present === true && item.valid === false);
|
|
284
|
+
if (root.truncated !== hasBoundsLimitation
|
|
285
|
+
|| definitelyMalformed && !hasMalformedLimitation
|
|
286
|
+
|| root.status === 'success' && hasMalformedLimitation)
|
|
287
|
+
return false;
|
|
288
|
+
return limitations.includes(HTTP_DELIVERY_LIMITATIONS.scope);
|
|
289
|
+
}
|
|
290
|
+
export { HTTP_DELIVERY_LIMITATIONS, MAX_HTTP_DELIVERY_HEADER_BYTES, HTTP_DELIVERY_METADATA_VERSION, MAX_HTTP_CACHE_SECONDS, MAX_HTTP_DELIVERY_TOKENS, MAX_PAGE_PUBLICATION_DECLARATIONS, MAX_PAGE_PUBLICATION_META_ELEMENTS, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES, MAX_PAGE_PUBLICATION_TAG_COUNT, PAGE_PUBLICATION_LIMITATIONS, PAGE_PUBLICATION_METADATA_VERSION, validHttpDeliveryMetadata, validPagePublicationMetadata, };
|
package/lib/hostname.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Linear-time validation for already-normalized ASCII DNS hostnames. Keep this
|
|
2
2
|
// helper deliberately independent of public-suffix policy: callers decide
|
|
3
3
|
// whether a syntactically valid hostname must also be registrable.
|
|
4
|
-
const ASCII_LABEL_RE = /^[
|
|
4
|
+
const ASCII_LABEL_RE = /^[A-Za-z0-9-]+$/u;
|
|
5
5
|
function isValidAsciiHostname(value, options = {}) {
|
|
6
6
|
if (typeof value !== 'string'
|
|
7
7
|
|| value.length < 1
|
package/lib/html-signals.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { isIP } from 'node:net';
|
|
|
7
7
|
import { domainToASCII } from 'node:url';
|
|
8
8
|
import { analyzeCredentialSurfaceProfile } from "./credential-surface-profile.mjs";
|
|
9
9
|
import { analyzeClientBehavior } from "./client-behavior-profile.mjs";
|
|
10
|
-
import { createObservation } from "
|
|
10
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
11
11
|
import { createPageFingerprints } from "./page-fingerprints.mjs";
|
|
12
12
|
import { detectPageLanguageSignal } from "./page-language-signals.mjs";
|
|
13
13
|
import { analyzePageRole } from "./page-role-profile.mjs";
|
|
@@ -15,6 +15,7 @@ import { analyzeCspMetaPolicies } from "./response-policy.mjs";
|
|
|
15
15
|
import { analyzeStaticHtml } from "./static-html-analysis.mjs";
|
|
16
16
|
import { analyzeStructuredDataIdentity } from "./structured-data-identity.mjs";
|
|
17
17
|
import { analyzeWebsiteTechnology } from "./website-technology.mjs";
|
|
18
|
+
import { PAGE_PUBLICATION_LIMITATIONS, PAGE_PUBLICATION_METADATA_VERSION, } from "./homepage-metadata-contract.mjs";
|
|
18
19
|
const MAX_TITLE_LENGTH = 200;
|
|
19
20
|
const MAX_EXTERNAL_ASSET_HOSTS = 20;
|
|
20
21
|
const MAX_IDENTITY_TAGS = 512;
|
|
@@ -34,8 +35,8 @@ const MAX_DOWNLOAD_FILE_TYPES = 20;
|
|
|
34
35
|
const MAX_TRACKING_IDENTIFIERS = 30;
|
|
35
36
|
const MAX_URLS_PER_TAG = 20;
|
|
36
37
|
const PAGE_IDENTITY_VERSION = 3;
|
|
37
|
-
const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/
|
|
38
|
-
const HAS_CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]
|
|
38
|
+
const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
39
|
+
const HAS_CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
39
40
|
const TITLE_RE = /<title[^>]*>([\s\S]*?)<\/title>/i;
|
|
40
41
|
// A password input is the single strongest static-HTML tell that a page is
|
|
41
42
|
// asking for credentials, regardless of what it claims to be.
|
|
@@ -618,6 +619,83 @@ function extractPageIdentity(html, domain, options = {}) {
|
|
|
618
619
|
fingerprints,
|
|
619
620
|
};
|
|
620
621
|
}
|
|
622
|
+
function publicationSourceStatus(observed, malformed, truncated) {
|
|
623
|
+
if (truncated)
|
|
624
|
+
return 'partial';
|
|
625
|
+
if (malformed)
|
|
626
|
+
return 'malformed';
|
|
627
|
+
return observed ? 'observed' : 'not_observed';
|
|
628
|
+
}
|
|
629
|
+
function buildPagePublicationMetadata(analysis, sourceTruncated) {
|
|
630
|
+
const truncated = sourceTruncated || analysis.truncated;
|
|
631
|
+
const malformed = analysis.robots.malformed || analysis.twitterCard.malformed;
|
|
632
|
+
const incomplete = truncated || malformed;
|
|
633
|
+
const limitations = [
|
|
634
|
+
PAGE_PUBLICATION_LIMITATIONS.scope,
|
|
635
|
+
];
|
|
636
|
+
if (sourceTruncated)
|
|
637
|
+
limitations.push(PAGE_PUBLICATION_LIMITATIONS.body);
|
|
638
|
+
if (analysis.truncated)
|
|
639
|
+
limitations.push(PAGE_PUBLICATION_LIMITATIONS.bounds);
|
|
640
|
+
if (malformed)
|
|
641
|
+
limitations.push(PAGE_PUBLICATION_LIMITATIONS.malformed);
|
|
642
|
+
const cardTypes = analysis.twitterCard.cardTypes;
|
|
643
|
+
const suppliedCardType = cardTypes.length === 1 ? cardTypes[0] : null;
|
|
644
|
+
const cardType = suppliedCardType && ['summary', 'summary_large_image', 'player', 'app', 'other'].includes(suppliedCardType)
|
|
645
|
+
? suppliedCardType
|
|
646
|
+
: cardTypes.length > 1 ? 'other' : null;
|
|
647
|
+
const documentTruncated = sourceTruncated || analysis.documentTruncated;
|
|
648
|
+
const robotsStatus = publicationSourceStatus(analysis.robots.observed, analysis.robots.malformed, documentTruncated || analysis.robots.truncated);
|
|
649
|
+
const twitterStatus = publicationSourceStatus(analysis.twitterCard.observed, analysis.twitterCard.malformed, documentTruncated || analysis.twitterCard.truncated);
|
|
650
|
+
return {
|
|
651
|
+
version: PAGE_PUBLICATION_METADATA_VERSION,
|
|
652
|
+
status: incomplete ? 'partial' : 'success',
|
|
653
|
+
complete: !incomplete,
|
|
654
|
+
truncated,
|
|
655
|
+
limitations,
|
|
656
|
+
robots: {
|
|
657
|
+
status: robotsStatus,
|
|
658
|
+
complete: robotsStatus === 'observed' || robotsStatus === 'not_observed',
|
|
659
|
+
truncated: robotsStatus === 'partial',
|
|
660
|
+
directives: analysis.robots.directives,
|
|
661
|
+
recognizedDirectiveCount: analysis.robots.recognizedDirectiveCount,
|
|
662
|
+
unknownDirectiveCount: analysis.robots.unknownDirectiveCount,
|
|
663
|
+
conflicting: analysis.robots.conflicting,
|
|
664
|
+
},
|
|
665
|
+
twitterCard: {
|
|
666
|
+
status: twitterStatus,
|
|
667
|
+
complete: twitterStatus === 'observed' || twitterStatus === 'not_observed',
|
|
668
|
+
truncated: twitterStatus === 'partial',
|
|
669
|
+
cardType,
|
|
670
|
+
declarationCount: analysis.twitterCard.declarationCount,
|
|
671
|
+
titlePresent: analysis.twitterCard.titlePresent,
|
|
672
|
+
descriptionPresent: analysis.twitterCard.descriptionPresent,
|
|
673
|
+
imagePresent: analysis.twitterCard.imagePresent,
|
|
674
|
+
imageAltPresent: analysis.twitterCard.imageAltPresent,
|
|
675
|
+
sitePresent: analysis.twitterCard.sitePresent,
|
|
676
|
+
creatorPresent: analysis.twitterCard.creatorPresent,
|
|
677
|
+
playerPresent: analysis.twitterCard.playerPresent,
|
|
678
|
+
appPresent: analysis.twitterCard.appPresent,
|
|
679
|
+
},
|
|
680
|
+
headings: {
|
|
681
|
+
...analysis.headings,
|
|
682
|
+
complete: !documentTruncated && !analysis.headings.truncated,
|
|
683
|
+
truncated: documentTruncated || analysis.headings.truncated,
|
|
684
|
+
},
|
|
685
|
+
images: {
|
|
686
|
+
...analysis.images,
|
|
687
|
+
totalComplete: !documentTruncated && analysis.images.totalComplete,
|
|
688
|
+
classificationComplete: !documentTruncated && analysis.images.classificationComplete,
|
|
689
|
+
truncated: documentTruncated || analysis.images.truncated,
|
|
690
|
+
},
|
|
691
|
+
renderBlockingCandidates: {
|
|
692
|
+
...analysis.renderBlockingCandidates,
|
|
693
|
+
complete: !documentTruncated && !analysis.renderBlockingCandidates.truncated,
|
|
694
|
+
truncated: documentTruncated || analysis.renderBlockingCandidates.truncated,
|
|
695
|
+
scope: 'explicit-head-static-v1',
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
}
|
|
621
699
|
function extractHtmlSignals(html, domain, options = {}) {
|
|
622
700
|
const pageIdentity = options.includePageIdentity === false ? null : extractPageIdentity(html, domain, options);
|
|
623
701
|
const includeCredentialSurfaceProfile = pageIdentity && options.includeCredentialSurfaceProfile === true;
|
|
@@ -628,6 +706,12 @@ function extractHtmlSignals(html, domain, options = {}) {
|
|
|
628
706
|
const htmlAnalysis = includeCredentialSurfaceProfile || includeStructuredDataIdentity || includeTechnologyProfile || includeDerivedPageProfiles
|
|
629
707
|
? analyzeStaticHtml(html, { baseUrl, includeVisibleText: true })
|
|
630
708
|
: null;
|
|
709
|
+
const pageIdentityOutput = pageIdentity && htmlAnalysis && options.includePublicationMetadata !== false
|
|
710
|
+
? {
|
|
711
|
+
...pageIdentity,
|
|
712
|
+
publicationMetadata: buildPagePublicationMetadata(htmlAnalysis.publicationMetadata, options.sourceTruncated === true),
|
|
713
|
+
}
|
|
714
|
+
: pageIdentity;
|
|
631
715
|
const pageLanguageSignal = detectPageLanguageSignal(html, pageIdentity?.documentLanguage, htmlAnalysis ?? undefined);
|
|
632
716
|
return {
|
|
633
717
|
pageTitle: extractPageTitle(html),
|
|
@@ -640,7 +724,7 @@ function extractHtmlSignals(html, domain, options = {}) {
|
|
|
640
724
|
cspMetaPolicy: htmlAnalysis
|
|
641
725
|
? analyzeCspMetaPolicies(htmlAnalysis.cspMetaPolicies, htmlAnalysis.cspMetaLimitReached)
|
|
642
726
|
: null,
|
|
643
|
-
pageIdentity,
|
|
727
|
+
pageIdentity: pageIdentityOutput,
|
|
644
728
|
credentialSurfaceProfile: includeCredentialSurfaceProfile && htmlAnalysis ? analyzeCredentialSurfaceProfile({
|
|
645
729
|
htmlAnalysis,
|
|
646
730
|
observedAt: options.observedAt,
|
|
@@ -675,4 +759,4 @@ function extractHtmlSignals(html, domain, options = {}) {
|
|
|
675
759
|
}) : null,
|
|
676
760
|
};
|
|
677
761
|
}
|
|
678
|
-
export { PAGE_IDENTITY_VERSION, MAX_IDENTITY_TAGS, MAX_FORMS, MAX_FORM_ACTION_ORIGINS, MAX_RESOURCE_TAGS, MAX_RESOURCE_ORIGINS, MAX_EMBEDDED_ORIGINS, MAX_CONTACT_DOMAINS, MAX_TRACKING_IDENTIFIERS, extractHtmlSignals, extractPageIdentity, };
|
|
762
|
+
export { PAGE_IDENTITY_VERSION, PAGE_PUBLICATION_METADATA_VERSION, MAX_IDENTITY_TAGS, MAX_FORMS, MAX_FORM_ACTION_ORIGINS, MAX_RESOURCE_TAGS, MAX_RESOURCE_ORIGINS, MAX_EMBEDDED_ORIGINS, MAX_CONTACT_DOMAINS, MAX_TRACKING_IDENTIFIERS, extractHtmlSignals, extractPageIdentity, };
|