@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
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
// One bounded
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Tokenizer, TokenizerMode } from 'parse5';
|
|
1
|
+
// One bounded native HTML parse over the already-captured homepage body.
|
|
2
|
+
// Consumers share element/attribute evidence instead of reparsing reconstructed
|
|
3
|
+
// markup. Only bounded transient projections leave this module, never a DOM.
|
|
4
|
+
import { htmlTreeEvents, isHtmlElement, parseBoundedHtml, MAX_STATIC_HTML_CHARS, MAX_STATIC_HTML_TAGS, MAX_STATIC_HTML_TREE_EVENTS, } from "./bounded-html-document.mjs";
|
|
6
5
|
import { MAX_CSP_META_POLICIES, MAX_RESPONSE_POLICY_HEADER_BYTES, } from "./response-policy.mjs";
|
|
6
|
+
import { MAX_FAVICON_BYTES, MAX_FAVICON_CANDIDATES } from "./outbound-request-bounds.mjs";
|
|
7
7
|
import { MAX_PAGE_PUBLICATION_DECLARATIONS, MAX_PAGE_PUBLICATION_META_ELEMENTS, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES, } from "./homepage-metadata-contract.mjs";
|
|
8
|
-
const
|
|
9
|
-
const MAX_STATIC_HTML_TAGS = 8_192;
|
|
10
|
-
const MAX_STATIC_STRUCTURE_TOKENS = 4_096;
|
|
11
|
-
const MAX_TECHNOLOGY_TAGS = 2_048;
|
|
8
|
+
const MAX_STATIC_STRUCTURE_TOKENS = MAX_STATIC_HTML_TREE_EVENTS;
|
|
12
9
|
const MAX_TAG_LENGTH = 4_096;
|
|
13
10
|
const MAX_ATTRIBUTES_PER_TAG = 128;
|
|
14
11
|
const MAX_SCRIPT_ELEMENTS = 64;
|
|
15
12
|
const MAX_SCRIPT_REFERENCE_LENGTH = 2_048;
|
|
16
13
|
const MAX_SCRIPT_MEDIA_TYPE_LENGTH = 120;
|
|
17
|
-
const MAX_INLINE_SCRIPT_CHARS = 32_768;
|
|
18
14
|
const MAX_INLINE_SCRIPT_TOTAL_CHARS = 65_536;
|
|
15
|
+
const MAX_INLINE_SCRIPT_CHARS = MAX_INLINE_SCRIPT_TOTAL_CHARS;
|
|
19
16
|
const MAX_STATIC_VISIBLE_TEXT_CHARS = MAX_STATIC_HTML_CHARS;
|
|
20
17
|
const MAX_STATIC_FORMS = 50;
|
|
21
18
|
const MAX_STATIC_INPUTS = 500;
|
|
@@ -39,29 +36,11 @@ const PAYMENT_AUTOCOMPLETE_TOKENS = new Set([
|
|
|
39
36
|
'transaction-currency',
|
|
40
37
|
'transaction-amount',
|
|
41
38
|
]);
|
|
42
|
-
const RAW_TEXT_MODES = Object.freeze({
|
|
43
|
-
iframe: TokenizerMode.RAWTEXT,
|
|
44
|
-
noembed: TokenizerMode.RAWTEXT,
|
|
45
|
-
noframes: TokenizerMode.RAWTEXT,
|
|
46
|
-
script: TokenizerMode.SCRIPT_DATA,
|
|
47
|
-
style: TokenizerMode.RAWTEXT,
|
|
48
|
-
template: TokenizerMode.RAWTEXT,
|
|
49
|
-
textarea: TokenizerMode.RCDATA,
|
|
50
|
-
title: TokenizerMode.RCDATA,
|
|
51
|
-
xmp: TokenizerMode.RAWTEXT,
|
|
52
|
-
});
|
|
53
39
|
const VOID_TAGS = new Set([
|
|
54
40
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta',
|
|
55
41
|
'param', 'source', 'track', 'wbr',
|
|
56
42
|
]);
|
|
57
|
-
const NON_VISIBLE_TEXT_TAGS = new Set(['script', 'style', 'template']);
|
|
58
|
-
const HEAD_CONTEXT_TAGS = new Set([
|
|
59
|
-
'html', 'head', 'base', 'basefont', 'bgsound', 'link', 'meta', 'title',
|
|
60
|
-
'noscript', 'noframes', 'style', 'template', 'script',
|
|
61
|
-
]);
|
|
62
|
-
const IMPLICIT_HEAD_TEXT_TAGS = new Set([
|
|
63
|
-
'title', 'noscript', 'noframes', 'style', 'template', 'script',
|
|
64
|
-
]);
|
|
43
|
+
const NON_VISIBLE_TEXT_TAGS = new Set(['head', 'script', 'style', 'template', 'noscript', 'iframe', 'noembed', 'noframes']);
|
|
65
44
|
const ROBOTS_DIRECTIVES = new Set([
|
|
66
45
|
'all', 'follow', 'index', 'max-image-preview', 'max-snippet', 'max-video-preview',
|
|
67
46
|
'noarchive', 'nocache', 'nofollow', 'noimageindex', 'noindex', 'none', 'nositelinkssearchbox',
|
|
@@ -73,7 +52,7 @@ const JAVASCRIPT_MEDIA_TYPES = new Set([
|
|
|
73
52
|
'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
|
|
74
53
|
'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript',
|
|
75
54
|
]);
|
|
76
|
-
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH) {
|
|
55
|
+
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH, lowercase = true) {
|
|
77
56
|
const attribute = attributes
|
|
78
57
|
.slice(0, MAX_ATTRIBUTES_PER_TAG)
|
|
79
58
|
.find((candidate) => candidate.name.toLowerCase() === name);
|
|
@@ -84,7 +63,7 @@ function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LEN
|
|
|
84
63
|
}
|
|
85
64
|
return {
|
|
86
65
|
present: true,
|
|
87
|
-
value: attribute.value.trim().toLowerCase(),
|
|
66
|
+
value: lowercase ? attribute.value.trim().toLowerCase() : attribute.value.trim(),
|
|
88
67
|
truncated: attributes.length > MAX_ATTRIBUTES_PER_TAG,
|
|
89
68
|
};
|
|
90
69
|
}
|
|
@@ -116,8 +95,8 @@ function formMethod(attributes) {
|
|
|
116
95
|
truncated: method.truncated,
|
|
117
96
|
};
|
|
118
97
|
}
|
|
119
|
-
function formAction(attributes,
|
|
120
|
-
const action = attributeValue(attributes, 'action');
|
|
98
|
+
function formAction(attributes, resolutionBase, documentOrigin) {
|
|
99
|
+
const action = attributeValue(attributes, 'action', MAX_FORM_ATTRIBUTE_LENGTH, false);
|
|
121
100
|
if (!action.present && action.truncated) {
|
|
122
101
|
return { relationship: 'unclassified', cleartext: false, truncated: true };
|
|
123
102
|
}
|
|
@@ -127,16 +106,16 @@ function formAction(attributes, baseUrl) {
|
|
|
127
106
|
if (action.value === null) {
|
|
128
107
|
return { relationship: 'unclassified', cleartext: false, truncated: true };
|
|
129
108
|
}
|
|
130
|
-
if (!
|
|
109
|
+
if (!resolutionBase || !documentOrigin) {
|
|
131
110
|
return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
|
|
132
111
|
}
|
|
133
112
|
try {
|
|
134
|
-
const parsed = new URL(action.value,
|
|
113
|
+
const parsed = new URL(action.value, resolutionBase);
|
|
135
114
|
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
|
|
136
115
|
return { relationship: 'unclassified', cleartext: false, truncated: action.truncated };
|
|
137
116
|
}
|
|
138
117
|
return {
|
|
139
|
-
relationship: parsed.origin ===
|
|
118
|
+
relationship: parsed.origin === documentOrigin ? 'sameOrigin' : 'external',
|
|
140
119
|
cleartext: parsed.protocol === 'http:',
|
|
141
120
|
truncated: action.truncated,
|
|
142
121
|
};
|
|
@@ -218,29 +197,18 @@ function stylesheetCandidate(attributes) {
|
|
|
218
197
|
truncated: rel.truncated || href.truncated || disabled.truncated || media.truncated,
|
|
219
198
|
};
|
|
220
199
|
}
|
|
221
|
-
function serializedStartTag(tagName, attributes) {
|
|
222
|
-
let serialized = `<${tagName.toLowerCase()}`;
|
|
223
|
-
let limitReached = attributes.length > MAX_ATTRIBUTES_PER_TAG;
|
|
224
|
-
for (const attribute of attributes.slice(0, MAX_ATTRIBUTES_PER_TAG)) {
|
|
225
|
-
const candidate = ` ${attribute.name.toLowerCase()}="${attribute.value.toLowerCase()}"`;
|
|
226
|
-
if (serialized.length + candidate.length + 1 > MAX_TAG_LENGTH) {
|
|
227
|
-
limitReached = true;
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
serialized += candidate;
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
markup: serialized.length + 1 <= MAX_TAG_LENGTH ? `${serialized}>` : null,
|
|
234
|
-
limitReached,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
200
|
function analyzeStaticHtml(value, options = {}) {
|
|
238
|
-
const
|
|
239
|
-
const inputLimitReached =
|
|
240
|
-
const
|
|
241
|
-
|
|
201
|
+
const parsed = parseBoundedHtml(value);
|
|
202
|
+
const { inputLimitReached } = parsed;
|
|
203
|
+
const documentUrl = safeBaseUrl(options.baseUrl);
|
|
204
|
+
let effectiveBaseUrl = documentUrl;
|
|
205
|
+
let baseHrefState = 'absent';
|
|
242
206
|
const includeVisibleText = options.includeVisibleText === true;
|
|
243
|
-
const
|
|
207
|
+
const elements = [];
|
|
208
|
+
const iconLinks = [];
|
|
209
|
+
const iconCounts = [0, 0];
|
|
210
|
+
const tokens = [];
|
|
211
|
+
const elementStack = [];
|
|
244
212
|
const visibleTextParts = [];
|
|
245
213
|
const structureTokens = [];
|
|
246
214
|
const scripts = [];
|
|
@@ -296,11 +264,10 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
296
264
|
};
|
|
297
265
|
const robotDirectives = new Set();
|
|
298
266
|
const twitterCardTypes = new Set();
|
|
299
|
-
let tokenizer;
|
|
300
267
|
let activeInlineScript = null;
|
|
301
268
|
let tagsExamined = 0;
|
|
302
269
|
let inlineCharactersExamined = 0;
|
|
303
|
-
let tagLimitReached =
|
|
270
|
+
let tagLimitReached = parsed.constructionLimitReached;
|
|
304
271
|
let structureLimitReached = false;
|
|
305
272
|
let scriptLimitReached = false;
|
|
306
273
|
let inlineLimitReached = false;
|
|
@@ -308,20 +275,28 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
308
275
|
let cspMetaLimitReached = false;
|
|
309
276
|
let publicationMetaElements = 0;
|
|
310
277
|
let robotDirectiveTokens = 0;
|
|
311
|
-
let publicationTagLimitReached =
|
|
278
|
+
let publicationTagLimitReached = parsed.constructionLimitReached;
|
|
312
279
|
let insideExplicitHead = false;
|
|
313
|
-
let
|
|
314
|
-
let headScopeClosed = false;
|
|
315
|
-
let bodyContentStarted = false;
|
|
280
|
+
let insideHead = false;
|
|
316
281
|
let scriptElementSeen = false;
|
|
317
282
|
let nonVisibleTextDepth = 0;
|
|
318
283
|
let visibleTextCharacters = 0;
|
|
284
|
+
let titleDepth = 0;
|
|
285
|
+
let titleCharacters = 0;
|
|
286
|
+
const titleParts = [];
|
|
287
|
+
const pendingFormActions = [];
|
|
288
|
+
function appendTitle(chars) {
|
|
289
|
+
if (titleDepth === 0 || !chars || titleCharacters >= 201)
|
|
290
|
+
return;
|
|
291
|
+
const retained = chars.slice(0, 201 - titleCharacters);
|
|
292
|
+
titleParts.push(retained);
|
|
293
|
+
titleCharacters += retained.length;
|
|
294
|
+
}
|
|
319
295
|
function appendInlineScript(chars) {
|
|
320
296
|
if (!activeInlineScript || !chars)
|
|
321
297
|
return;
|
|
322
|
-
const perScriptRemaining = MAX_INLINE_SCRIPT_CHARS - activeInlineScript.inlineContent.length;
|
|
323
298
|
const totalRemaining = MAX_INLINE_SCRIPT_TOTAL_CHARS - inlineCharactersExamined;
|
|
324
|
-
const retainedLength = Math.max(0, Math.min(chars.length,
|
|
299
|
+
const retainedLength = Math.max(0, Math.min(chars.length, totalRemaining));
|
|
325
300
|
if (retainedLength > 0) {
|
|
326
301
|
activeInlineScript.inlineContent += chars.slice(0, retainedLength);
|
|
327
302
|
inlineCharactersExamined += retainedLength;
|
|
@@ -348,32 +323,118 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
348
323
|
if (retainedLength < chars.length)
|
|
349
324
|
visibleTextLimitReached = true;
|
|
350
325
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
326
|
+
function appendToken(token) {
|
|
327
|
+
if (tokens.length >= MAX_STATIC_STRUCTURE_TOKENS)
|
|
328
|
+
structureLimitReached = true;
|
|
329
|
+
else
|
|
330
|
+
tokens.push(token);
|
|
331
|
+
}
|
|
332
|
+
for (const event of htmlTreeEvents(parsed.document)) {
|
|
333
|
+
if (event.kind === 'text') {
|
|
334
|
+
appendInlineScript(event.text);
|
|
335
|
+
appendTitle(event.text);
|
|
336
|
+
appendVisibleText(event.text);
|
|
337
|
+
if (nonVisibleTextDepth === 0)
|
|
338
|
+
appendToken({ kind: 'text', value: event.text });
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const node = event.element;
|
|
342
|
+
const tagName = node.tagName.toLowerCase();
|
|
343
|
+
const htmlElement = isHtmlElement(node);
|
|
344
|
+
if (event.kind === 'end') {
|
|
345
|
+
elementStack.pop();
|
|
346
|
+
if (tagName === 'script')
|
|
362
347
|
activeInlineScript = null;
|
|
363
|
-
|
|
348
|
+
if (htmlElement && tagName === 'title')
|
|
349
|
+
titleDepth = 0;
|
|
350
|
+
if (htmlElement && tagName === 'head') {
|
|
351
|
+
insideHead = false;
|
|
352
|
+
insideExplicitHead = false;
|
|
353
|
+
}
|
|
354
|
+
if (!htmlElement || !VOID_TAGS.has(tagName)) {
|
|
355
|
+
appendStructureToken(`/${tagName}`);
|
|
356
|
+
appendToken({ kind: 'end', name: tagName });
|
|
357
|
+
}
|
|
358
|
+
if (NON_VISIBLE_TEXT_TAGS.has(tagName))
|
|
359
|
+
nonVisibleTextDepth -= 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
{
|
|
363
|
+
const token = { attrs: node.attrs };
|
|
364
|
+
if (NON_VISIBLE_TEXT_TAGS.has(tagName))
|
|
365
|
+
nonVisibleTextDepth += 1;
|
|
366
|
+
if (node.sourceCodeLocation?.startTag)
|
|
367
|
+
tagsExamined += 1;
|
|
368
|
+
appendStructureToken(htmlElement && VOID_TAGS.has(tagName) ? `${tagName}/` : tagName);
|
|
369
|
+
if (htmlElement && tagName === 'head') {
|
|
370
|
+
insideHead = true;
|
|
371
|
+
insideExplicitHead = Boolean(node.sourceCodeLocation?.startTag);
|
|
364
372
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
373
|
+
const inPublicationHead = insideHead;
|
|
374
|
+
const attributes = [];
|
|
375
|
+
let attributeLength = 0;
|
|
376
|
+
for (const attribute of node.attrs) {
|
|
377
|
+
attributeLength += attribute.name.length + attribute.value.length;
|
|
378
|
+
if (attributes.length >= MAX_ATTRIBUTES_PER_TAG || attributeLength > MAX_TAG_LENGTH)
|
|
379
|
+
break;
|
|
380
|
+
attributes.push({ name: attribute.name, value: attribute.value });
|
|
381
|
+
}
|
|
382
|
+
const element = {
|
|
383
|
+
name: tagName, html: htmlElement, attributes,
|
|
384
|
+
attributesTruncated: attributes.length !== node.attrs.length,
|
|
385
|
+
parent: elementStack.at(-1) ?? null, inHead: insideHead,
|
|
386
|
+
};
|
|
387
|
+
elementStack.push(elements.length);
|
|
388
|
+
elements.push(element);
|
|
389
|
+
appendToken({ kind: 'start', element });
|
|
390
|
+
// Foreign drawing attributes are not HTML technology, form or resource
|
|
391
|
+
// inputs. Their per-element omission still reaches fingerprinting, but
|
|
392
|
+
// must not make unrelated HTML evidence incomplete.
|
|
393
|
+
if (htmlElement && element.attributesTruncated)
|
|
394
|
+
tagLimitReached = true;
|
|
395
|
+
if (!htmlElement)
|
|
396
|
+
continue;
|
|
397
|
+
if (tagName === 'link') {
|
|
398
|
+
const rel = node.attrs.find((attribute) => attribute.name === 'rel')?.value;
|
|
399
|
+
const href = node.attrs.find((attribute) => attribute.name === 'href')?.value.trim();
|
|
400
|
+
if (rel && rel.length <= 128 && href) {
|
|
401
|
+
const relations = rel.toLowerCase().split(/[\t\n\f\r ]+/u);
|
|
402
|
+
const priority = relations.includes('icon') ? 0
|
|
403
|
+
: relations.some((relation) => ['apple-touch-icon', 'apple-touch-icon-precomposed'].includes(relation)) ? 1 : null;
|
|
404
|
+
// Inline image bytes have their own bound; the general 4 KiB
|
|
405
|
+
// attribute projection must not silently discard supported icons.
|
|
406
|
+
const maxHref = /^data:/iu.test(href) ? MAX_FAVICON_BYTES * 3 + 128 : 2048;
|
|
407
|
+
if (priority !== null && iconCounts[priority] < MAX_FAVICON_CANDIDATES && href.length <= maxHref) {
|
|
408
|
+
iconLinks.push({ href, priority });
|
|
409
|
+
iconCounts[priority] += 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
372
412
|
}
|
|
373
|
-
|
|
374
|
-
|
|
413
|
+
// The first document base applies even when it appears in the body;
|
|
414
|
+
// publication metadata's separate head-only policy does not govern URLs.
|
|
415
|
+
if (tagName === 'base' && baseHrefState === 'absent') {
|
|
416
|
+
const href = attributeValue(token.attrs, 'href', MAX_FORM_ATTRIBUTE_LENGTH, false);
|
|
417
|
+
if (href.truncated && !href.present) {
|
|
418
|
+
effectiveBaseUrl = documentUrl;
|
|
419
|
+
baseHrefState = 'invalid';
|
|
420
|
+
}
|
|
421
|
+
else if (href.present) {
|
|
422
|
+
try {
|
|
423
|
+
const parsed = href.value !== null && documentUrl ? new URL(href.value, documentUrl) : null;
|
|
424
|
+
if (!parsed || !['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
|
|
425
|
+
throw new Error('Invalid document base URL.');
|
|
426
|
+
}
|
|
427
|
+
effectiveBaseUrl = parsed;
|
|
428
|
+
baseHrefState = 'valid';
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
effectiveBaseUrl = documentUrl;
|
|
432
|
+
baseHrefState = 'invalid';
|
|
433
|
+
}
|
|
434
|
+
}
|
|
375
435
|
}
|
|
376
|
-
|
|
436
|
+
if (tagName === 'title' && titleParts.length === 0)
|
|
437
|
+
titleDepth = 1;
|
|
377
438
|
if (/^h[1-6]$/u.test(tagName)) {
|
|
378
439
|
const key = tagName;
|
|
379
440
|
publicationMetadata.headings[key] += 1;
|
|
@@ -414,10 +475,10 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
414
475
|
if (candidate.candidate)
|
|
415
476
|
publicationMetadata.renderBlockingCandidates.stylesheet += 1;
|
|
416
477
|
}
|
|
417
|
-
if (tagName === 'meta' &&
|
|
478
|
+
if (tagName === 'meta' && insideHead) {
|
|
418
479
|
const httpEquiv = attributeValue(token.attrs, 'http-equiv', 64);
|
|
419
480
|
if (httpEquiv.value === 'content-security-policy') {
|
|
420
|
-
const content = attributeValue(token.attrs, 'content', MAX_RESPONSE_POLICY_HEADER_BYTES);
|
|
481
|
+
const content = attributeValue(token.attrs, 'content', MAX_RESPONSE_POLICY_HEADER_BYTES, false);
|
|
421
482
|
if (content.value === null || !content.value) {
|
|
422
483
|
cspMetaLimitReached = cspMetaLimitReached || content.present || content.truncated;
|
|
423
484
|
}
|
|
@@ -548,16 +609,6 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
548
609
|
}
|
|
549
610
|
}
|
|
550
611
|
}
|
|
551
|
-
if (markup.length < MAX_TECHNOLOGY_TAGS) {
|
|
552
|
-
const serialized = serializedStartTag(tagName, token.attrs);
|
|
553
|
-
if (serialized.limitReached)
|
|
554
|
-
tagLimitReached = true;
|
|
555
|
-
if (serialized.markup)
|
|
556
|
-
markup.push(serialized.markup);
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
tagLimitReached = true;
|
|
560
|
-
}
|
|
561
612
|
if (tagName === 'form') {
|
|
562
613
|
if (forms.formsObserved >= MAX_STATIC_FORMS) {
|
|
563
614
|
forms.truncated = true;
|
|
@@ -566,11 +617,8 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
566
617
|
forms.formsObserved += 1;
|
|
567
618
|
const method = formMethod(token.attrs);
|
|
568
619
|
forms.methods[method.value] += 1;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (action.cleartext)
|
|
572
|
-
forms.actions.cleartext += 1;
|
|
573
|
-
if (method.truncated || action.truncated)
|
|
620
|
+
pendingFormActions.push(token.attrs);
|
|
621
|
+
if (method.truncated || token.attrs.length > MAX_ATTRIBUTES_PER_TAG)
|
|
574
622
|
forms.truncated = true;
|
|
575
623
|
}
|
|
576
624
|
}
|
|
@@ -591,13 +639,13 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
591
639
|
}
|
|
592
640
|
if (tagName !== 'script') {
|
|
593
641
|
activeInlineScript = null;
|
|
594
|
-
|
|
642
|
+
continue;
|
|
595
643
|
}
|
|
596
644
|
scriptElementSeen = true;
|
|
597
645
|
if (scripts.length >= MAX_SCRIPT_ELEMENTS) {
|
|
598
646
|
scriptLimitReached = true;
|
|
599
647
|
activeInlineScript = null;
|
|
600
|
-
|
|
648
|
+
continue;
|
|
601
649
|
}
|
|
602
650
|
const reference = scriptReference(token.attrs);
|
|
603
651
|
const script = {
|
|
@@ -607,49 +655,18 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
607
655
|
};
|
|
608
656
|
scripts.push(script);
|
|
609
657
|
activeInlineScript = reference ? null : script;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (!VOID_TAGS.has(tagName))
|
|
623
|
-
appendStructureToken(`/${tagName}`);
|
|
624
|
-
if (NON_VISIBLE_TEXT_TAGS.has(tagName) && nonVisibleTextDepth > 0)
|
|
625
|
-
nonVisibleTextDepth -= 1;
|
|
626
|
-
},
|
|
627
|
-
onCharacter(token) {
|
|
628
|
-
if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0 && token.chars.trim()) {
|
|
629
|
-
bodyContentStarted = true;
|
|
630
|
-
headScopeClosed = true;
|
|
631
|
-
}
|
|
632
|
-
appendInlineScript(token.chars);
|
|
633
|
-
appendVisibleText(token.chars);
|
|
634
|
-
},
|
|
635
|
-
onNullCharacter(token) {
|
|
636
|
-
if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0) {
|
|
637
|
-
bodyContentStarted = true;
|
|
638
|
-
headScopeClosed = true;
|
|
639
|
-
}
|
|
640
|
-
appendInlineScript(token.chars);
|
|
641
|
-
appendVisibleText(token.chars);
|
|
642
|
-
},
|
|
643
|
-
onWhitespaceCharacter(token) {
|
|
644
|
-
appendInlineScript(token.chars);
|
|
645
|
-
appendVisibleText(token.chars);
|
|
646
|
-
},
|
|
647
|
-
onComment() { },
|
|
648
|
-
onDoctype() { },
|
|
649
|
-
onEof() { },
|
|
650
|
-
};
|
|
651
|
-
tokenizer = new Tokenizer({ sourceCodeLocationInfo: false }, handler);
|
|
652
|
-
tokenizer.write(html, true);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const attributes of pendingFormActions) {
|
|
661
|
+
const action = formAction(attributes, effectiveBaseUrl, documentUrl?.origin ?? null);
|
|
662
|
+
forms.actions[action.relationship] += 1;
|
|
663
|
+
if (action.cleartext)
|
|
664
|
+
forms.actions.cleartext += 1;
|
|
665
|
+
if (action.truncated)
|
|
666
|
+
forms.truncated = true;
|
|
667
|
+
}
|
|
668
|
+
if (baseHrefState === 'invalid')
|
|
669
|
+
forms.truncated = true;
|
|
653
670
|
publicationMetadata.truncated = publicationMetadata.truncated || inputLimitReached || publicationTagLimitReached;
|
|
654
671
|
publicationMetadata.documentTruncated = inputLimitReached || publicationTagLimitReached;
|
|
655
672
|
if (publicationMetadata.documentTruncated) {
|
|
@@ -671,7 +688,17 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
671
688
|
publicationMetadata.renderBlockingCandidates.total = publicationMetadata.renderBlockingCandidates.script
|
|
672
689
|
+ publicationMetadata.renderBlockingCandidates.stylesheet;
|
|
673
690
|
return {
|
|
674
|
-
|
|
691
|
+
title: (() => {
|
|
692
|
+
const value = titleParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\ufffd/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
693
|
+
if (!value)
|
|
694
|
+
return null;
|
|
695
|
+
return value.length > 200 ? `${value.slice(0, 199)}…` : value;
|
|
696
|
+
})(),
|
|
697
|
+
effectiveBaseUrl: effectiveBaseUrl?.toString() ?? null,
|
|
698
|
+
baseHrefState,
|
|
699
|
+
elements,
|
|
700
|
+
iconLinks: iconLinks.sort((left, right) => left.priority - right.priority).slice(0, MAX_FAVICON_CANDIDATES),
|
|
701
|
+
tokens,
|
|
675
702
|
visibleText: visibleTextParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\s+/gu, ' '),
|
|
676
703
|
structureTokens,
|
|
677
704
|
scripts,
|
|
@@ -689,4 +716,4 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
689
716
|
inlineCharactersExamined,
|
|
690
717
|
};
|
|
691
718
|
}
|
|
692
|
-
export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_PUBLICATION_META_ELEMENTS, MAX_STATIC_PUBLICATION_DECLARATIONS, MAX_STATIC_ROBOTS_DIRECTIVES, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH,
|
|
719
|
+
export { MAX_ATTRIBUTES_PER_TAG, MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_MEDIA_TYPE_LENGTH, MAX_STATIC_FORMS, MAX_STATIC_HTML_CHARS, MAX_STATIC_INPUTS, MAX_STATIC_PUBLICATION_META_ELEMENTS, MAX_STATIC_PUBLICATION_DECLARATIONS, MAX_STATIC_ROBOTS_DIRECTIVES, MAX_STATIC_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, analyzeStaticHtml, };
|
|
@@ -290,6 +290,7 @@ function analyzeStructuredDataIdentity(input = {}) {
|
|
|
290
290
|
}
|
|
291
291
|
const truncated = input.sourceTruncated === true
|
|
292
292
|
|| htmlAnalysis.inputLimitReached
|
|
293
|
+
|| htmlAnalysis.tagLimitReached
|
|
293
294
|
|| htmlAnalysis.scriptLimitReached
|
|
294
295
|
|| htmlAnalysis.inlineLimitReached
|
|
295
296
|
|| limitReached;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
2
|
+
export function attributeValue(element, name) {
|
|
3
|
+
return element.attributes.find((attribute) => attribute.name === name)?.value.toLowerCase() ?? '';
|
|
4
|
+
}
|
|
5
|
+
export function hasAttributes(element, ...names) {
|
|
6
|
+
return element.attributes.some((attribute) => names.includes(attribute.name));
|
|
7
|
+
}
|
|
8
|
+
function httpUrl(value, base) {
|
|
9
|
+
if (typeof value !== 'string' || value.length > 2048 || CONTROL_RE.test(value) || !value.trim())
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(value, base);
|
|
13
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password ? url : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function createTechnologyMarkupContext(analysis, effectiveBaseUrl, documentOrigin) {
|
|
20
|
+
const elements = analysis.elements.filter((element) => element.html && element.name !== 'template');
|
|
21
|
+
const documentUrl = httpUrl(documentOrigin);
|
|
22
|
+
const suppliedBase = effectiveBaseUrl ?? analysis.effectiveBaseUrl;
|
|
23
|
+
const base = httpUrl(suppliedBase) ?? documentUrl;
|
|
24
|
+
// A reserved base permits legacy minimised relative references without
|
|
25
|
+
// attributing an absolute external URL to an unknown document origin.
|
|
26
|
+
const resolutionBase = base ?? new URL('https://document.invalid/');
|
|
27
|
+
const references = (element, names) => (element.attributes.flatMap((attribute) => {
|
|
28
|
+
if (!names.includes(attribute.name))
|
|
29
|
+
return [];
|
|
30
|
+
if (attribute.value.trim().startsWith('#'))
|
|
31
|
+
return [];
|
|
32
|
+
const url = httpUrl(attribute.value, resolutionBase);
|
|
33
|
+
if (!url)
|
|
34
|
+
return [];
|
|
35
|
+
const relative = !/^(?:[a-z][a-z0-9+.-]*:|\/\/)/iu.test(attribute.value.trim());
|
|
36
|
+
const embedded = documentUrl
|
|
37
|
+
? url.origin !== documentUrl.origin
|
|
38
|
+
: Boolean(base || suppliedBase || documentOrigin) || !relative;
|
|
39
|
+
return [{ url, embedded }];
|
|
40
|
+
}));
|
|
41
|
+
const resources = [];
|
|
42
|
+
const navigation = [];
|
|
43
|
+
for (const element of elements) {
|
|
44
|
+
switch (element.name) {
|
|
45
|
+
case 'script':
|
|
46
|
+
case 'img':
|
|
47
|
+
case 'iframe':
|
|
48
|
+
case 'frame':
|
|
49
|
+
case 'source':
|
|
50
|
+
case 'audio':
|
|
51
|
+
case 'embed':
|
|
52
|
+
resources.push(...references(element, ['src']));
|
|
53
|
+
break;
|
|
54
|
+
case 'video':
|
|
55
|
+
resources.push(...references(element, ['src', 'poster']));
|
|
56
|
+
break;
|
|
57
|
+
case 'link': {
|
|
58
|
+
const relations = attributeValue(element, 'rel').trim().split(/\s+/u);
|
|
59
|
+
// Existing minimised inputs retain a link reference without its rel.
|
|
60
|
+
// Explicit document metadata or navigation relations are not assets.
|
|
61
|
+
if (!hasAttributes(element, 'rel') || relations.some((relation) => ['stylesheet', 'preload', 'modulepreload', 'icon'].includes(relation))) {
|
|
62
|
+
resources.push(...references(element, ['href']));
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'object':
|
|
67
|
+
resources.push(...references(element, ['data']));
|
|
68
|
+
break;
|
|
69
|
+
case 'input':
|
|
70
|
+
if (attributeValue(element, 'type') === 'image')
|
|
71
|
+
resources.push(...references(element, ['src']));
|
|
72
|
+
break;
|
|
73
|
+
case 'a':
|
|
74
|
+
case 'area':
|
|
75
|
+
navigation.push(...references(element, ['href']));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { elements, resources, navigation };
|
|
80
|
+
}
|
|
81
|
+
export function elementMarker(matches, reviewMarkup) {
|
|
82
|
+
return { matches: ({ elements }) => elements.some(matches), reviewMarkup };
|
|
83
|
+
}
|
|
84
|
+
export function resourceMarker(matches, reviewPath) {
|
|
85
|
+
const sharedOrigin = reviewPath.startsWith('https://');
|
|
86
|
+
return {
|
|
87
|
+
matches: ({ resources }) => resources.some(({ url }) => matches(url)),
|
|
88
|
+
embedded: ({ resources }) => sharedOrigin || !resources.some(({ url, embedded }) => !embedded && matches(url)),
|
|
89
|
+
reviewMarkup: `<link href="${reviewPath}">`,
|
|
90
|
+
embeddedReviewMarkup: `<link href="${sharedOrigin ? reviewPath : `https://embedded.invalid${reviewPath}`}">`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function navigationMarker(matches, reviewPath) {
|
|
94
|
+
return {
|
|
95
|
+
matches: ({ navigation }) => navigation.some(({ url }) => matches(url)),
|
|
96
|
+
embedded: ({ navigation }) => !navigation.some(({ url, embedded }) => !embedded && matches(url)),
|
|
97
|
+
reviewMarkup: `<a href="${reviewPath}"></a>`,
|
|
98
|
+
embeddedReviewMarkup: `<a href="https://embedded.invalid/${reviewPath}"></a>`,
|
|
99
|
+
};
|
|
100
|
+
}
|