@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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/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/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 +12 -4
- 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} +70 -47
- 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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Only normalized start-tag markup and capped script indicators are retained.
|
|
5
5
|
import { Tokenizer, TokenizerMode } from 'parse5';
|
|
6
6
|
import { MAX_CSP_META_POLICIES, MAX_RESPONSE_POLICY_HEADER_BYTES, } from "./response-policy.mjs";
|
|
7
|
+
import { MAX_PAGE_PUBLICATION_DECLARATIONS, MAX_PAGE_PUBLICATION_META_ELEMENTS, MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES, } from "./homepage-metadata-contract.mjs";
|
|
7
8
|
const MAX_STATIC_HTML_CHARS = 300_000;
|
|
8
9
|
const MAX_STATIC_HTML_TAGS = 8_192;
|
|
9
10
|
const MAX_STATIC_STRUCTURE_TOKENS = 4_096;
|
|
@@ -18,8 +19,12 @@ const MAX_INLINE_SCRIPT_TOTAL_CHARS = 65_536;
|
|
|
18
19
|
const MAX_STATIC_VISIBLE_TEXT_CHARS = MAX_STATIC_HTML_CHARS;
|
|
19
20
|
const MAX_STATIC_FORMS = 50;
|
|
20
21
|
const MAX_STATIC_INPUTS = 500;
|
|
22
|
+
const MAX_STATIC_PUBLICATION_META_ELEMENTS = MAX_PAGE_PUBLICATION_META_ELEMENTS;
|
|
23
|
+
const MAX_STATIC_PUBLICATION_DECLARATIONS = MAX_PAGE_PUBLICATION_DECLARATIONS;
|
|
24
|
+
const MAX_STATIC_ROBOTS_DIRECTIVES = MAX_PAGE_PUBLICATION_ROBOTS_DIRECTIVES;
|
|
21
25
|
const MAX_FORM_ATTRIBUTE_LENGTH = 2_048;
|
|
22
|
-
const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]
|
|
26
|
+
const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
27
|
+
const CONTROL_CHARACTER_RE_GLOBAL = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
23
28
|
const PAYMENT_AUTOCOMPLETE_TOKENS = new Set([
|
|
24
29
|
'cc-name',
|
|
25
30
|
'cc-given-name',
|
|
@@ -50,6 +55,24 @@ const VOID_TAGS = new Set([
|
|
|
50
55
|
'param', 'source', 'track', 'wbr',
|
|
51
56
|
]);
|
|
52
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
|
+
]);
|
|
65
|
+
const ROBOTS_DIRECTIVES = new Set([
|
|
66
|
+
'all', 'follow', 'index', 'max-image-preview', 'max-snippet', 'max-video-preview',
|
|
67
|
+
'noarchive', 'nocache', 'nofollow', 'noimageindex', 'noindex', 'none', 'nositelinkssearchbox',
|
|
68
|
+
'nosnippet', 'notranslate', 'unavailable_after',
|
|
69
|
+
]);
|
|
70
|
+
const JAVASCRIPT_MEDIA_TYPES = new Set([
|
|
71
|
+
'application/ecmascript', 'application/javascript', 'application/x-ecmascript',
|
|
72
|
+
'application/x-javascript', 'text/ecmascript', 'text/javascript', 'text/javascript1.0',
|
|
73
|
+
'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
|
|
74
|
+
'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript',
|
|
75
|
+
]);
|
|
53
76
|
function attributeValue(attributes, name, maximumLength = MAX_FORM_ATTRIBUTE_LENGTH) {
|
|
54
77
|
const attribute = attributes
|
|
55
78
|
.slice(0, MAX_ATTRIBUTES_PER_TAG)
|
|
@@ -171,6 +194,30 @@ function scriptMediaType(attributes) {
|
|
|
171
194
|
}
|
|
172
195
|
return null;
|
|
173
196
|
}
|
|
197
|
+
function classicScriptCandidate(attributes) {
|
|
198
|
+
const type = attributeValue(attributes, 'type', MAX_SCRIPT_MEDIA_TYPE_LENGTH);
|
|
199
|
+
const asyncAttribute = attributeValue(attributes, 'async', 10);
|
|
200
|
+
const deferAttribute = attributeValue(attributes, 'defer', 10);
|
|
201
|
+
const truncated = type.truncated || asyncAttribute.truncated || deferAttribute.truncated;
|
|
202
|
+
if (asyncAttribute.present || deferAttribute.present || type.value === 'module')
|
|
203
|
+
return { candidate: false, truncated };
|
|
204
|
+
return {
|
|
205
|
+
candidate: !type.present || type.value === '' || Boolean(type.value && JAVASCRIPT_MEDIA_TYPES.has(type.value)),
|
|
206
|
+
truncated,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function stylesheetCandidate(attributes) {
|
|
210
|
+
const rel = attributeValue(attributes, 'rel', 320);
|
|
211
|
+
const href = attributeValue(attributes, 'href', MAX_SCRIPT_REFERENCE_LENGTH);
|
|
212
|
+
const disabled = attributeValue(attributes, 'disabled', 10);
|
|
213
|
+
const media = attributeValue(attributes, 'media', 120);
|
|
214
|
+
return {
|
|
215
|
+
candidate: rel.value?.split(/\s+/u).includes('stylesheet') === true
|
|
216
|
+
&& href.present && Boolean(href.value) && !disabled.present
|
|
217
|
+
&& (!media.present || media.value === '' || media.value === 'all' || media.value === 'screen'),
|
|
218
|
+
truncated: rel.truncated || href.truncated || disabled.truncated || media.truncated,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
174
221
|
function serializedStartTag(tagName, attributes) {
|
|
175
222
|
let serialized = `<${tagName.toLowerCase()}`;
|
|
176
223
|
let limitReached = attributes.length > MAX_ATTRIBUTES_PER_TAG;
|
|
@@ -207,6 +254,48 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
207
254
|
actions: { sameOrigin: 0, external: 0, missing: 0, cleartext: 0, unclassified: 0 },
|
|
208
255
|
truncated: false,
|
|
209
256
|
};
|
|
257
|
+
const publicationMetadata = {
|
|
258
|
+
truncated: false,
|
|
259
|
+
documentTruncated: false,
|
|
260
|
+
robots: {
|
|
261
|
+
observed: false,
|
|
262
|
+
malformed: false,
|
|
263
|
+
truncated: false,
|
|
264
|
+
directives: [],
|
|
265
|
+
recognizedDirectiveCount: 0,
|
|
266
|
+
unknownDirectiveCount: 0,
|
|
267
|
+
conflicting: false,
|
|
268
|
+
},
|
|
269
|
+
twitterCard: {
|
|
270
|
+
observed: false,
|
|
271
|
+
malformed: false,
|
|
272
|
+
truncated: false,
|
|
273
|
+
cardTypes: [],
|
|
274
|
+
declarationCount: 0,
|
|
275
|
+
titlePresent: false,
|
|
276
|
+
descriptionPresent: false,
|
|
277
|
+
imagePresent: false,
|
|
278
|
+
imageAltPresent: false,
|
|
279
|
+
sitePresent: false,
|
|
280
|
+
creatorPresent: false,
|
|
281
|
+
playerPresent: false,
|
|
282
|
+
appPresent: false,
|
|
283
|
+
},
|
|
284
|
+
headings: { truncated: false, total: 0, h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 },
|
|
285
|
+
images: {
|
|
286
|
+
totalComplete: true,
|
|
287
|
+
classificationComplete: true,
|
|
288
|
+
truncated: false,
|
|
289
|
+
total: 0,
|
|
290
|
+
altMissing: 0,
|
|
291
|
+
altEmpty: 0,
|
|
292
|
+
altNonEmpty: 0,
|
|
293
|
+
altUnclassified: 0,
|
|
294
|
+
},
|
|
295
|
+
renderBlockingCandidates: { truncated: false, script: 0, stylesheet: 0, total: 0 },
|
|
296
|
+
};
|
|
297
|
+
const robotDirectives = new Set();
|
|
298
|
+
const twitterCardTypes = new Set();
|
|
210
299
|
let tokenizer;
|
|
211
300
|
let activeInlineScript = null;
|
|
212
301
|
let tagsExamined = 0;
|
|
@@ -217,7 +306,13 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
217
306
|
let inlineLimitReached = false;
|
|
218
307
|
let visibleTextLimitReached = false;
|
|
219
308
|
let cspMetaLimitReached = false;
|
|
309
|
+
let publicationMetaElements = 0;
|
|
310
|
+
let robotDirectiveTokens = 0;
|
|
311
|
+
let publicationTagLimitReached = false;
|
|
220
312
|
let insideExplicitHead = false;
|
|
313
|
+
let implicitHeadTextDepth = 0;
|
|
314
|
+
let headScopeClosed = false;
|
|
315
|
+
let bodyContentStarted = false;
|
|
221
316
|
let scriptElementSeen = false;
|
|
222
317
|
let nonVisibleTextDepth = 0;
|
|
223
318
|
let visibleTextCharacters = 0;
|
|
@@ -263,6 +358,7 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
263
358
|
nonVisibleTextDepth += 1;
|
|
264
359
|
if (tagsExamined >= MAX_STATIC_HTML_TAGS) {
|
|
265
360
|
tagLimitReached = true;
|
|
361
|
+
publicationTagLimitReached = true;
|
|
266
362
|
activeInlineScript = null;
|
|
267
363
|
return;
|
|
268
364
|
}
|
|
@@ -270,6 +366,54 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
270
366
|
appendStructureToken(token.selfClosing || VOID_TAGS.has(tagName) ? `${tagName}/` : tagName);
|
|
271
367
|
if (tagName === 'head')
|
|
272
368
|
insideExplicitHead = true;
|
|
369
|
+
if (tagName === 'body' || (!insideExplicitHead && !HEAD_CONTEXT_TAGS.has(tagName))) {
|
|
370
|
+
bodyContentStarted = true;
|
|
371
|
+
headScopeClosed = true;
|
|
372
|
+
}
|
|
373
|
+
if (!insideExplicitHead && !headScopeClosed && !token.selfClosing && IMPLICIT_HEAD_TEXT_TAGS.has(tagName)) {
|
|
374
|
+
implicitHeadTextDepth += 1;
|
|
375
|
+
}
|
|
376
|
+
const inPublicationHead = insideExplicitHead || (!headScopeClosed && !bodyContentStarted && HEAD_CONTEXT_TAGS.has(tagName));
|
|
377
|
+
if (/^h[1-6]$/u.test(tagName)) {
|
|
378
|
+
const key = tagName;
|
|
379
|
+
publicationMetadata.headings[key] += 1;
|
|
380
|
+
publicationMetadata.headings.total += 1;
|
|
381
|
+
}
|
|
382
|
+
if (tagName === 'img') {
|
|
383
|
+
publicationMetadata.images.total += 1;
|
|
384
|
+
const alt = attributeValue(token.attrs, 'alt', MAX_FORM_ATTRIBUTE_LENGTH);
|
|
385
|
+
if (alt.truncated) {
|
|
386
|
+
publicationMetadata.truncated = true;
|
|
387
|
+
publicationMetadata.images.classificationComplete = false;
|
|
388
|
+
publicationMetadata.images.truncated = true;
|
|
389
|
+
}
|
|
390
|
+
if (alt.truncated && (!alt.present || alt.value === null))
|
|
391
|
+
publicationMetadata.images.altUnclassified += 1;
|
|
392
|
+
else if (!alt.present)
|
|
393
|
+
publicationMetadata.images.altMissing += 1;
|
|
394
|
+
else if (alt.value === '')
|
|
395
|
+
publicationMetadata.images.altEmpty += 1;
|
|
396
|
+
else if (alt.value !== null)
|
|
397
|
+
publicationMetadata.images.altNonEmpty += 1;
|
|
398
|
+
}
|
|
399
|
+
if (insideExplicitHead && tagName === 'script') {
|
|
400
|
+
const candidate = classicScriptCandidate(token.attrs);
|
|
401
|
+
if (candidate.truncated) {
|
|
402
|
+
publicationMetadata.truncated = true;
|
|
403
|
+
publicationMetadata.renderBlockingCandidates.truncated = true;
|
|
404
|
+
}
|
|
405
|
+
if (candidate.candidate)
|
|
406
|
+
publicationMetadata.renderBlockingCandidates.script += 1;
|
|
407
|
+
}
|
|
408
|
+
else if (insideExplicitHead && tagName === 'link') {
|
|
409
|
+
const candidate = stylesheetCandidate(token.attrs);
|
|
410
|
+
if (candidate.truncated) {
|
|
411
|
+
publicationMetadata.truncated = true;
|
|
412
|
+
publicationMetadata.renderBlockingCandidates.truncated = true;
|
|
413
|
+
}
|
|
414
|
+
if (candidate.candidate)
|
|
415
|
+
publicationMetadata.renderBlockingCandidates.stylesheet += 1;
|
|
416
|
+
}
|
|
273
417
|
if (tagName === 'meta' && insideExplicitHead) {
|
|
274
418
|
const httpEquiv = attributeValue(token.attrs, 'http-equiv', 64);
|
|
275
419
|
if (httpEquiv.value === 'content-security-policy') {
|
|
@@ -285,6 +429,125 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
285
429
|
}
|
|
286
430
|
}
|
|
287
431
|
}
|
|
432
|
+
if (tagName === 'meta' && inPublicationHead) {
|
|
433
|
+
const name = attributeValue(token.attrs, 'name', 120);
|
|
434
|
+
const property = attributeValue(token.attrs, 'property', 120);
|
|
435
|
+
const suppliedKeys = [name.value, property.value].filter((value) => Boolean(value));
|
|
436
|
+
const key = suppliedKeys[0] || null;
|
|
437
|
+
const conflictingKeys = suppliedKeys.length > 1 && new Set(suppliedKeys).size > 1;
|
|
438
|
+
if (name.truncated || property.truncated) {
|
|
439
|
+
publicationMetadata.truncated = true;
|
|
440
|
+
// An over-bound or control-bearing key can hide which declaration
|
|
441
|
+
// family it belongs to. Mark both bounded declaration families
|
|
442
|
+
// partial instead of emitting a root state its children contradict.
|
|
443
|
+
publicationMetadata.robots.truncated = true;
|
|
444
|
+
publicationMetadata.twitterCard.truncated = true;
|
|
445
|
+
}
|
|
446
|
+
const publicationKey = suppliedKeys.some((value) => value === 'robots' || value.startsWith('twitter:'));
|
|
447
|
+
const processPublication = publicationKey
|
|
448
|
+
&& publicationMetaElements < MAX_STATIC_PUBLICATION_META_ELEMENTS;
|
|
449
|
+
if (publicationKey && !processPublication) {
|
|
450
|
+
publicationMetadata.truncated = true;
|
|
451
|
+
if (suppliedKeys.includes('robots'))
|
|
452
|
+
publicationMetadata.robots.truncated = true;
|
|
453
|
+
if (suppliedKeys.some((value) => value.startsWith('twitter:')))
|
|
454
|
+
publicationMetadata.twitterCard.truncated = true;
|
|
455
|
+
}
|
|
456
|
+
else if (processPublication) {
|
|
457
|
+
publicationMetaElements += 1;
|
|
458
|
+
}
|
|
459
|
+
if (processPublication && conflictingKeys) {
|
|
460
|
+
if (suppliedKeys.includes('robots')) {
|
|
461
|
+
publicationMetadata.robots.observed = true;
|
|
462
|
+
publicationMetadata.robots.malformed = true;
|
|
463
|
+
}
|
|
464
|
+
if (suppliedKeys.some((value) => value.startsWith('twitter:'))) {
|
|
465
|
+
publicationMetadata.twitterCard.observed = true;
|
|
466
|
+
publicationMetadata.twitterCard.malformed = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else if (processPublication && key === 'robots') {
|
|
470
|
+
publicationMetadata.robots.observed = true;
|
|
471
|
+
const content = attributeValue(token.attrs, 'content', 1_024);
|
|
472
|
+
if (content.value === null || content.truncated) {
|
|
473
|
+
publicationMetadata.robots.malformed = true;
|
|
474
|
+
publicationMetadata.truncated = publicationMetadata.truncated || content.truncated;
|
|
475
|
+
publicationMetadata.robots.truncated = publicationMetadata.robots.truncated || content.truncated;
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
for (const suppliedDirective of content.value.split(/[,\t\n\f\r ]+/u)) {
|
|
479
|
+
if (robotDirectiveTokens >= MAX_STATIC_ROBOTS_DIRECTIVES) {
|
|
480
|
+
publicationMetadata.truncated = true;
|
|
481
|
+
publicationMetadata.robots.truncated = true;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
robotDirectiveTokens += 1;
|
|
485
|
+
const directive = suppliedDirective.trim();
|
|
486
|
+
if (!directive) {
|
|
487
|
+
publicationMetadata.robots.malformed = true;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const directiveName = directive.split(':', 1)[0].trim().replace(/\s+/gu, '_');
|
|
491
|
+
if (ROBOTS_DIRECTIVES.has(directiveName)) {
|
|
492
|
+
publicationMetadata.robots.recognizedDirectiveCount += 1;
|
|
493
|
+
robotDirectives.add(directiveName);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
publicationMetadata.robots.unknownDirectiveCount += 1;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else if (processPublication && key?.startsWith('twitter:')) {
|
|
502
|
+
const category = key === 'twitter:card' ? 'card'
|
|
503
|
+
: key === 'twitter:title' ? 'title'
|
|
504
|
+
: key === 'twitter:description' ? 'description'
|
|
505
|
+
: key === 'twitter:image:alt' ? 'imageAlt'
|
|
506
|
+
: key === 'twitter:image' || key.startsWith('twitter:image:') ? 'image'
|
|
507
|
+
: key === 'twitter:site' || key.startsWith('twitter:site:') ? 'site'
|
|
508
|
+
: key === 'twitter:creator' || key.startsWith('twitter:creator:') ? 'creator'
|
|
509
|
+
: key === 'twitter:player' || key.startsWith('twitter:player:') ? 'player'
|
|
510
|
+
: key === 'twitter:app' || key.startsWith('twitter:app:') ? 'app'
|
|
511
|
+
: null;
|
|
512
|
+
if (category) {
|
|
513
|
+
publicationMetadata.twitterCard.observed = true;
|
|
514
|
+
if (publicationMetadata.twitterCard.declarationCount >= MAX_STATIC_PUBLICATION_DECLARATIONS) {
|
|
515
|
+
publicationMetadata.truncated = true;
|
|
516
|
+
publicationMetadata.twitterCard.truncated = true;
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
publicationMetadata.twitterCard.declarationCount += 1;
|
|
520
|
+
const content = attributeValue(token.attrs, 'content', 1_024);
|
|
521
|
+
if (content.value === null || content.truncated || !content.value) {
|
|
522
|
+
publicationMetadata.twitterCard.malformed = true;
|
|
523
|
+
publicationMetadata.truncated = publicationMetadata.truncated || content.truncated;
|
|
524
|
+
publicationMetadata.twitterCard.truncated = publicationMetadata.twitterCard.truncated || content.truncated;
|
|
525
|
+
}
|
|
526
|
+
else if (category === 'card') {
|
|
527
|
+
twitterCardTypes.add(['summary', 'summary_large_image', 'player', 'app'].includes(content.value)
|
|
528
|
+
? content.value
|
|
529
|
+
: 'other');
|
|
530
|
+
}
|
|
531
|
+
else if (category === 'title')
|
|
532
|
+
publicationMetadata.twitterCard.titlePresent = true;
|
|
533
|
+
else if (category === 'description')
|
|
534
|
+
publicationMetadata.twitterCard.descriptionPresent = true;
|
|
535
|
+
else if (category === 'image')
|
|
536
|
+
publicationMetadata.twitterCard.imagePresent = true;
|
|
537
|
+
else if (category === 'imageAlt')
|
|
538
|
+
publicationMetadata.twitterCard.imageAltPresent = true;
|
|
539
|
+
else if (category === 'site')
|
|
540
|
+
publicationMetadata.twitterCard.sitePresent = true;
|
|
541
|
+
else if (category === 'creator')
|
|
542
|
+
publicationMetadata.twitterCard.creatorPresent = true;
|
|
543
|
+
else if (category === 'player')
|
|
544
|
+
publicationMetadata.twitterCard.playerPresent = true;
|
|
545
|
+
else if (category === 'app')
|
|
546
|
+
publicationMetadata.twitterCard.appPresent = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
288
551
|
if (markup.length < MAX_TECHNOLOGY_TAGS) {
|
|
289
552
|
const serialized = serializedStartTag(tagName, token.attrs);
|
|
290
553
|
if (serialized.limitReached)
|
|
@@ -349,18 +612,31 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
349
612
|
const tagName = token.tagName.toLowerCase();
|
|
350
613
|
if (tagName === 'script')
|
|
351
614
|
activeInlineScript = null;
|
|
352
|
-
if (tagName === 'head')
|
|
615
|
+
if (tagName === 'head') {
|
|
353
616
|
insideExplicitHead = false;
|
|
617
|
+
headScopeClosed = true;
|
|
618
|
+
}
|
|
619
|
+
if (!insideExplicitHead && IMPLICIT_HEAD_TEXT_TAGS.has(tagName) && implicitHeadTextDepth > 0) {
|
|
620
|
+
implicitHeadTextDepth -= 1;
|
|
621
|
+
}
|
|
354
622
|
if (!VOID_TAGS.has(tagName))
|
|
355
623
|
appendStructureToken(`/${tagName}`);
|
|
356
624
|
if (NON_VISIBLE_TEXT_TAGS.has(tagName) && nonVisibleTextDepth > 0)
|
|
357
625
|
nonVisibleTextDepth -= 1;
|
|
358
626
|
},
|
|
359
627
|
onCharacter(token) {
|
|
628
|
+
if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0 && token.chars.trim()) {
|
|
629
|
+
bodyContentStarted = true;
|
|
630
|
+
headScopeClosed = true;
|
|
631
|
+
}
|
|
360
632
|
appendInlineScript(token.chars);
|
|
361
633
|
appendVisibleText(token.chars);
|
|
362
634
|
},
|
|
363
635
|
onNullCharacter(token) {
|
|
636
|
+
if (!insideExplicitHead && !headScopeClosed && implicitHeadTextDepth === 0) {
|
|
637
|
+
bodyContentStarted = true;
|
|
638
|
+
headScopeClosed = true;
|
|
639
|
+
}
|
|
364
640
|
appendInlineScript(token.chars);
|
|
365
641
|
appendVisibleText(token.chars);
|
|
366
642
|
},
|
|
@@ -374,14 +650,35 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
374
650
|
};
|
|
375
651
|
tokenizer = new Tokenizer({ sourceCodeLocationInfo: false }, handler);
|
|
376
652
|
tokenizer.write(html, true);
|
|
653
|
+
publicationMetadata.truncated = publicationMetadata.truncated || inputLimitReached || publicationTagLimitReached;
|
|
654
|
+
publicationMetadata.documentTruncated = inputLimitReached || publicationTagLimitReached;
|
|
655
|
+
if (publicationMetadata.documentTruncated) {
|
|
656
|
+
publicationMetadata.robots.truncated = true;
|
|
657
|
+
publicationMetadata.twitterCard.truncated = true;
|
|
658
|
+
publicationMetadata.headings.truncated = true;
|
|
659
|
+
publicationMetadata.images.totalComplete = false;
|
|
660
|
+
publicationMetadata.images.classificationComplete = false;
|
|
661
|
+
publicationMetadata.images.truncated = true;
|
|
662
|
+
publicationMetadata.renderBlockingCandidates.truncated = true;
|
|
663
|
+
}
|
|
664
|
+
publicationMetadata.robots.directives = [...robotDirectives].sort();
|
|
665
|
+
publicationMetadata.robots.conflicting = ((robotDirectives.has('index') || robotDirectives.has('all'))
|
|
666
|
+
&& (robotDirectives.has('noindex') || robotDirectives.has('none'))) || ((robotDirectives.has('follow') || robotDirectives.has('all'))
|
|
667
|
+
&& (robotDirectives.has('nofollow') || robotDirectives.has('none')));
|
|
668
|
+
publicationMetadata.twitterCard.cardTypes = [...twitterCardTypes].sort().slice(0, 8);
|
|
669
|
+
if (twitterCardTypes.size > 8)
|
|
670
|
+
publicationMetadata.truncated = true;
|
|
671
|
+
publicationMetadata.renderBlockingCandidates.total = publicationMetadata.renderBlockingCandidates.script
|
|
672
|
+
+ publicationMetadata.renderBlockingCandidates.stylesheet;
|
|
377
673
|
return {
|
|
378
674
|
markup: markup.join('\n'),
|
|
379
|
-
visibleText: visibleTextParts.join(''),
|
|
675
|
+
visibleText: visibleTextParts.join('').replace(CONTROL_CHARACTER_RE_GLOBAL, ' ').replace(/\s+/gu, ' '),
|
|
380
676
|
structureTokens,
|
|
381
677
|
scripts,
|
|
382
678
|
cspMetaPolicies,
|
|
383
679
|
cspMetaLimitReached,
|
|
384
680
|
forms,
|
|
681
|
+
publicationMetadata,
|
|
385
682
|
inputLimitReached,
|
|
386
683
|
tagLimitReached,
|
|
387
684
|
structureLimitReached,
|
|
@@ -392,4 +689,4 @@ function analyzeStaticHtml(value, options = {}) {
|
|
|
392
689
|
inlineCharactersExamined,
|
|
393
690
|
};
|
|
394
691
|
}
|
|
395
|
-
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_HTML_TAGS, MAX_STATIC_STRUCTURE_TOKENS, MAX_STATIC_VISIBLE_TEXT_CHARS, MAX_TAG_LENGTH, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, };
|
|
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, MAX_TECHNOLOGY_TAGS, analyzeStaticHtml, };
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// and fixed limitations. Raw JSON-LD and arbitrary publisher properties are
|
|
5
5
|
// discarded immediately after analysis.
|
|
6
6
|
import { domainToASCII } from 'node:url';
|
|
7
|
-
import { createObservation } from "
|
|
7
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
8
|
+
import { isUriShapedLabel } from "./portable-generator.mjs";
|
|
8
9
|
import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
9
10
|
const STRUCTURED_DATA_IDENTITY_VERSION = 1;
|
|
10
11
|
const MAX_STRUCTURED_DATA_SCRIPTS = 8;
|
|
@@ -19,8 +20,8 @@ const MAX_STRUCTURED_DATA_TYPES = 8;
|
|
|
19
20
|
const MAX_STRUCTURED_DATA_NAME_LENGTH = 160;
|
|
20
21
|
const MAX_STRUCTURED_DATA_URL_LENGTH = 2_048;
|
|
21
22
|
const MAX_STRUCTURED_DATA_SAME_AS_HOSTS = 12;
|
|
22
|
-
const CONTROL_AND_DIRECTIONAL_RE = /[\u0000-\u001f\u007f
|
|
23
|
-
const CONTROL_AND_DIRECTIONAL_GLOBAL_RE = /[\u0000-\u001f\u007f
|
|
23
|
+
const CONTROL_AND_DIRECTIONAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
|
|
24
|
+
const CONTROL_AND_DIRECTIONAL_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
24
25
|
const HOSTNAME_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/iu;
|
|
25
26
|
const CURATED_TYPES = new Map([
|
|
26
27
|
['brand', 'Brand'],
|
|
@@ -44,17 +45,21 @@ function record(value) {
|
|
|
44
45
|
: null;
|
|
45
46
|
}
|
|
46
47
|
function boundedName(value) {
|
|
47
|
-
if (typeof value !== 'string'
|
|
48
|
-
return null;
|
|
48
|
+
if (typeof value !== 'string')
|
|
49
|
+
return { value: null, truncated: false };
|
|
50
|
+
if (value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
|
|
51
|
+
return { value: null, truncated: true };
|
|
52
|
+
if (isUriShapedLabel(value))
|
|
53
|
+
return { value: null, truncated: false };
|
|
49
54
|
const normalized = value
|
|
50
55
|
.replace(CONTROL_AND_DIRECTIONAL_GLOBAL_RE, ' ')
|
|
51
56
|
.replace(/\s+/gu, ' ')
|
|
52
57
|
.trim();
|
|
53
58
|
if (!normalized)
|
|
54
|
-
return null;
|
|
59
|
+
return { value: null, truncated: false };
|
|
55
60
|
return normalized.length <= MAX_STRUCTURED_DATA_NAME_LENGTH
|
|
56
|
-
? normalized
|
|
57
|
-
: `${normalized.slice(0, MAX_STRUCTURED_DATA_NAME_LENGTH)}
|
|
61
|
+
? { value: normalized, truncated: false }
|
|
62
|
+
: { value: `${normalized.slice(0, MAX_STRUCTURED_DATA_NAME_LENGTH - 1)}…`, truncated: true };
|
|
58
63
|
}
|
|
59
64
|
function safeBaseUrl(value) {
|
|
60
65
|
if (typeof value !== 'string' || value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
|
|
@@ -116,7 +121,7 @@ function curatedTypes(value) {
|
|
|
116
121
|
if (normalized)
|
|
117
122
|
types.add(normalized);
|
|
118
123
|
}
|
|
119
|
-
return [...types].sort();
|
|
124
|
+
return { value: [...types].sort(), truncated: values.length > MAX_STRUCTURED_DATA_TYPES };
|
|
120
125
|
}
|
|
121
126
|
function sameAsHosts(value) {
|
|
122
127
|
const values = Array.isArray(value) ? value : [value];
|
|
@@ -126,7 +131,7 @@ function sameAsHosts(value) {
|
|
|
126
131
|
if (host)
|
|
127
132
|
hosts.add(host);
|
|
128
133
|
}
|
|
129
|
-
return [...hosts].sort();
|
|
134
|
+
return { value: [...hosts].sort(), truncated: values.length > MAX_STRUCTURED_DATA_SAME_AS_HOSTS };
|
|
130
135
|
}
|
|
131
136
|
function jsonStructureWithinBounds(value) {
|
|
132
137
|
let depth = 0;
|
|
@@ -161,14 +166,21 @@ function jsonStructureWithinBounds(value) {
|
|
|
161
166
|
}
|
|
162
167
|
function entityFromObject(value, baseUrl) {
|
|
163
168
|
const types = curatedTypes(value['@type']);
|
|
164
|
-
if (!types.length)
|
|
165
|
-
return null;
|
|
166
169
|
const name = boundedName(value.name);
|
|
167
170
|
const declaredOrigin = normalizedOrigin(value.url, baseUrl);
|
|
168
171
|
const hosts = sameAsHosts(value.sameAs);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
const projection = {
|
|
173
|
+
nameTruncated: name.truncated,
|
|
174
|
+
typesTruncated: types.truncated,
|
|
175
|
+
sameAsTruncated: hosts.truncated,
|
|
176
|
+
};
|
|
177
|
+
if (!types.value.length || (!name.value && !declaredOrigin && !hosts.value.length)) {
|
|
178
|
+
return { entity: null, ...projection };
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
entity: { types: types.value, name: name.value, declaredOrigin, sameAsHosts: hosts.value },
|
|
182
|
+
...projection,
|
|
183
|
+
};
|
|
172
184
|
}
|
|
173
185
|
function analyzeStructuredDataIdentity(input = {}) {
|
|
174
186
|
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
|
|
@@ -183,6 +195,9 @@ function analyzeStructuredDataIdentity(input = {}) {
|
|
|
183
195
|
let objectsExamined = 0;
|
|
184
196
|
let arrayItemsExamined = 0;
|
|
185
197
|
let discardedProperties = 0;
|
|
198
|
+
let truncatedNames = 0;
|
|
199
|
+
let truncatedTypeLists = 0;
|
|
200
|
+
let truncatedSameAsLists = 0;
|
|
186
201
|
let entityLimitReached = false;
|
|
187
202
|
let limitReached = scripts.length > MAX_STRUCTURED_DATA_SCRIPTS;
|
|
188
203
|
for (const script of selectedScripts) {
|
|
@@ -242,12 +257,20 @@ function analyzeStructuredDataIdentity(input = {}) {
|
|
|
242
257
|
continue;
|
|
243
258
|
}
|
|
244
259
|
objectsExamined += 1;
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
|
|
260
|
+
const projection = entityFromObject(object, baseUrl);
|
|
261
|
+
if (projection.nameTruncated)
|
|
262
|
+
truncatedNames += 1;
|
|
263
|
+
if (projection.typesTruncated)
|
|
264
|
+
truncatedTypeLists += 1;
|
|
265
|
+
if (projection.sameAsTruncated)
|
|
266
|
+
truncatedSameAsLists += 1;
|
|
267
|
+
if (projection.nameTruncated || projection.typesTruncated || projection.sameAsTruncated)
|
|
268
|
+
limitReached = true;
|
|
269
|
+
if (projection.entity) {
|
|
270
|
+
const key = JSON.stringify(projection.entity);
|
|
248
271
|
if (!entities.has(key)) {
|
|
249
272
|
if (entities.size < MAX_STRUCTURED_DATA_ENTITIES)
|
|
250
|
-
entities.set(key, entity);
|
|
273
|
+
entities.set(key, projection.entity);
|
|
251
274
|
else {
|
|
252
275
|
entityLimitReached = true;
|
|
253
276
|
limitReached = true;
|
|
@@ -294,6 +317,12 @@ function analyzeStructuredDataIdentity(input = {}) {
|
|
|
294
317
|
if (entityLimitReached) {
|
|
295
318
|
limitations.push(`Only the first ${MAX_STRUCTURED_DATA_ENTITIES} structured identity entities were retained.`);
|
|
296
319
|
}
|
|
320
|
+
if (truncatedNames > 0)
|
|
321
|
+
limitations.push(`${truncatedNames} structured identity label${truncatedNames === 1 ? ' was' : 's were'} shortened to the ${MAX_STRUCTURED_DATA_NAME_LENGTH}-character retention limit.`);
|
|
322
|
+
if (truncatedTypeLists > 0)
|
|
323
|
+
limitations.push(`${truncatedTypeLists} structured identity type list${truncatedTypeLists === 1 ? ' exceeded' : 's exceeded'} the ${MAX_STRUCTURED_DATA_TYPES}-item inspection limit.`);
|
|
324
|
+
if (truncatedSameAsLists > 0)
|
|
325
|
+
limitations.push(`${truncatedSameAsLists} structured identity sameAs list${truncatedSameAsLists === 1 ? ' exceeded' : 's exceeded'} the ${MAX_STRUCTURED_DATA_SAME_AS_HOSTS}-item inspection limit.`);
|
|
297
326
|
return {
|
|
298
327
|
structuredDataVersion: STRUCTURED_DATA_IDENTITY_VERSION,
|
|
299
328
|
...createObservation({
|
|
@@ -314,6 +343,9 @@ function analyzeStructuredDataIdentity(input = {}) {
|
|
|
314
343
|
objectsExamined,
|
|
315
344
|
arrayItemsExamined,
|
|
316
345
|
discardedProperties,
|
|
346
|
+
truncatedNames,
|
|
347
|
+
truncatedTypeLists,
|
|
348
|
+
truncatedSameAsLists,
|
|
317
349
|
entities: entities.size,
|
|
318
350
|
},
|
|
319
351
|
}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCuratedConnectorResult, normalizeCuratedConnectorTarget, } from "./threat-intelligence-runtime.mjs";
|
|
2
2
|
import { assertCuratedConnectorDefinition } from "./threat-intelligence-definition-registry.mjs";
|
|
3
|
+
import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
3
4
|
const MAX_CONNECTOR_KEY_LENGTH = 80;
|
|
4
5
|
const MAX_CONNECTOR_FIXTURE_BYTES = 512 * 1024;
|
|
5
6
|
function isRecord(value) {
|
|
@@ -24,10 +25,7 @@ function fixtureId(value) {
|
|
|
24
25
|
return value;
|
|
25
26
|
}
|
|
26
27
|
function fixtureTimestamp(value) {
|
|
27
|
-
|
|
28
|
-
return null;
|
|
29
|
-
const timestamp = Date.parse(value);
|
|
30
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
28
|
+
return normalizeLegacyIsoTimestamp(value);
|
|
31
29
|
}
|
|
32
30
|
export function runCuratedConnectorFixture(connector, fixture, normalizeFixture) {
|
|
33
31
|
assertCuratedConnectorDefinition(connector);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerCuratedConnector, registerThreatIntelligenceProvider, isCuratedConnectorDefinition, isThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
|
|
2
2
|
import { CURATED_CONNECTOR_CONTRACT_VERSION, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_RESPONSE_BYTES, MAX_TIMEOUT_MS, THREAT_INTELLIGENCE_CONTRACT_VERSION, } from "./threat-intelligence-runtime.mjs";
|
|
3
|
+
import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
3
4
|
const MAX_PROVIDER_ID_LENGTH = 40;
|
|
4
5
|
const MAX_PROVIDER_LABEL_LENGTH = 100;
|
|
5
6
|
const MAX_URL_LENGTH = 2_048;
|
|
@@ -137,13 +138,7 @@ function strictBoundedString(value, maximum) {
|
|
|
137
138
|
return normalized && normalized.length <= maximum ? normalized : null;
|
|
138
139
|
}
|
|
139
140
|
function isoTimestamp(value) {
|
|
140
|
-
|
|
141
|
-
value.length > 64 ||
|
|
142
|
-
/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
const timestamp = Date.parse(value);
|
|
146
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
141
|
+
return normalizeLegacyIsoTimestamp(value);
|
|
147
142
|
}
|
|
148
143
|
function httpsUrl(value) {
|
|
149
144
|
const raw = strictBoundedString(value, MAX_URL_LENGTH);
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
// network requests and contains no provider credentials. Adapters added later
|
|
4
4
|
// must declare exact input exposure, reviewed terms, and bounded request and
|
|
5
5
|
// output budgets before producing separately attributed normalized evidence.
|
|
6
|
-
import { createObservation } from "
|
|
7
|
-
import { THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_RESULT_STATES, } from "./threat-intelligence-types.mjs";
|
|
6
|
+
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
7
|
+
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
|
|
8
8
|
import { assertCuratedConnectorDefinition, assertThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
|
|
9
9
|
import { boundedHttpsUrl, boundedString, isRecord, isoTimestamp, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
|
|
10
10
|
import { CURATED_CONNECTOR_ENTITY_TYPES, MAX_THREAT_INTELLIGENCE_URL_LENGTH, normalizeCuratedConnectorTarget, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-targets.mjs";
|
|
11
|
-
const THREAT_INTELLIGENCE_CONTRACT_VERSION = 1;
|
|
12
|
-
const THREAT_INTELLIGENCE_SCHEMA = 'whoisleuth.threat-intelligence-result';
|
|
13
11
|
const CURATED_CONNECTOR_CONTRACT_VERSION = 1;
|
|
14
12
|
const CURATED_CONNECTOR_RESULT_SCHEMA = 'whoisleuth.curated-connector-result';
|
|
15
13
|
const RESULT_STATES = new Set(THREAT_INTELLIGENCE_RESULT_STATES);
|
|
@@ -650,4 +648,4 @@ function createCuratedConnectorResult(connector, target, input = {}, observedAt
|
|
|
650
648
|
}),
|
|
651
649
|
};
|
|
652
650
|
}
|
|
653
|
-
export { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_SCHEMA, CURATED_CONNECTOR_CONTRACT_VERSION, CURATED_CONNECTOR_RESULT_SCHEMA, MAX_FINDINGS, MAX_INPUT_FINDINGS, MAX_TIMEOUT_MS, MAX_RESPONSE_BYTES, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_CONNECTOR_INPUT_ENTITIES, MAX_CONNECTOR_INPUT_RELATIONSHIPS, normalizeThreatIntelligenceTarget, normalizeCuratedConnectorTarget, createThreatIntelligenceResult, createCuratedConnectorResult, };
|
|
651
|
+
export { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_SCHEMA, CURATED_CONNECTOR_CONTRACT_VERSION, CURATED_CONNECTOR_RESULT_SCHEMA, MAX_FINDINGS, MAX_INPUT_FINDINGS, MAX_TIMEOUT_MS, MAX_RESPONSE_BYTES, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_CONNECTOR_INPUT_ENTITIES, MAX_CONNECTOR_INPUT_RELATIONSHIPS, normalizeThreatIntelligenceTarget, normalizeCuratedConnectorTarget, createThreatIntelligenceResult, createCuratedConnectorResult, };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Provider-neutral types shared by threat-intelligence and curated connector
|
|
2
2
|
// manifests, fixture harnesses, normalizers, and result builders.
|
|
3
|
+
export const THREAT_INTELLIGENCE_SCHEMA = 'whoisleuth.threat-intelligence-result';
|
|
4
|
+
export const THREAT_INTELLIGENCE_CONTRACT_VERSION = 1;
|
|
5
|
+
export const THREAT_INTELLIGENCE_ENVELOPE_VERSION = 1;
|
|
3
6
|
export const THREAT_INTELLIGENCE_CATEGORIES = Object.freeze([
|
|
4
7
|
'phishing',
|
|
5
8
|
'malware',
|
|
@@ -6,6 +6,7 @@ import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
|
|
|
6
6
|
import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
7
7
|
import { providerPolicyAdmission } from "./provider-policy-admission.mjs";
|
|
8
8
|
import { createThreatIntelligenceResult, defineThreatIntelligenceProvider, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-contract.mjs";
|
|
9
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
9
10
|
const THREATFOX_SEARCH_ENDPOINT = 'https://threatfox-api.abuse.ch/api/v1/';
|
|
10
11
|
const THREATFOX_MAX_RESULTS = 20;
|
|
11
12
|
const THREATFOX_MAX_INPUT_RESULTS = 100;
|
|
@@ -75,8 +76,7 @@ function isoTimestamp(value) {
|
|
|
75
76
|
const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?: UTC)?$/u.test(raw)
|
|
76
77
|
? `${raw.slice(0, 10)}T${raw.slice(11, 19)}Z`
|
|
77
78
|
: raw;
|
|
78
|
-
|
|
79
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
79
|
+
return normalizeExplicitIsoTimestamp(normalized);
|
|
80
80
|
}
|
|
81
81
|
function normalizedTags(value) {
|
|
82
82
|
if (!Array.isArray(value))
|
|
@@ -112,7 +112,11 @@ function normalizeIocFinding(value, targetDomain) {
|
|
|
112
112
|
if (!id || !/^\d{1,32}$/u.test(id) || ioc !== targetDomain || iocType !== 'domain' || !threatType)
|
|
113
113
|
return null;
|
|
114
114
|
const firstObservedAt = isoTimestamp(value.first_seen);
|
|
115
|
-
const
|
|
115
|
+
const parsedLastObservedAt = isoTimestamp(value.last_seen);
|
|
116
|
+
if ((value.first_seen !== undefined && value.first_seen !== null && !firstObservedAt)
|
|
117
|
+
|| (value.last_seen !== undefined && value.last_seen !== null && !parsedLastObservedAt))
|
|
118
|
+
return null;
|
|
119
|
+
const lastObservedAt = parsedLastObservedAt || firstObservedAt;
|
|
116
120
|
const tags = normalizedTags(value.tags);
|
|
117
121
|
tags.push(`role:${threatType}`);
|
|
118
122
|
if (malware)
|