@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,512 @@
|
|
|
1
|
+
import { registerCuratedConnector, registerThreatIntelligenceProvider, isCuratedConnectorDefinition, isThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
|
|
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
|
+
const MAX_PROVIDER_ID_LENGTH = 40;
|
|
4
|
+
const MAX_PROVIDER_LABEL_LENGTH = 100;
|
|
5
|
+
const MAX_URL_LENGTH = 2_048;
|
|
6
|
+
const MAX_REQUEST_BUDGET = 1_000_000;
|
|
7
|
+
const MAX_CONNECTOR_SCOPES = 20;
|
|
8
|
+
const MAX_CONNECTOR_SCOPE_LENGTH = 80;
|
|
9
|
+
const CAPABILITIES = new Set([
|
|
10
|
+
'domain_lookup',
|
|
11
|
+
'url_lookup',
|
|
12
|
+
'indicator_search',
|
|
13
|
+
]);
|
|
14
|
+
const URL_EXPOSURES = new Set([
|
|
15
|
+
'registrable_domain',
|
|
16
|
+
'hostname',
|
|
17
|
+
'origin',
|
|
18
|
+
'full_url',
|
|
19
|
+
]);
|
|
20
|
+
const COMMERCIAL_USE = new Set(['allowed', 'restricted', 'unknown']);
|
|
21
|
+
const ATTRIBUTION = new Set(['required', 'not_required', 'unknown']);
|
|
22
|
+
const CACHING = new Set([
|
|
23
|
+
'prohibited',
|
|
24
|
+
'transient',
|
|
25
|
+
'bounded',
|
|
26
|
+
'provider_defined',
|
|
27
|
+
'unknown',
|
|
28
|
+
]);
|
|
29
|
+
const QUERY_RETENTION = new Set([
|
|
30
|
+
'none',
|
|
31
|
+
'limited',
|
|
32
|
+
'provider_defined',
|
|
33
|
+
'unknown',
|
|
34
|
+
]);
|
|
35
|
+
const REDISTRIBUTION = new Set([
|
|
36
|
+
'allowed',
|
|
37
|
+
'restricted',
|
|
38
|
+
'prohibited',
|
|
39
|
+
'unknown',
|
|
40
|
+
]);
|
|
41
|
+
const CONNECTOR_KINDS = new Set([
|
|
42
|
+
'discovery',
|
|
43
|
+
'enrichment',
|
|
44
|
+
]);
|
|
45
|
+
const CONNECTOR_COLLECTIONS = new Set([
|
|
46
|
+
'passive',
|
|
47
|
+
'active',
|
|
48
|
+
'third_party',
|
|
49
|
+
]);
|
|
50
|
+
const CONNECTOR_CREDENTIAL_MODES = new Set([
|
|
51
|
+
'none',
|
|
52
|
+
'optional',
|
|
53
|
+
'required',
|
|
54
|
+
]);
|
|
55
|
+
const CONNECTOR_ENTITY_TYPES = new Set([
|
|
56
|
+
'domain',
|
|
57
|
+
'hostname',
|
|
58
|
+
'url',
|
|
59
|
+
'ipv4',
|
|
60
|
+
'ipv6',
|
|
61
|
+
'asn',
|
|
62
|
+
'certificate',
|
|
63
|
+
]);
|
|
64
|
+
const CONNECTOR_RELATIONSHIP_TYPES = new Set([
|
|
65
|
+
'domain_resolves_to_ip',
|
|
66
|
+
'domain_uses_nameserver',
|
|
67
|
+
'domain_uses_mail_server',
|
|
68
|
+
'domain_presented_certificate',
|
|
69
|
+
'certificate_names_domain',
|
|
70
|
+
'ip_hosts_domain',
|
|
71
|
+
'domain_related_to_domain',
|
|
72
|
+
]);
|
|
73
|
+
const CONNECTOR_TARGET_EXPOSURES = Object.freeze({
|
|
74
|
+
domain: new Set(['registrable_domain']),
|
|
75
|
+
hostname: new Set(['hostname']),
|
|
76
|
+
url: new Set([
|
|
77
|
+
'registrable_domain',
|
|
78
|
+
'hostname',
|
|
79
|
+
'origin',
|
|
80
|
+
'full_url',
|
|
81
|
+
]),
|
|
82
|
+
ipv4: new Set(['ip_address']),
|
|
83
|
+
ipv6: new Set(['ip_address']),
|
|
84
|
+
asn: new Set(['asn']),
|
|
85
|
+
certificate: new Set([
|
|
86
|
+
'certificate_fingerprint',
|
|
87
|
+
]),
|
|
88
|
+
});
|
|
89
|
+
const CONNECTOR_RELATIONSHIP_ENDPOINTS = Object.freeze({
|
|
90
|
+
domain_resolves_to_ip: {
|
|
91
|
+
from: new Set(['domain', 'hostname']),
|
|
92
|
+
to: new Set(['ipv4', 'ipv6']),
|
|
93
|
+
},
|
|
94
|
+
domain_uses_nameserver: {
|
|
95
|
+
from: new Set(['domain']),
|
|
96
|
+
to: new Set(['hostname']),
|
|
97
|
+
},
|
|
98
|
+
domain_uses_mail_server: {
|
|
99
|
+
from: new Set(['domain']),
|
|
100
|
+
to: new Set(['hostname']),
|
|
101
|
+
},
|
|
102
|
+
domain_presented_certificate: {
|
|
103
|
+
from: new Set(['domain', 'hostname']),
|
|
104
|
+
to: new Set(['certificate']),
|
|
105
|
+
},
|
|
106
|
+
certificate_names_domain: {
|
|
107
|
+
from: new Set(['certificate']),
|
|
108
|
+
to: new Set(['domain', 'hostname']),
|
|
109
|
+
},
|
|
110
|
+
ip_hosts_domain: {
|
|
111
|
+
from: new Set(['ipv4', 'ipv6']),
|
|
112
|
+
to: new Set(['domain', 'hostname']),
|
|
113
|
+
},
|
|
114
|
+
domain_related_to_domain: {
|
|
115
|
+
from: new Set(['domain', 'hostname']),
|
|
116
|
+
to: new Set(['domain', 'hostname']),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
function isRecord(value) {
|
|
120
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
121
|
+
}
|
|
122
|
+
function exactKeys(value, allowed, label) {
|
|
123
|
+
if (!isRecord(value))
|
|
124
|
+
throw new TypeError(`${label} must be an object`);
|
|
125
|
+
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
126
|
+
if (unknown) {
|
|
127
|
+
throw new TypeError(`${label} contains an unknown field: ${unknown}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function strictBoundedString(value, maximum) {
|
|
131
|
+
if (typeof value !== 'string' ||
|
|
132
|
+
value.length > maximum ||
|
|
133
|
+
/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const normalized = value.trim().replace(/\s+/gu, ' ');
|
|
137
|
+
return normalized && normalized.length <= maximum ? normalized : null;
|
|
138
|
+
}
|
|
139
|
+
function isoTimestamp(value) {
|
|
140
|
+
if (typeof value !== 'string' ||
|
|
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;
|
|
147
|
+
}
|
|
148
|
+
function httpsUrl(value) {
|
|
149
|
+
const raw = strictBoundedString(value, MAX_URL_LENGTH);
|
|
150
|
+
if (!raw)
|
|
151
|
+
return null;
|
|
152
|
+
try {
|
|
153
|
+
const parsed = new URL(raw);
|
|
154
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const normalized = parsed.toString();
|
|
158
|
+
return normalized.length <= MAX_URL_LENGTH ? normalized : null;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function enumValue(value, allowed, label) {
|
|
165
|
+
if (typeof value !== 'string' || !allowed.has(value)) {
|
|
166
|
+
throw new TypeError(`${label} is invalid`);
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
function boundedInteger(value, minimum, maximum, label) {
|
|
171
|
+
if (typeof value !== 'number' ||
|
|
172
|
+
!Number.isSafeInteger(value) ||
|
|
173
|
+
value < minimum ||
|
|
174
|
+
value > maximum) {
|
|
175
|
+
throw new TypeError(`${label} must be an integer between ${minimum} and ${maximum}`);
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
function normalizeTargets(value) {
|
|
180
|
+
exactKeys(value, new Set(['domain', 'url']), 'Provider targets');
|
|
181
|
+
const targets = {};
|
|
182
|
+
if (value.domain !== undefined) {
|
|
183
|
+
targets.domain = enumValue(value.domain, new Set(['registrable_domain']), 'Provider domain exposure');
|
|
184
|
+
}
|
|
185
|
+
if (value.url !== undefined) {
|
|
186
|
+
targets.url = enumValue(value.url, URL_EXPOSURES, 'Provider url exposure');
|
|
187
|
+
}
|
|
188
|
+
if (!Object.keys(targets).length) {
|
|
189
|
+
throw new TypeError('At least one provider target is required');
|
|
190
|
+
}
|
|
191
|
+
return Object.freeze(targets);
|
|
192
|
+
}
|
|
193
|
+
function normalizeTerms(value) {
|
|
194
|
+
exactKeys(value, new Set([
|
|
195
|
+
'reviewedAt',
|
|
196
|
+
'termsUrl',
|
|
197
|
+
'privacyUrl',
|
|
198
|
+
'commercialUse',
|
|
199
|
+
'attribution',
|
|
200
|
+
'caching',
|
|
201
|
+
'queryRetention',
|
|
202
|
+
'redistribution',
|
|
203
|
+
]), 'Provider terms');
|
|
204
|
+
const reviewedAt = isoTimestamp(value.reviewedAt);
|
|
205
|
+
const termsUrl = httpsUrl(value.termsUrl);
|
|
206
|
+
const privacyUrl = value.privacyUrl === null ? null : httpsUrl(value.privacyUrl);
|
|
207
|
+
if (!reviewedAt || !termsUrl || (value.privacyUrl !== null && !privacyUrl)) {
|
|
208
|
+
throw new TypeError('Provider terms require a valid review timestamp and HTTPS policy URLs');
|
|
209
|
+
}
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
reviewedAt,
|
|
212
|
+
termsUrl,
|
|
213
|
+
privacyUrl,
|
|
214
|
+
commercialUse: enumValue(value.commercialUse, COMMERCIAL_USE, 'Commercial-use policy'),
|
|
215
|
+
attribution: enumValue(value.attribution, ATTRIBUTION, 'Attribution policy'),
|
|
216
|
+
caching: enumValue(value.caching, CACHING, 'Caching policy'),
|
|
217
|
+
queryRetention: enumValue(value.queryRetention, QUERY_RETENTION, 'Provider query-retention policy'),
|
|
218
|
+
redistribution: enumValue(value.redistribution, REDISTRIBUTION, 'Redistribution policy'),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function normalizeLimits(value, terms) {
|
|
222
|
+
exactKeys(value, new Set([
|
|
223
|
+
'timeoutMs',
|
|
224
|
+
'maxResponseBytes',
|
|
225
|
+
'cacheTtlMs',
|
|
226
|
+
'concurrency',
|
|
227
|
+
'dailyRequests',
|
|
228
|
+
'monthlyRequests',
|
|
229
|
+
]), 'Provider limits');
|
|
230
|
+
const limits = {
|
|
231
|
+
timeoutMs: boundedInteger(value.timeoutMs, 250, MAX_TIMEOUT_MS, 'Provider timeout'),
|
|
232
|
+
maxResponseBytes: boundedInteger(value.maxResponseBytes, 1_024, MAX_RESPONSE_BYTES, 'Provider response cap'),
|
|
233
|
+
cacheTtlMs: boundedInteger(value.cacheTtlMs, 0, MAX_CACHE_TTL_MS, 'Provider cache TTL'),
|
|
234
|
+
concurrency: boundedInteger(value.concurrency, 1, 10, 'Provider concurrency'),
|
|
235
|
+
dailyRequests: boundedInteger(value.dailyRequests, 1, MAX_REQUEST_BUDGET, 'Provider daily request budget'),
|
|
236
|
+
monthlyRequests: boundedInteger(value.monthlyRequests, 1, MAX_REQUEST_BUDGET, 'Provider monthly request budget'),
|
|
237
|
+
};
|
|
238
|
+
if (limits.monthlyRequests < limits.dailyRequests) {
|
|
239
|
+
throw new TypeError('Provider monthly request budget must not be lower than its daily budget');
|
|
240
|
+
}
|
|
241
|
+
if (['prohibited', 'unknown'].includes(terms.caching) &&
|
|
242
|
+
limits.cacheTtlMs !== 0) {
|
|
243
|
+
throw new TypeError('Provider cache TTL must be zero when caching is prohibited or unknown');
|
|
244
|
+
}
|
|
245
|
+
return Object.freeze(limits);
|
|
246
|
+
}
|
|
247
|
+
export function defineThreatIntelligenceProvider(value) {
|
|
248
|
+
exactKeys(value, new Set([
|
|
249
|
+
'id',
|
|
250
|
+
'label',
|
|
251
|
+
'capabilities',
|
|
252
|
+
'targets',
|
|
253
|
+
'interaction',
|
|
254
|
+
'terms',
|
|
255
|
+
'limits',
|
|
256
|
+
]), 'Provider definition');
|
|
257
|
+
const id = strictBoundedString(value.id, MAX_PROVIDER_ID_LENGTH);
|
|
258
|
+
const label = strictBoundedString(value.label, MAX_PROVIDER_LABEL_LENGTH);
|
|
259
|
+
if (!id || !/^[a-z0-9][a-z0-9_-]*$/u.test(id) || !label) {
|
|
260
|
+
throw new TypeError('Provider identity is invalid');
|
|
261
|
+
}
|
|
262
|
+
if (value.interaction !== 'lookup_only') {
|
|
263
|
+
throw new TypeError('Threat-intelligence providers must be lookup-only in contract version 1');
|
|
264
|
+
}
|
|
265
|
+
if (!Array.isArray(value.capabilities) ||
|
|
266
|
+
!value.capabilities.length ||
|
|
267
|
+
value.capabilities.length > CAPABILITIES.size) {
|
|
268
|
+
throw new TypeError('Provider capabilities must be a non-empty bounded array');
|
|
269
|
+
}
|
|
270
|
+
const capabilities = [
|
|
271
|
+
...new Set(value.capabilities.map((item) => enumValue(item, CAPABILITIES, 'Provider capability'))),
|
|
272
|
+
].sort();
|
|
273
|
+
if (capabilities.length !== value.capabilities.length) {
|
|
274
|
+
throw new TypeError('Provider capabilities must be unique');
|
|
275
|
+
}
|
|
276
|
+
const targets = normalizeTargets(value.targets);
|
|
277
|
+
if (capabilities.includes('domain_lookup') && !targets.domain) {
|
|
278
|
+
throw new TypeError('Domain lookup capability requires a domain target');
|
|
279
|
+
}
|
|
280
|
+
if (capabilities.includes('url_lookup') && !targets.url) {
|
|
281
|
+
throw new TypeError('URL lookup capability requires a URL target');
|
|
282
|
+
}
|
|
283
|
+
if (targets.domain &&
|
|
284
|
+
!capabilities.some((item) => ['domain_lookup', 'indicator_search'].includes(item))) {
|
|
285
|
+
throw new TypeError('Domain targets require a compatible lookup capability');
|
|
286
|
+
}
|
|
287
|
+
if (targets.url &&
|
|
288
|
+
!capabilities.some((item) => ['url_lookup', 'indicator_search'].includes(item))) {
|
|
289
|
+
throw new TypeError('URL targets require a compatible lookup capability');
|
|
290
|
+
}
|
|
291
|
+
const terms = normalizeTerms(value.terms);
|
|
292
|
+
return registerThreatIntelligenceProvider(Object.freeze({
|
|
293
|
+
version: THREAT_INTELLIGENCE_CONTRACT_VERSION,
|
|
294
|
+
id,
|
|
295
|
+
label,
|
|
296
|
+
capabilities: Object.freeze(capabilities),
|
|
297
|
+
targets,
|
|
298
|
+
interaction: 'lookup_only',
|
|
299
|
+
terms,
|
|
300
|
+
limits: normalizeLimits(value.limits, terms),
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
function normalizeConnectorCredentials(value) {
|
|
304
|
+
exactKeys(value, new Set(['mode', 'scopes']), 'Connector credentials');
|
|
305
|
+
const mode = enumValue(value.mode, CONNECTOR_CREDENTIAL_MODES, 'Connector credential mode');
|
|
306
|
+
if (!Array.isArray(value.scopes) || value.scopes.length > MAX_CONNECTOR_SCOPES) {
|
|
307
|
+
throw new TypeError('Connector credential scopes must be a bounded array');
|
|
308
|
+
}
|
|
309
|
+
const scopes = value.scopes.map((item) => strictBoundedString(item, MAX_CONNECTOR_SCOPE_LENGTH));
|
|
310
|
+
if (scopes.some((item) => !item || !/^[a-z0-9][a-z0-9:_-]*$/iu.test(item))) {
|
|
311
|
+
throw new TypeError('Connector credential scopes are invalid');
|
|
312
|
+
}
|
|
313
|
+
const unique = [...new Set(scopes)].sort();
|
|
314
|
+
if (unique.length !== scopes.length ||
|
|
315
|
+
(mode === 'none' && unique.length !== 0) ||
|
|
316
|
+
(mode !== 'none' && unique.length === 0)) {
|
|
317
|
+
throw new TypeError('Connector credential mode and scopes are inconsistent');
|
|
318
|
+
}
|
|
319
|
+
return Object.freeze({ mode, scopes: Object.freeze(unique) });
|
|
320
|
+
}
|
|
321
|
+
function normalizeConnectorInputs(value) {
|
|
322
|
+
if (!Array.isArray(value) ||
|
|
323
|
+
value.length === 0 ||
|
|
324
|
+
value.length > CONNECTOR_ENTITY_TYPES.size) {
|
|
325
|
+
throw new TypeError('Connector inputs must be a non-empty bounded array');
|
|
326
|
+
}
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
const inputs = value.map((item) => {
|
|
329
|
+
exactKeys(item, new Set(['type', 'exposure']), 'Connector input');
|
|
330
|
+
const type = enumValue(item.type, CONNECTOR_ENTITY_TYPES, 'Connector input type');
|
|
331
|
+
const exposure = enumValue(item.exposure, CONNECTOR_TARGET_EXPOSURES[type], 'Connector input exposure');
|
|
332
|
+
if (seen.has(type)) {
|
|
333
|
+
throw new TypeError('Connector input types must be unique');
|
|
334
|
+
}
|
|
335
|
+
seen.add(type);
|
|
336
|
+
return Object.freeze({ type, exposure });
|
|
337
|
+
});
|
|
338
|
+
return Object.freeze(inputs.sort((left, right) => left.type.localeCompare(right.type)));
|
|
339
|
+
}
|
|
340
|
+
function normalizeConnectorOutputs(value) {
|
|
341
|
+
exactKeys(value, new Set(['entities', 'relationships']), 'Connector outputs');
|
|
342
|
+
if (!Array.isArray(value.entities) ||
|
|
343
|
+
!Array.isArray(value.relationships) ||
|
|
344
|
+
value.entities.length > CONNECTOR_ENTITY_TYPES.size ||
|
|
345
|
+
value.relationships.length > CONNECTOR_RELATIONSHIP_TYPES.size) {
|
|
346
|
+
throw new TypeError('Connector outputs must use bounded arrays');
|
|
347
|
+
}
|
|
348
|
+
const entities = value.entities.map((item) => enumValue(item, CONNECTOR_ENTITY_TYPES, 'Connector output entity'));
|
|
349
|
+
const relationships = value.relationships.map((item) => enumValue(item, CONNECTOR_RELATIONSHIP_TYPES, 'Connector output relationship'));
|
|
350
|
+
if (new Set(entities).size !== entities.length ||
|
|
351
|
+
new Set(relationships).size !== relationships.length) {
|
|
352
|
+
throw new TypeError('Connector outputs must be unique');
|
|
353
|
+
}
|
|
354
|
+
if (!entities.length) {
|
|
355
|
+
throw new TypeError('Connector outputs require at least one normalized entity type');
|
|
356
|
+
}
|
|
357
|
+
const entityTypes = new Set(entities);
|
|
358
|
+
for (const relationship of relationships) {
|
|
359
|
+
const endpoints = CONNECTOR_RELATIONSHIP_ENDPOINTS[relationship];
|
|
360
|
+
if (![...endpoints.from].some((type) => entityTypes.has(type)) ||
|
|
361
|
+
![...endpoints.to].some((type) => entityTypes.has(type))) {
|
|
362
|
+
throw new TypeError(`Connector relationship output ${relationship} lacks compatible entity outputs`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return Object.freeze({
|
|
366
|
+
entities: Object.freeze(entities.sort()),
|
|
367
|
+
relationships: Object.freeze(relationships.sort()),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function normalizeConnectorLimits(value, terms) {
|
|
371
|
+
exactKeys(value, new Set([
|
|
372
|
+
'timeoutMs',
|
|
373
|
+
'maxResponseBytes',
|
|
374
|
+
'cacheTtlMs',
|
|
375
|
+
'concurrency',
|
|
376
|
+
'dailyRequests',
|
|
377
|
+
'monthlyRequests',
|
|
378
|
+
'maxEntities',
|
|
379
|
+
'maxRelationships',
|
|
380
|
+
]), 'Connector limits');
|
|
381
|
+
const limits = {
|
|
382
|
+
timeoutMs: boundedInteger(value.timeoutMs, 250, MAX_TIMEOUT_MS, 'Connector timeout'),
|
|
383
|
+
maxResponseBytes: boundedInteger(value.maxResponseBytes, 1_024, MAX_RESPONSE_BYTES, 'Connector response cap'),
|
|
384
|
+
cacheTtlMs: boundedInteger(value.cacheTtlMs, 0, MAX_CACHE_TTL_MS, 'Connector cache TTL'),
|
|
385
|
+
concurrency: boundedInteger(value.concurrency, 1, 10, 'Connector concurrency'),
|
|
386
|
+
dailyRequests: boundedInteger(value.dailyRequests, 1, MAX_REQUEST_BUDGET, 'Connector daily request budget'),
|
|
387
|
+
monthlyRequests: boundedInteger(value.monthlyRequests, 1, MAX_REQUEST_BUDGET, 'Connector monthly request budget'),
|
|
388
|
+
maxEntities: boundedInteger(value.maxEntities, 1, MAX_CONNECTOR_ENTITIES, 'Connector entity cap'),
|
|
389
|
+
maxRelationships: boundedInteger(value.maxRelationships, 0, MAX_CONNECTOR_RELATIONSHIPS, 'Connector relationship cap'),
|
|
390
|
+
};
|
|
391
|
+
if (limits.monthlyRequests < limits.dailyRequests) {
|
|
392
|
+
throw new TypeError('Connector monthly request budget must not be lower than its daily budget');
|
|
393
|
+
}
|
|
394
|
+
if (['prohibited', 'unknown'].includes(terms.caching) &&
|
|
395
|
+
limits.cacheTtlMs !== 0) {
|
|
396
|
+
throw new TypeError('Connector cache TTL must be zero when caching is prohibited or unknown');
|
|
397
|
+
}
|
|
398
|
+
return Object.freeze(limits);
|
|
399
|
+
}
|
|
400
|
+
export function defineCuratedConnector(value) {
|
|
401
|
+
exactKeys(value, new Set([
|
|
402
|
+
'id',
|
|
403
|
+
'label',
|
|
404
|
+
'kinds',
|
|
405
|
+
'inputs',
|
|
406
|
+
'outputs',
|
|
407
|
+
'collection',
|
|
408
|
+
'credentials',
|
|
409
|
+
'terms',
|
|
410
|
+
'limits',
|
|
411
|
+
'enabledByDefault',
|
|
412
|
+
]), 'Connector definition');
|
|
413
|
+
const id = strictBoundedString(value.id, MAX_PROVIDER_ID_LENGTH);
|
|
414
|
+
const label = strictBoundedString(value.label, MAX_PROVIDER_LABEL_LENGTH);
|
|
415
|
+
if (!id || !/^[a-z0-9][a-z0-9_-]*$/u.test(id) || !label) {
|
|
416
|
+
throw new TypeError('Connector identity is invalid');
|
|
417
|
+
}
|
|
418
|
+
if (!Array.isArray(value.kinds) ||
|
|
419
|
+
!value.kinds.length ||
|
|
420
|
+
value.kinds.length > CONNECTOR_KINDS.size) {
|
|
421
|
+
throw new TypeError('Connector kinds must be a non-empty bounded array');
|
|
422
|
+
}
|
|
423
|
+
const kinds = value.kinds.map((item) => enumValue(item, CONNECTOR_KINDS, 'Connector kind'));
|
|
424
|
+
if (new Set(kinds).size !== kinds.length) {
|
|
425
|
+
throw new TypeError('Connector kinds must be unique');
|
|
426
|
+
}
|
|
427
|
+
if (value.enabledByDefault !== false) {
|
|
428
|
+
throw new TypeError('Curated connectors must be disabled by default');
|
|
429
|
+
}
|
|
430
|
+
const terms = normalizeTerms(value.terms);
|
|
431
|
+
const outputs = normalizeConnectorOutputs(value.outputs);
|
|
432
|
+
const limits = normalizeConnectorLimits(value.limits, terms);
|
|
433
|
+
if (outputs.relationships.length > 0 && limits.maxRelationships === 0) {
|
|
434
|
+
throw new TypeError('Connector relationship outputs require a positive relationship cap');
|
|
435
|
+
}
|
|
436
|
+
return registerCuratedConnector(Object.freeze({
|
|
437
|
+
version: CURATED_CONNECTOR_CONTRACT_VERSION,
|
|
438
|
+
id,
|
|
439
|
+
label,
|
|
440
|
+
kinds: Object.freeze(kinds.sort()),
|
|
441
|
+
inputs: normalizeConnectorInputs(value.inputs),
|
|
442
|
+
outputs,
|
|
443
|
+
collection: enumValue(value.collection, CONNECTOR_COLLECTIONS, 'Connector collection mode'),
|
|
444
|
+
credentials: normalizeConnectorCredentials(value.credentials),
|
|
445
|
+
terms,
|
|
446
|
+
limits,
|
|
447
|
+
enabledByDefault: false,
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
export function buildThreatIntelligenceProviderMatrix(providers) {
|
|
451
|
+
if (!Array.isArray(providers) || providers.length > 100) {
|
|
452
|
+
throw new TypeError('Provider matrix input must be a bounded array');
|
|
453
|
+
}
|
|
454
|
+
const seen = new Set();
|
|
455
|
+
return providers
|
|
456
|
+
.map((provider) => {
|
|
457
|
+
if (!isRecord(provider) ||
|
|
458
|
+
!isThreatIntelligenceProvider(provider) ||
|
|
459
|
+
typeof provider.id !== 'string' ||
|
|
460
|
+
seen.has(provider.id)) {
|
|
461
|
+
throw new TypeError('Provider matrix requires unique versioned definitions');
|
|
462
|
+
}
|
|
463
|
+
const definition = provider;
|
|
464
|
+
seen.add(definition.id);
|
|
465
|
+
return {
|
|
466
|
+
id: definition.id,
|
|
467
|
+
label: definition.label,
|
|
468
|
+
capabilities: [...definition.capabilities],
|
|
469
|
+
targets: { ...definition.targets },
|
|
470
|
+
interaction: definition.interaction,
|
|
471
|
+
terms: { ...definition.terms },
|
|
472
|
+
limits: { ...definition.limits },
|
|
473
|
+
};
|
|
474
|
+
})
|
|
475
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
476
|
+
}
|
|
477
|
+
export function buildCuratedConnectorMatrix(connectors) {
|
|
478
|
+
if (!Array.isArray(connectors) || connectors.length > 100) {
|
|
479
|
+
throw new TypeError('Connector matrix input must be a bounded array');
|
|
480
|
+
}
|
|
481
|
+
const seen = new Set();
|
|
482
|
+
return connectors
|
|
483
|
+
.map((connector) => {
|
|
484
|
+
if (!isRecord(connector) ||
|
|
485
|
+
!isCuratedConnectorDefinition(connector) ||
|
|
486
|
+
typeof connector.id !== 'string' ||
|
|
487
|
+
seen.has(connector.id)) {
|
|
488
|
+
throw new TypeError('Connector matrix requires unique versioned definitions');
|
|
489
|
+
}
|
|
490
|
+
const definition = connector;
|
|
491
|
+
seen.add(definition.id);
|
|
492
|
+
return {
|
|
493
|
+
id: definition.id,
|
|
494
|
+
label: definition.label,
|
|
495
|
+
kinds: [...definition.kinds],
|
|
496
|
+
inputs: definition.inputs.map((item) => ({ ...item })),
|
|
497
|
+
outputs: {
|
|
498
|
+
entities: [...definition.outputs.entities],
|
|
499
|
+
relationships: [...definition.outputs.relationships],
|
|
500
|
+
},
|
|
501
|
+
collection: definition.collection,
|
|
502
|
+
credentials: {
|
|
503
|
+
mode: definition.credentials.mode,
|
|
504
|
+
scopes: [...definition.credentials.scopes],
|
|
505
|
+
},
|
|
506
|
+
terms: { ...definition.terms },
|
|
507
|
+
limits: { ...definition.limits },
|
|
508
|
+
enabledByDefault: false,
|
|
509
|
+
};
|
|
510
|
+
})
|
|
511
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
512
|
+
}
|