@slicedearth/whoisleuth-cli 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
- package/NOTICE +76 -0
- package/README.md +84 -0
- package/TRADEMARKS.md +41 -0
- package/bin/whoisleuth.mjs +35 -0
- package/cli/archive-inspect.mjs +198 -0
- package/cli/arguments.mjs +1022 -0
- package/cli/artifact-verify.mjs +210 -0
- package/cli/bulk-checkpoint.mjs +218 -0
- package/cli/bulk-command-runner.mjs +109 -0
- package/cli/bulk-output.mjs +112 -0
- package/cli/bulk.mjs +133 -0
- package/cli/compare.mjs +238 -0
- package/cli/completion.mjs +175 -0
- package/cli/csv.mjs +1 -0
- package/cli/discover.mjs +50 -0
- package/cli/discovery-command-runner.mjs +36 -0
- package/cli/discovery-observation-snapshot.mjs +315 -0
- package/cli/discovery-scan-command-runner.mjs +141 -0
- package/cli/discovery-scan.mjs +325 -0
- package/cli/discovery-snapshot.mjs +133 -0
- package/cli/discovery-workflow.mjs +73 -0
- package/cli/doctor.mjs +137 -0
- package/cli/errors.mjs +21 -0
- package/cli/evidence-command-arguments.mjs +145 -0
- package/cli/evidence-command-runner.mjs +106 -0
- package/cli/evidence-signing.mjs +215 -0
- package/cli/exit-codes.mjs +9 -0
- package/cli/export-evidence.mjs +28 -0
- package/cli/formatters/evidence-report.mjs +3 -0
- package/cli/formatters/html.mjs +69 -0
- package/cli/formatters/json.mjs +135 -0
- package/cli/formatters/markdown.mjs +3 -0
- package/cli/formatters/terminal.mjs +658 -0
- package/cli/http.mjs +27 -0
- package/cli/lookup-command-runner.mjs +87 -0
- package/cli/lookup-diff.mjs +171 -0
- package/cli/mail-review.mjs +204 -0
- package/cli/manual.mjs +34 -0
- package/cli/network-command-runner.mjs +88 -0
- package/cli/output-file.mjs +97 -0
- package/cli/page-compare.mjs +123 -0
- package/cli/posture.mjs +20 -0
- package/cli/progress-events.mjs +53 -0
- package/cli/progress.mjs +56 -0
- package/cli/registry-access.mjs +3 -0
- package/cli/registry-support.mjs +120 -0
- package/cli/risk-calibration.mjs +294 -0
- package/cli/runner-types.mjs +1 -0
- package/cli/runner.mjs +726 -0
- package/cli/saved-lookup.mjs +87 -0
- package/cli/source-reliability.mjs +727 -0
- package/cli/strict-exit.mjs +55 -0
- package/cli/terminal-presentation.mjs +147 -0
- package/docs/cli.md +827 -0
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
- package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
- package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
- package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
- package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
- package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
- package/frontend/src/lib/analysis/bulk-sort.js +49 -0
- package/frontend/src/lib/analysis/campaign-model.js +265 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
- package/frontend/src/lib/analysis/case-migration-model.js +248 -0
- package/frontend/src/lib/analysis/case-model.js +5 -0
- package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
- package/frontend/src/lib/analysis/case-record-core.js +232 -0
- package/frontend/src/lib/analysis/case-record-model.js +7 -0
- package/frontend/src/lib/analysis/case-record-operations.js +224 -0
- package/frontend/src/lib/analysis/case-response-model.js +640 -0
- package/frontend/src/lib/analysis/case-response-packet.js +573 -0
- package/frontend/src/lib/analysis/case-storage-model.js +93 -0
- package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
- package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
- package/frontend/src/lib/analysis/http-summary.js +165 -0
- package/frontend/src/lib/analysis/investigation-guide.js +500 -0
- package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
- package/frontend/src/lib/analysis/page-baseline.js +272 -0
- package/frontend/src/lib/analysis/page-similarity.js +179 -0
- package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
- package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
- package/frontend/src/lib/analysis/scoring.js +135 -0
- package/frontend/src/lib/analysis/shortlist-model.js +209 -0
- package/frontend/src/lib/analysis/utils.js +265 -0
- package/frontend/src/lib/analysis/watchlist-history.js +643 -0
- package/frontend/src/lib/analysis/watchlist-store.js +127 -0
- package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
- package/frontend/src/lib/analysis/workspace-archive.js +472 -0
- package/lib/abort.mjs +26 -0
- package/lib/abusech-auth.mjs +19 -0
- package/lib/availability.mjs +716 -0
- package/lib/bounded-contract-normalizers.mjs +61 -0
- package/lib/browser-library-profile.mjs +310 -0
- package/lib/bulk-comparison-evidence.mjs +83 -0
- package/lib/classify.mjs +93 -0
- package/lib/client-behavior-profile.mjs +83 -0
- package/lib/credential-surface-profile.mjs +66 -0
- package/lib/csv.mjs +23 -0
- package/lib/ct-query.mjs +27 -0
- package/lib/ct-search.mjs +393 -0
- package/lib/distributed-operation-budget.mjs +461 -0
- package/lib/dns-delegation-health.mjs +344 -0
- package/lib/dns-intelligence.mjs +739 -0
- package/lib/dns-mx.mjs +24 -0
- package/lib/dns-resolver-selection.mjs +33 -0
- package/lib/domain-posture-analysis.mjs +254 -0
- package/lib/domain-posture-parsers.mjs +358 -0
- package/lib/domain-posture.mjs +745 -0
- package/lib/error-detail.mjs +10 -0
- package/lib/evidence-export.mjs +431 -0
- package/lib/evidence-report-markdown.mjs +71 -0
- package/lib/evidence-report.mjs +391 -0
- package/lib/external-intelligence-risk.mjs +108 -0
- package/lib/favicon.mjs +146 -0
- package/lib/feature-policy.mjs +70 -0
- package/lib/generated/retire-browser-catalog.mjs +8724 -0
- package/lib/generated/unicode-confusables-17.mjs +93 -0
- package/lib/hostname.mjs +38 -0
- package/lib/html-signals.mjs +680 -0
- package/lib/http-intelligence.mjs +232 -0
- package/lib/idn-confusable-policy.mjs +86 -0
- package/lib/idn-confusables.mjs +465 -0
- package/lib/lookup-cache.mjs +130 -0
- package/lib/lookup-progress.mjs +291 -0
- package/lib/lookup-source-progress.mjs +109 -0
- package/lib/lookup-threat-provider-inventory.mjs +13 -0
- package/lib/lookup.mjs +592 -0
- package/lib/observation.mjs +114 -0
- package/lib/observed-network-context.mjs +355 -0
- package/lib/operation-budget.mjs +489 -0
- package/lib/page-fingerprints.mjs +481 -0
- package/lib/page-role-profile.mjs +117 -0
- package/lib/perceptual-hash.mjs +451 -0
- package/lib/rdap-attempts.mjs +34 -0
- package/lib/rdap-bootstrap.mjs +173 -0
- package/lib/rdap-capabilities.mjs +292 -0
- package/lib/rdap-client.mjs +125 -0
- package/lib/rdap-normalization.mjs +691 -0
- package/lib/rdap-registrar.mjs +196 -0
- package/lib/rdap-transport.mjs +39 -0
- package/lib/rdap-types.mjs +1 -0
- package/lib/rdap-validation.mjs +94 -0
- package/lib/rdap.mjs +16 -0
- package/lib/registry-access.mjs +32 -0
- package/lib/registry-capabilities.mjs +117 -0
- package/lib/registry-capability-catalogue.mjs +1206 -0
- package/lib/registry-comparison.mjs +318 -0
- package/lib/registry-dates.mjs +190 -0
- package/lib/registry-insights.mjs +325 -0
- package/lib/response-policy.mjs +256 -0
- package/lib/risk-scoring.mjs +164 -0
- package/lib/safe-fetch.mjs +446 -0
- package/lib/security-txt.mjs +359 -0
- package/lib/service-binding-dns.mjs +608 -0
- package/lib/sslbl-certificates.generated.mjs +143 -0
- package/lib/sslbl-intelligence.mjs +241 -0
- package/lib/static-html-analysis.mjs +340 -0
- package/lib/structured-data-identity.mjs +323 -0
- package/lib/threat-intelligence-contract.mjs +5 -0
- package/lib/threat-intelligence-definition-registry.mjs +34 -0
- package/lib/threat-intelligence-fixtures.mjs +82 -0
- package/lib/threat-intelligence-manifests.mjs +512 -0
- package/lib/threat-intelligence-runtime.mjs +662 -0
- package/lib/threat-intelligence-targets.mjs +168 -0
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +307 -0
- package/lib/tls-intelligence.mjs +814 -0
- package/lib/typosquat-generator.mjs +884 -0
- package/lib/urlhaus-intelligence.mjs +361 -0
- package/lib/urlscan-intelligence.mjs +349 -0
- package/lib/website-security-posture.mjs +325 -0
- package/lib/website-technology.mjs +437 -0
- package/lib/whois-authority.mjs +98 -0
- package/lib/whois-chain.mjs +261 -0
- package/lib/whois-contacts.mjs +163 -0
- package/lib/whois-contracts.mjs +3 -0
- package/lib/whois-normalization.mjs +27 -0
- package/lib/whois-transport.mjs +100 -0
- package/lib/whois-values.mjs +92 -0
- package/lib/whois.mjs +1187 -0
- package/package.json +55 -0
- package/third-party-notices.txt +5571 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// Privacy-minimized identity claims derived from JSON-LD script elements in
|
|
2
|
+
// the already-captured deep-Lookup homepage response. The bounded projection
|
|
3
|
+
// retains only curated schema types, labels, origins, hostnames, source health,
|
|
4
|
+
// and fixed limitations. Raw JSON-LD and arbitrary publisher properties are
|
|
5
|
+
// discarded immediately after analysis.
|
|
6
|
+
import { domainToASCII } from 'node:url';
|
|
7
|
+
import { createObservation } from "./observation.mjs";
|
|
8
|
+
import { analyzeStaticHtml, } from "./static-html-analysis.mjs";
|
|
9
|
+
const STRUCTURED_DATA_IDENTITY_VERSION = 1;
|
|
10
|
+
const MAX_STRUCTURED_DATA_SCRIPTS = 8;
|
|
11
|
+
const MAX_STRUCTURED_DATA_SCRIPT_CHARS = 16_384;
|
|
12
|
+
const MAX_STRUCTURED_DATA_TOTAL_CHARS = 32_768;
|
|
13
|
+
const MAX_STRUCTURED_DATA_DEPTH = 16;
|
|
14
|
+
const MAX_STRUCTURED_DATA_OBJECTS = 256;
|
|
15
|
+
const MAX_STRUCTURED_DATA_ARRAY_ITEMS = 256;
|
|
16
|
+
const MAX_STRUCTURED_DATA_PROPERTIES = 64;
|
|
17
|
+
const MAX_STRUCTURED_DATA_ENTITIES = 16;
|
|
18
|
+
const MAX_STRUCTURED_DATA_TYPES = 8;
|
|
19
|
+
const MAX_STRUCTURED_DATA_NAME_LENGTH = 160;
|
|
20
|
+
const MAX_STRUCTURED_DATA_URL_LENGTH = 2_048;
|
|
21
|
+
const MAX_STRUCTURED_DATA_SAME_AS_HOSTS = 12;
|
|
22
|
+
const CONTROL_AND_DIRECTIONAL_RE = /[\u0000-\u001f\u007f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/u;
|
|
23
|
+
const CONTROL_AND_DIRECTIONAL_GLOBAL_RE = /[\u0000-\u001f\u007f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/gu;
|
|
24
|
+
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
|
+
const CURATED_TYPES = new Map([
|
|
26
|
+
['brand', 'Brand'],
|
|
27
|
+
['corporation', 'Corporation'],
|
|
28
|
+
['educationalorganization', 'EducationalOrganization'],
|
|
29
|
+
['governmentorganization', 'GovernmentOrganization'],
|
|
30
|
+
['localbusiness', 'LocalBusiness'],
|
|
31
|
+
['newsmediaorganization', 'NewsMediaOrganization'],
|
|
32
|
+
['ngo', 'NGO'],
|
|
33
|
+
['onlinebusiness', 'OnlineBusiness'],
|
|
34
|
+
['organization', 'Organization'],
|
|
35
|
+
['performinggroup', 'PerformingGroup'],
|
|
36
|
+
['project', 'Project'],
|
|
37
|
+
['sportsorganization', 'SportsOrganization'],
|
|
38
|
+
['webpage', 'WebPage'],
|
|
39
|
+
['website', 'WebSite'],
|
|
40
|
+
]);
|
|
41
|
+
function record(value) {
|
|
42
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
43
|
+
? value
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
function boundedName(value) {
|
|
47
|
+
if (typeof value !== 'string' || value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
|
|
48
|
+
return null;
|
|
49
|
+
const normalized = value
|
|
50
|
+
.replace(CONTROL_AND_DIRECTIONAL_GLOBAL_RE, ' ')
|
|
51
|
+
.replace(/\s+/gu, ' ')
|
|
52
|
+
.trim();
|
|
53
|
+
if (!normalized)
|
|
54
|
+
return null;
|
|
55
|
+
return normalized.length <= MAX_STRUCTURED_DATA_NAME_LENGTH
|
|
56
|
+
? normalized
|
|
57
|
+
: `${normalized.slice(0, MAX_STRUCTURED_DATA_NAME_LENGTH)}…`;
|
|
58
|
+
}
|
|
59
|
+
function safeBaseUrl(value) {
|
|
60
|
+
if (typeof value !== 'string' || value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = new URL(value);
|
|
64
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return parsed.toString();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function normalizedOrigin(value, baseUrl) {
|
|
74
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_STRUCTURED_DATA_URL_LENGTH)
|
|
75
|
+
return null;
|
|
76
|
+
if (CONTROL_AND_DIRECTIONAL_RE.test(value))
|
|
77
|
+
return null;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = baseUrl ? new URL(value, baseUrl) : new URL(value);
|
|
80
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname)
|
|
81
|
+
return null;
|
|
82
|
+
return parsed.origin;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function normalizedSameAsHost(value) {
|
|
89
|
+
if (typeof value !== 'string'
|
|
90
|
+
|| value.length === 0
|
|
91
|
+
|| value.length > MAX_STRUCTURED_DATA_URL_LENGTH
|
|
92
|
+
|| CONTROL_AND_DIRECTIONAL_RE.test(value))
|
|
93
|
+
return null;
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(value);
|
|
96
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname)
|
|
97
|
+
return null;
|
|
98
|
+
const ascii = domainToASCII(parsed.hostname.toLowerCase().replace(/\.$/u, ''));
|
|
99
|
+
return ascii && HOSTNAME_RE.test(ascii) ? ascii : null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function curatedType(value) {
|
|
106
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 160)
|
|
107
|
+
return null;
|
|
108
|
+
const suffix = value.trim().split(/[\/#]/u).pop()?.replace(/[^a-z]/giu, '').toLowerCase() || '';
|
|
109
|
+
return CURATED_TYPES.get(suffix) || null;
|
|
110
|
+
}
|
|
111
|
+
function curatedTypes(value) {
|
|
112
|
+
const values = Array.isArray(value) ? value : [value];
|
|
113
|
+
const types = new Set();
|
|
114
|
+
for (const candidate of values.slice(0, MAX_STRUCTURED_DATA_TYPES)) {
|
|
115
|
+
const normalized = curatedType(candidate);
|
|
116
|
+
if (normalized)
|
|
117
|
+
types.add(normalized);
|
|
118
|
+
}
|
|
119
|
+
return [...types].sort();
|
|
120
|
+
}
|
|
121
|
+
function sameAsHosts(value) {
|
|
122
|
+
const values = Array.isArray(value) ? value : [value];
|
|
123
|
+
const hosts = new Set();
|
|
124
|
+
for (const candidate of values.slice(0, MAX_STRUCTURED_DATA_SAME_AS_HOSTS)) {
|
|
125
|
+
const host = normalizedSameAsHost(candidate);
|
|
126
|
+
if (host)
|
|
127
|
+
hosts.add(host);
|
|
128
|
+
}
|
|
129
|
+
return [...hosts].sort();
|
|
130
|
+
}
|
|
131
|
+
function jsonStructureWithinBounds(value) {
|
|
132
|
+
let depth = 0;
|
|
133
|
+
let inString = false;
|
|
134
|
+
let escaped = false;
|
|
135
|
+
for (const character of value) {
|
|
136
|
+
if (inString) {
|
|
137
|
+
if (escaped)
|
|
138
|
+
escaped = false;
|
|
139
|
+
else if (character === '\\')
|
|
140
|
+
escaped = true;
|
|
141
|
+
else if (character === '"')
|
|
142
|
+
inString = false;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (character === '"') {
|
|
146
|
+
inString = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (character === '{' || character === '[') {
|
|
150
|
+
depth += 1;
|
|
151
|
+
if (depth > MAX_STRUCTURED_DATA_DEPTH)
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
else if (character === '}' || character === ']') {
|
|
155
|
+
depth -= 1;
|
|
156
|
+
if (depth < 0)
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return !inString && depth === 0;
|
|
161
|
+
}
|
|
162
|
+
function entityFromObject(value, baseUrl) {
|
|
163
|
+
const types = curatedTypes(value['@type']);
|
|
164
|
+
if (!types.length)
|
|
165
|
+
return null;
|
|
166
|
+
const name = boundedName(value.name);
|
|
167
|
+
const declaredOrigin = normalizedOrigin(value.url, baseUrl);
|
|
168
|
+
const hosts = sameAsHosts(value.sameAs);
|
|
169
|
+
if (!name && !declaredOrigin && !hosts.length)
|
|
170
|
+
return null;
|
|
171
|
+
return { types, name, declaredOrigin, sameAsHosts: hosts };
|
|
172
|
+
}
|
|
173
|
+
function analyzeStructuredDataIdentity(input = {}) {
|
|
174
|
+
const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
|
|
175
|
+
const baseUrl = safeBaseUrl(input.baseUrl);
|
|
176
|
+
const scripts = htmlAnalysis.scripts.filter((script) => script.mediaType === 'application/ld+json');
|
|
177
|
+
const selectedScripts = scripts.slice(0, MAX_STRUCTURED_DATA_SCRIPTS);
|
|
178
|
+
const entities = new Map();
|
|
179
|
+
let charactersExamined = 0;
|
|
180
|
+
let documentsParsed = 0;
|
|
181
|
+
let malformedScripts = 0;
|
|
182
|
+
let externalScriptsSkipped = 0;
|
|
183
|
+
let objectsExamined = 0;
|
|
184
|
+
let arrayItemsExamined = 0;
|
|
185
|
+
let discardedProperties = 0;
|
|
186
|
+
let entityLimitReached = false;
|
|
187
|
+
let limitReached = scripts.length > MAX_STRUCTURED_DATA_SCRIPTS;
|
|
188
|
+
for (const script of selectedScripts) {
|
|
189
|
+
if (script.reference) {
|
|
190
|
+
externalScriptsSkipped += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const remaining = MAX_STRUCTURED_DATA_TOTAL_CHARS - charactersExamined;
|
|
194
|
+
if (script.inlineContent.length === 0
|
|
195
|
+
|| script.inlineContent.length > MAX_STRUCTURED_DATA_SCRIPT_CHARS
|
|
196
|
+
|| script.inlineContent.length > remaining) {
|
|
197
|
+
limitReached = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
charactersExamined += script.inlineContent.length;
|
|
201
|
+
if (!jsonStructureWithinBounds(script.inlineContent)) {
|
|
202
|
+
malformedScripts += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let parsed;
|
|
206
|
+
try {
|
|
207
|
+
parsed = JSON.parse(script.inlineContent);
|
|
208
|
+
documentsParsed += 1;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
malformedScripts += 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const pending = [{ value: parsed, depth: 0 }];
|
|
215
|
+
while (pending.length) {
|
|
216
|
+
const current = pending.pop();
|
|
217
|
+
if (!current)
|
|
218
|
+
break;
|
|
219
|
+
if (current.depth > MAX_STRUCTURED_DATA_DEPTH) {
|
|
220
|
+
limitReached = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(current.value)) {
|
|
224
|
+
if (arrayItemsExamined >= MAX_STRUCTURED_DATA_ARRAY_ITEMS) {
|
|
225
|
+
limitReached = true;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const remainingItems = MAX_STRUCTURED_DATA_ARRAY_ITEMS - arrayItemsExamined;
|
|
229
|
+
const retained = current.value.slice(0, remainingItems);
|
|
230
|
+
arrayItemsExamined += retained.length;
|
|
231
|
+
if (retained.length < current.value.length)
|
|
232
|
+
limitReached = true;
|
|
233
|
+
for (const child of retained)
|
|
234
|
+
pending.push({ value: child, depth: current.depth + 1 });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const object = record(current.value);
|
|
238
|
+
if (!object)
|
|
239
|
+
continue;
|
|
240
|
+
if (objectsExamined >= MAX_STRUCTURED_DATA_OBJECTS) {
|
|
241
|
+
limitReached = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
objectsExamined += 1;
|
|
245
|
+
const entity = entityFromObject(object, baseUrl);
|
|
246
|
+
if (entity) {
|
|
247
|
+
const key = JSON.stringify(entity);
|
|
248
|
+
if (!entities.has(key)) {
|
|
249
|
+
if (entities.size < MAX_STRUCTURED_DATA_ENTITIES)
|
|
250
|
+
entities.set(key, entity);
|
|
251
|
+
else {
|
|
252
|
+
entityLimitReached = true;
|
|
253
|
+
limitReached = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const entries = Object.entries(object);
|
|
258
|
+
const traversed = entries.slice(0, MAX_STRUCTURED_DATA_PROPERTIES);
|
|
259
|
+
if (traversed.length < entries.length) {
|
|
260
|
+
discardedProperties += entries.length - traversed.length;
|
|
261
|
+
limitReached = true;
|
|
262
|
+
}
|
|
263
|
+
for (const [, child] of traversed) {
|
|
264
|
+
if (child !== null && typeof child === 'object') {
|
|
265
|
+
pending.push({ value: child, depth: current.depth + 1 });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const truncated = input.sourceTruncated === true
|
|
271
|
+
|| htmlAnalysis.inputLimitReached
|
|
272
|
+
|| htmlAnalysis.scriptLimitReached
|
|
273
|
+
|| htmlAnalysis.inlineLimitReached
|
|
274
|
+
|| limitReached;
|
|
275
|
+
const partial = truncated || malformedScripts > 0 || externalScriptsSkipped > 0;
|
|
276
|
+
const limitations = [
|
|
277
|
+
'Structured identity fields are publisher-declared metadata and do not prove identity, ownership, control, safety, or maliciousness.',
|
|
278
|
+
'Only curated schema types, bounded labels, declared origins, and sameAs hostnames are retained; raw JSON-LD and arbitrary properties are discarded.',
|
|
279
|
+
'Static response evidence cannot observe JSON-LD added later by JavaScript.',
|
|
280
|
+
];
|
|
281
|
+
if (input.sourceTruncated === true || htmlAnalysis.inputLimitReached) {
|
|
282
|
+
limitations.push('The captured homepage body was truncated, so structured identity evidence may be incomplete.');
|
|
283
|
+
}
|
|
284
|
+
if (htmlAnalysis.scriptLimitReached || scripts.length > MAX_STRUCTURED_DATA_SCRIPTS) {
|
|
285
|
+
limitations.push(`Only the first ${MAX_STRUCTURED_DATA_SCRIPTS} JSON-LD script elements were eligible for analysis.`);
|
|
286
|
+
}
|
|
287
|
+
if (htmlAnalysis.inlineLimitReached || limitReached) {
|
|
288
|
+
limitations.push(`JSON-LD analysis was bounded to ${MAX_STRUCTURED_DATA_SCRIPT_CHARS} characters per script and ${MAX_STRUCTURED_DATA_TOTAL_CHARS} characters in total.`);
|
|
289
|
+
}
|
|
290
|
+
if (malformedScripts > 0)
|
|
291
|
+
limitations.push(`${malformedScripts} JSON-LD script${malformedScripts === 1 ? ' was' : 's were'} malformed or exceeded the nesting boundary.`);
|
|
292
|
+
if (externalScriptsSkipped > 0)
|
|
293
|
+
limitations.push('Referenced JSON-LD scripts were not fetched.');
|
|
294
|
+
if (entityLimitReached) {
|
|
295
|
+
limitations.push(`Only the first ${MAX_STRUCTURED_DATA_ENTITIES} structured identity entities were retained.`);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
structuredDataVersion: STRUCTURED_DATA_IDENTITY_VERSION,
|
|
299
|
+
...createObservation({
|
|
300
|
+
status: partial ? 'partial' : 'success',
|
|
301
|
+
observedAt: input.observedAt,
|
|
302
|
+
scanMode: 'deep',
|
|
303
|
+
source: 'html',
|
|
304
|
+
complete: !partial,
|
|
305
|
+
truncated,
|
|
306
|
+
limitations,
|
|
307
|
+
diagnostics: {
|
|
308
|
+
scriptsObserved: scripts.length,
|
|
309
|
+
scriptsExamined: selectedScripts.length,
|
|
310
|
+
charactersExamined,
|
|
311
|
+
documentsParsed,
|
|
312
|
+
malformedScripts,
|
|
313
|
+
externalScriptsSkipped,
|
|
314
|
+
objectsExamined,
|
|
315
|
+
arrayItemsExamined,
|
|
316
|
+
discardedProperties,
|
|
317
|
+
entities: entities.size,
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
entities: [...entities.values()],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
export { MAX_STRUCTURED_DATA_ARRAY_ITEMS, MAX_STRUCTURED_DATA_DEPTH, MAX_STRUCTURED_DATA_ENTITIES, MAX_STRUCTURED_DATA_OBJECTS, MAX_STRUCTURED_DATA_SAME_AS_HOSTS, MAX_STRUCTURED_DATA_SCRIPT_CHARS, MAX_STRUCTURED_DATA_SCRIPTS, MAX_STRUCTURED_DATA_TOTAL_CHARS, STRUCTURED_DATA_IDENTITY_VERSION, analyzeStructuredDataIdentity, };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Stable public facade for provider manifests, normalized results, matrices,
|
|
2
|
+
// and the offline connector fixture harness.
|
|
3
|
+
export * from "./threat-intelligence-runtime.mjs";
|
|
4
|
+
export * from "./threat-intelligence-manifests.mjs";
|
|
5
|
+
export * from "./threat-intelligence-fixtures.mjs";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const PROVIDER_DEFINITIONS = new WeakSet();
|
|
2
|
+
const CURATED_CONNECTOR_DEFINITIONS = new WeakSet();
|
|
3
|
+
export function registerThreatIntelligenceProvider(definition) {
|
|
4
|
+
PROVIDER_DEFINITIONS.add(definition);
|
|
5
|
+
return definition;
|
|
6
|
+
}
|
|
7
|
+
export function assertThreatIntelligenceProvider(provider) {
|
|
8
|
+
if (!provider ||
|
|
9
|
+
typeof provider !== 'object' ||
|
|
10
|
+
!PROVIDER_DEFINITIONS.has(provider)) {
|
|
11
|
+
throw new TypeError('A versioned threat-intelligence provider definition is required');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isThreatIntelligenceProvider(provider) {
|
|
15
|
+
return Boolean(provider &&
|
|
16
|
+
typeof provider === 'object' &&
|
|
17
|
+
PROVIDER_DEFINITIONS.has(provider));
|
|
18
|
+
}
|
|
19
|
+
export function registerCuratedConnector(definition) {
|
|
20
|
+
CURATED_CONNECTOR_DEFINITIONS.add(definition);
|
|
21
|
+
return definition;
|
|
22
|
+
}
|
|
23
|
+
export function assertCuratedConnectorDefinition(connector) {
|
|
24
|
+
if (!connector ||
|
|
25
|
+
typeof connector !== 'object' ||
|
|
26
|
+
!CURATED_CONNECTOR_DEFINITIONS.has(connector)) {
|
|
27
|
+
throw new TypeError('A versioned curated connector definition is required');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function isCuratedConnectorDefinition(connector) {
|
|
31
|
+
return Boolean(connector &&
|
|
32
|
+
typeof connector === 'object' &&
|
|
33
|
+
CURATED_CONNECTOR_DEFINITIONS.has(connector));
|
|
34
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createCuratedConnectorResult, normalizeCuratedConnectorTarget, } from "./threat-intelligence-runtime.mjs";
|
|
2
|
+
import { assertCuratedConnectorDefinition } from "./threat-intelligence-definition-registry.mjs";
|
|
3
|
+
const MAX_CONNECTOR_KEY_LENGTH = 80;
|
|
4
|
+
const MAX_CONNECTOR_FIXTURE_BYTES = 512 * 1024;
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
7
|
+
}
|
|
8
|
+
function exactFixtureKeys(value) {
|
|
9
|
+
if (!isRecord(value))
|
|
10
|
+
throw new TypeError('Connector fixture must be an object');
|
|
11
|
+
const allowed = new Set(['id', 'target', 'observedAt', 'json']);
|
|
12
|
+
for (const key of Object.keys(value)) {
|
|
13
|
+
if (!allowed.has(key)) {
|
|
14
|
+
throw new TypeError(`Connector fixture has unknown field: ${key}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function fixtureId(value) {
|
|
19
|
+
if (typeof value !== 'string' ||
|
|
20
|
+
value.length > MAX_CONNECTOR_KEY_LENGTH ||
|
|
21
|
+
!/^[a-z0-9][a-z0-9._-]*$/iu.test(value)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function fixtureTimestamp(value) {
|
|
27
|
+
if (typeof value !== 'string' || value.length > 50)
|
|
28
|
+
return null;
|
|
29
|
+
const timestamp = Date.parse(value);
|
|
30
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
31
|
+
}
|
|
32
|
+
export function runCuratedConnectorFixture(connector, fixture, normalizeFixture) {
|
|
33
|
+
assertCuratedConnectorDefinition(connector);
|
|
34
|
+
exactFixtureKeys(fixture);
|
|
35
|
+
const id = fixtureId(fixture.id);
|
|
36
|
+
const observedAt = fixtureTimestamp(fixture.observedAt);
|
|
37
|
+
if (!id || !observedAt) {
|
|
38
|
+
throw new TypeError('Connector fixture identity or timestamp is invalid');
|
|
39
|
+
}
|
|
40
|
+
if (typeof fixture.json !== 'string' ||
|
|
41
|
+
Buffer.byteLength(fixture.json, 'utf8') > MAX_CONNECTOR_FIXTURE_BYTES) {
|
|
42
|
+
throw new TypeError(`Connector fixture JSON must not exceed ${MAX_CONNECTOR_FIXTURE_BYTES} bytes`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof normalizeFixture !== 'function') {
|
|
45
|
+
throw new TypeError('Connector fixture requires a synchronous normalizer');
|
|
46
|
+
}
|
|
47
|
+
let firstPayload;
|
|
48
|
+
let secondPayload;
|
|
49
|
+
try {
|
|
50
|
+
firstPayload = JSON.parse(fixture.json);
|
|
51
|
+
secondPayload = JSON.parse(fixture.json);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new TypeError('Connector fixture JSON is invalid');
|
|
55
|
+
}
|
|
56
|
+
const targetType = isRecord(fixture.target) && typeof fixture.target.type === 'string'
|
|
57
|
+
? fixture.target.type
|
|
58
|
+
: null;
|
|
59
|
+
const declaration = targetType
|
|
60
|
+
? connector.inputs.find((item) => item.type === targetType)
|
|
61
|
+
: null;
|
|
62
|
+
if (!declaration) {
|
|
63
|
+
throw new TypeError('Connector fixture target is unsupported');
|
|
64
|
+
}
|
|
65
|
+
const normalizedTarget = normalizeCuratedConnectorTarget(fixture.target, declaration.exposure);
|
|
66
|
+
const normalizeOnce = (payload) => {
|
|
67
|
+
const output = normalizeFixture(payload, Object.freeze({ ...normalizedTarget }));
|
|
68
|
+
if (output &&
|
|
69
|
+
(typeof output === 'object' || typeof output === 'function') &&
|
|
70
|
+
typeof output.then === 'function') {
|
|
71
|
+
throw new TypeError('Connector fixture normalizers must be synchronous');
|
|
72
|
+
}
|
|
73
|
+
return createCuratedConnectorResult(connector, fixture.target, output, observedAt);
|
|
74
|
+
};
|
|
75
|
+
const first = normalizeOnce(firstPayload);
|
|
76
|
+
const second = normalizeOnce(secondPayload);
|
|
77
|
+
if (JSON.stringify(first) !== JSON.stringify(second)) {
|
|
78
|
+
throw new TypeError(`Connector fixture ${id} is not deterministic`);
|
|
79
|
+
}
|
|
80
|
+
return first;
|
|
81
|
+
}
|
|
82
|
+
export { MAX_CONNECTOR_FIXTURE_BYTES };
|