@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,662 @@
|
|
|
1
|
+
// Provider-neutral contracts for optional external threat-intelligence sources
|
|
2
|
+
// and curated discovery or enrichment connectors. This module performs no
|
|
3
|
+
// network requests and contains no provider credentials. Adapters added later
|
|
4
|
+
// must declare exact input exposure, reviewed terms, and bounded request and
|
|
5
|
+
// output budgets before producing separately attributed normalized evidence.
|
|
6
|
+
import { createObservation } from "./observation.mjs";
|
|
7
|
+
import { assertCuratedConnectorDefinition, assertThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
|
|
8
|
+
import { boundedHttpsUrl, boundedInteger, boundedString, isRecord, isoTimestamp, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
|
|
9
|
+
import { CURATED_CONNECTOR_ENTITY_TYPES, MAX_THREAT_INTELLIGENCE_URL_LENGTH, normalizeCuratedConnectorTarget, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-targets.mjs";
|
|
10
|
+
const THREAT_INTELLIGENCE_CONTRACT_VERSION = 1;
|
|
11
|
+
const THREAT_INTELLIGENCE_SCHEMA = 'whoisleuth.threat-intelligence-result';
|
|
12
|
+
const CURATED_CONNECTOR_CONTRACT_VERSION = 1;
|
|
13
|
+
const CURATED_CONNECTOR_RESULT_SCHEMA = 'whoisleuth.curated-connector-result';
|
|
14
|
+
const RESULT_STATES = new Set([
|
|
15
|
+
'success',
|
|
16
|
+
'partial',
|
|
17
|
+
'not_found',
|
|
18
|
+
'unsupported',
|
|
19
|
+
'skipped',
|
|
20
|
+
'rate_limited',
|
|
21
|
+
'unavailable',
|
|
22
|
+
'error',
|
|
23
|
+
]);
|
|
24
|
+
const TERMINAL_STATES_WITHOUT_FINDINGS = new Set([
|
|
25
|
+
'not_found',
|
|
26
|
+
'unsupported',
|
|
27
|
+
'skipped',
|
|
28
|
+
'rate_limited',
|
|
29
|
+
'unavailable',
|
|
30
|
+
'error',
|
|
31
|
+
]);
|
|
32
|
+
const CATEGORIES = new Set(['phishing', 'malware', 'spam', 'suspicious', 'abuse', 'unknown']);
|
|
33
|
+
const SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'unknown']);
|
|
34
|
+
const CONFIDENCES = new Set(['high', 'medium', 'low', 'unknown']);
|
|
35
|
+
const CONNECTOR_RELATIONSHIP_TYPES = new Set([
|
|
36
|
+
'domain_resolves_to_ip',
|
|
37
|
+
'domain_uses_nameserver',
|
|
38
|
+
'domain_uses_mail_server',
|
|
39
|
+
'domain_presented_certificate',
|
|
40
|
+
'certificate_names_domain',
|
|
41
|
+
'ip_hosts_domain',
|
|
42
|
+
'domain_related_to_domain',
|
|
43
|
+
]);
|
|
44
|
+
const CONNECTOR_RELATIONSHIP_CLASSIFICATIONS = new Set([
|
|
45
|
+
'direct',
|
|
46
|
+
'normalized',
|
|
47
|
+
'derived',
|
|
48
|
+
]);
|
|
49
|
+
const CONNECTOR_RELATIONSHIP_ENDPOINTS = Object.freeze({
|
|
50
|
+
domain_resolves_to_ip: {
|
|
51
|
+
from: new Set(['domain', 'hostname']),
|
|
52
|
+
to: new Set(['ipv4', 'ipv6']),
|
|
53
|
+
},
|
|
54
|
+
domain_uses_nameserver: {
|
|
55
|
+
from: new Set(['domain']),
|
|
56
|
+
to: new Set(['hostname']),
|
|
57
|
+
},
|
|
58
|
+
domain_uses_mail_server: {
|
|
59
|
+
from: new Set(['domain']),
|
|
60
|
+
to: new Set(['hostname']),
|
|
61
|
+
},
|
|
62
|
+
domain_presented_certificate: {
|
|
63
|
+
from: new Set(['domain', 'hostname']),
|
|
64
|
+
to: new Set(['certificate']),
|
|
65
|
+
},
|
|
66
|
+
certificate_names_domain: {
|
|
67
|
+
from: new Set(['certificate']),
|
|
68
|
+
to: new Set(['domain', 'hostname']),
|
|
69
|
+
},
|
|
70
|
+
ip_hosts_domain: {
|
|
71
|
+
from: new Set(['ipv4', 'ipv6']),
|
|
72
|
+
to: new Set(['domain', 'hostname']),
|
|
73
|
+
},
|
|
74
|
+
domain_related_to_domain: {
|
|
75
|
+
from: new Set(['domain', 'hostname']),
|
|
76
|
+
to: new Set(['domain', 'hostname']),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
// createObservation() retains at most 40 source characters, so provider IDs use
|
|
80
|
+
// the same ceiling and can never be truncated at the provenance boundary.
|
|
81
|
+
const MAX_PROVIDER_ID_LENGTH = 40;
|
|
82
|
+
const MAX_PROVIDER_LABEL_LENGTH = 100;
|
|
83
|
+
const MAX_DETAIL_LENGTH = 500;
|
|
84
|
+
const MAX_FINDING_ID_LENGTH = 160;
|
|
85
|
+
const MAX_VERDICT_LENGTH = 160;
|
|
86
|
+
const MAX_TAGS = 20;
|
|
87
|
+
const MAX_TAG_LENGTH = 64;
|
|
88
|
+
// Match the shared observation envelope so the contract never promises more
|
|
89
|
+
// limitations than createObservation() can retain.
|
|
90
|
+
const MAX_LIMITATIONS = 10;
|
|
91
|
+
const MAX_FINDINGS = 100;
|
|
92
|
+
const MAX_INPUT_FINDINGS = 500;
|
|
93
|
+
const MAX_TIMEOUT_MS = 15_000;
|
|
94
|
+
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
95
|
+
const MAX_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
96
|
+
const MAX_CONNECTOR_ENTITIES = 200;
|
|
97
|
+
const MAX_CONNECTOR_RELATIONSHIPS = 400;
|
|
98
|
+
const MAX_CONNECTOR_INPUT_ENTITIES = 800;
|
|
99
|
+
const MAX_CONNECTOR_INPUT_RELATIONSHIPS = 1_600;
|
|
100
|
+
const MAX_CONNECTOR_ATTRIBUTES = 20;
|
|
101
|
+
const MAX_CONNECTOR_ATTRIBUTE_LENGTH = 300;
|
|
102
|
+
const MAX_CONNECTOR_METHOD_LENGTH = 160;
|
|
103
|
+
const MAX_CONNECTOR_KEY_LENGTH = 80;
|
|
104
|
+
const BASE_LIMITATION = 'External provider observations are attributed context, not proof that a target is safe, malicious, active, or controlled by any party.';
|
|
105
|
+
const NO_MATCH_LIMITATION = 'No matching provider record is not evidence that the target is safe.';
|
|
106
|
+
const CONNECTOR_BASE_LIMITATION = 'Connector observations and relationships are attributed investigation pivots, not proof of ownership, coordination, activity, safety, or maliciousness.';
|
|
107
|
+
const CONNECTOR_NO_MATCH_LIMITATION = 'No matching connector output is not evidence that an entity or relationship is absent or safe.';
|
|
108
|
+
function httpsUrl(value) {
|
|
109
|
+
return boundedHttpsUrl(value, MAX_THREAT_INTELLIGENCE_URL_LENGTH);
|
|
110
|
+
}
|
|
111
|
+
function connectorEntityExposure(type) {
|
|
112
|
+
if (type === 'domain')
|
|
113
|
+
return 'registrable_domain';
|
|
114
|
+
if (type === 'hostname')
|
|
115
|
+
return 'hostname';
|
|
116
|
+
if (type === 'url')
|
|
117
|
+
return 'full_url';
|
|
118
|
+
if (type === 'ipv4' || type === 'ipv6')
|
|
119
|
+
return 'ip_address';
|
|
120
|
+
if (type === 'asn')
|
|
121
|
+
return 'asn';
|
|
122
|
+
return 'certificate_fingerprint';
|
|
123
|
+
}
|
|
124
|
+
function stableHash(value, seed) {
|
|
125
|
+
let hash = seed >>> 0;
|
|
126
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
127
|
+
hash ^= value.charCodeAt(index);
|
|
128
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
129
|
+
}
|
|
130
|
+
return hash.toString(36);
|
|
131
|
+
}
|
|
132
|
+
function connectorStableId(prefix, canonical) {
|
|
133
|
+
return `${prefix}:${stableHash(canonical, 2166136261)}-${stableHash(canonical, 3339675911)}`;
|
|
134
|
+
}
|
|
135
|
+
function hasOnlyKeys(value, allowed) {
|
|
136
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
137
|
+
}
|
|
138
|
+
function normalizeConnectorAttributes(value) {
|
|
139
|
+
if (value == null)
|
|
140
|
+
return { values: {}, discarded: 0 };
|
|
141
|
+
if (!isRecord(value))
|
|
142
|
+
return { values: {}, discarded: 1 };
|
|
143
|
+
const output = {};
|
|
144
|
+
const inputKeys = Object.keys(value).sort();
|
|
145
|
+
let discarded = Math.max(0, inputKeys.length - MAX_CONNECTOR_ATTRIBUTES * 2);
|
|
146
|
+
for (const key of inputKeys.slice(0, MAX_CONNECTOR_ATTRIBUTES * 2)) {
|
|
147
|
+
if (Object.keys(output).length >= MAX_CONNECTOR_ATTRIBUTES
|
|
148
|
+
|| key.length > 40
|
|
149
|
+
|| !/^[a-z0-9][a-z0-9_-]*$/iu.test(key)) {
|
|
150
|
+
discarded += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const raw = value[key];
|
|
154
|
+
if (typeof raw === 'boolean')
|
|
155
|
+
output[key] = raw;
|
|
156
|
+
else if (typeof raw === 'number' && Number.isFinite(raw))
|
|
157
|
+
output[key] = raw;
|
|
158
|
+
else {
|
|
159
|
+
const normalized = strictBoundedString(raw, MAX_CONNECTOR_ATTRIBUTE_LENGTH);
|
|
160
|
+
if (normalized !== null)
|
|
161
|
+
output[key] = normalized;
|
|
162
|
+
else
|
|
163
|
+
discarded += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { values: output, discarded };
|
|
167
|
+
}
|
|
168
|
+
function normalizeConnectorEntity(value, allowedTypes) {
|
|
169
|
+
if (!isRecord(value)
|
|
170
|
+
|| !hasOnlyKeys(value, new Set(['key', 'type', 'value', 'label', 'attributes'])))
|
|
171
|
+
return null;
|
|
172
|
+
const key = strictBoundedString(value.key, MAX_CONNECTOR_KEY_LENGTH);
|
|
173
|
+
const type = typeof value.type === 'string' && CURATED_CONNECTOR_ENTITY_TYPES.has(value.type)
|
|
174
|
+
? value.type
|
|
175
|
+
: null;
|
|
176
|
+
if (!key || !/^[a-z0-9][a-z0-9._:-]*$/iu.test(key) || !type || !allowedTypes.has(type))
|
|
177
|
+
return null;
|
|
178
|
+
let normalized;
|
|
179
|
+
try {
|
|
180
|
+
normalized = normalizeCuratedConnectorTarget({ type, value: value.value }, connectorEntityExposure(type));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const canonicalKey = `${type}:${normalized.value}`;
|
|
186
|
+
const attributes = normalizeConnectorAttributes(value.attributes);
|
|
187
|
+
return {
|
|
188
|
+
key,
|
|
189
|
+
canonicalKey,
|
|
190
|
+
discardedAttributes: attributes.discarded,
|
|
191
|
+
entity: {
|
|
192
|
+
id: connectorStableId('entity', canonicalKey),
|
|
193
|
+
type,
|
|
194
|
+
canonical: normalized.value,
|
|
195
|
+
label: boundedString(value.label, MAX_PROVIDER_LABEL_LENGTH) || normalized.value,
|
|
196
|
+
attributes: attributes.values,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function compareConnectorEntities(left, right) {
|
|
201
|
+
return left.entity.id.localeCompare(right.entity.id)
|
|
202
|
+
|| left.key.localeCompare(right.key)
|
|
203
|
+
|| JSON.stringify(left.entity).localeCompare(JSON.stringify(right.entity));
|
|
204
|
+
}
|
|
205
|
+
function normalizeConnectorLimitations(value) {
|
|
206
|
+
if (value == null)
|
|
207
|
+
return { values: [], discarded: 0 };
|
|
208
|
+
if (!Array.isArray(value))
|
|
209
|
+
return { values: [], discarded: 1 };
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
let discarded = Math.max(0, value.length - MAX_LIMITATIONS * 2);
|
|
212
|
+
for (const item of value.slice(0, MAX_LIMITATIONS * 2)) {
|
|
213
|
+
const normalized = strictBoundedString(item, MAX_DETAIL_LENGTH);
|
|
214
|
+
if (!normalized) {
|
|
215
|
+
discarded += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (seen.has(normalized))
|
|
219
|
+
continue;
|
|
220
|
+
if (seen.size >= MAX_LIMITATIONS) {
|
|
221
|
+
discarded += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
seen.add(normalized);
|
|
225
|
+
}
|
|
226
|
+
return { values: [...seen], discarded };
|
|
227
|
+
}
|
|
228
|
+
function normalizeConnectorRelationship(value, allowedTypes, entitiesByKey) {
|
|
229
|
+
if (!isRecord(value)
|
|
230
|
+
|| !hasOnlyKeys(value, new Set([
|
|
231
|
+
'type',
|
|
232
|
+
'fromKey',
|
|
233
|
+
'toKey',
|
|
234
|
+
'classification',
|
|
235
|
+
'method',
|
|
236
|
+
'firstObservedAt',
|
|
237
|
+
'lastObservedAt',
|
|
238
|
+
'complete',
|
|
239
|
+
'truncated',
|
|
240
|
+
'limitations',
|
|
241
|
+
])))
|
|
242
|
+
return null;
|
|
243
|
+
const type = typeof value.type === 'string'
|
|
244
|
+
&& CONNECTOR_RELATIONSHIP_TYPES.has(value.type)
|
|
245
|
+
&& allowedTypes.has(value.type)
|
|
246
|
+
? value.type
|
|
247
|
+
: null;
|
|
248
|
+
const fromKey = strictBoundedString(value.fromKey, MAX_CONNECTOR_KEY_LENGTH);
|
|
249
|
+
const toKey = strictBoundedString(value.toKey, MAX_CONNECTOR_KEY_LENGTH);
|
|
250
|
+
const fromEntity = fromKey ? entitiesByKey.get(fromKey) : null;
|
|
251
|
+
const toEntity = toKey ? entitiesByKey.get(toKey) : null;
|
|
252
|
+
const classification = typeof value.classification === 'string'
|
|
253
|
+
&& CONNECTOR_RELATIONSHIP_CLASSIFICATIONS.has(value.classification)
|
|
254
|
+
? value.classification
|
|
255
|
+
: null;
|
|
256
|
+
const method = strictBoundedString(value.method, MAX_CONNECTOR_METHOD_LENGTH);
|
|
257
|
+
const firstObservedAt = value.firstObservedAt == null ? null : isoTimestamp(value.firstObservedAt);
|
|
258
|
+
const lastObservedAt = value.lastObservedAt == null ? null : isoTimestamp(value.lastObservedAt);
|
|
259
|
+
const endpoints = type ? CONNECTOR_RELATIONSHIP_ENDPOINTS[type] : null;
|
|
260
|
+
if (!type || !fromEntity || !toEntity || fromEntity.id === toEntity.id
|
|
261
|
+
|| !endpoints?.from.has(fromEntity.type) || !endpoints.to.has(toEntity.type)
|
|
262
|
+
|| !classification || !method
|
|
263
|
+
|| (value.firstObservedAt != null && !firstObservedAt)
|
|
264
|
+
|| (value.lastObservedAt != null && !lastObservedAt)
|
|
265
|
+
|| (firstObservedAt && lastObservedAt && firstObservedAt > lastObservedAt)
|
|
266
|
+
|| (value.complete != null && typeof value.complete !== 'boolean')
|
|
267
|
+
|| (value.truncated != null && typeof value.truncated !== 'boolean'))
|
|
268
|
+
return null;
|
|
269
|
+
const canonical = JSON.stringify([type, fromEntity.id, toEntity.id, classification, method]);
|
|
270
|
+
const limitations = normalizeConnectorLimitations(value.limitations);
|
|
271
|
+
return {
|
|
272
|
+
canonical,
|
|
273
|
+
discardedMetadata: limitations.discarded,
|
|
274
|
+
relationship: {
|
|
275
|
+
id: connectorStableId('relationship', canonical),
|
|
276
|
+
type,
|
|
277
|
+
from: fromEntity.id,
|
|
278
|
+
to: toEntity.id,
|
|
279
|
+
classification,
|
|
280
|
+
method,
|
|
281
|
+
firstObservedAt,
|
|
282
|
+
lastObservedAt,
|
|
283
|
+
complete: typeof value.complete === 'boolean' ? value.complete : null,
|
|
284
|
+
truncated: typeof value.truncated === 'boolean' ? value.truncated : null,
|
|
285
|
+
limitations: limitations.values,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function compareConnectorRelationships(left, right) {
|
|
290
|
+
return left.relationship.type.localeCompare(right.relationship.type)
|
|
291
|
+
|| left.relationship.from.localeCompare(right.relationship.from)
|
|
292
|
+
|| left.relationship.to.localeCompare(right.relationship.to)
|
|
293
|
+
|| left.relationship.id.localeCompare(right.relationship.id)
|
|
294
|
+
|| JSON.stringify(left.relationship).localeCompare(JSON.stringify(right.relationship));
|
|
295
|
+
}
|
|
296
|
+
function normalizeTags(value) {
|
|
297
|
+
if (!Array.isArray(value))
|
|
298
|
+
return [];
|
|
299
|
+
return [...new Set(value
|
|
300
|
+
.slice(0, MAX_TAGS * 2)
|
|
301
|
+
.map((item) => boundedString(item, MAX_TAG_LENGTH))
|
|
302
|
+
.filter((item) => item !== null))]
|
|
303
|
+
.sort()
|
|
304
|
+
.slice(0, MAX_TAGS);
|
|
305
|
+
}
|
|
306
|
+
function normalizeFinding(value) {
|
|
307
|
+
if (!isRecord(value))
|
|
308
|
+
return null;
|
|
309
|
+
const category = typeof value.category === 'string' && CATEGORIES.has(value.category)
|
|
310
|
+
? value.category
|
|
311
|
+
: null;
|
|
312
|
+
if (!category)
|
|
313
|
+
return null;
|
|
314
|
+
const firstObservedAt = value.firstObservedAt == null ? null : isoTimestamp(value.firstObservedAt);
|
|
315
|
+
const lastObservedAt = value.lastObservedAt == null ? null : isoTimestamp(value.lastObservedAt);
|
|
316
|
+
if ((value.firstObservedAt != null && !firstObservedAt)
|
|
317
|
+
|| (value.lastObservedAt != null && !lastObservedAt)
|
|
318
|
+
|| (firstObservedAt && lastObservedAt && firstObservedAt > lastObservedAt))
|
|
319
|
+
return null;
|
|
320
|
+
const referenceUrl = value.referenceUrl == null ? null : httpsUrl(value.referenceUrl);
|
|
321
|
+
if (value.referenceUrl != null && !referenceUrl)
|
|
322
|
+
return null;
|
|
323
|
+
return {
|
|
324
|
+
id: strictBoundedString(value.id, MAX_FINDING_ID_LENGTH),
|
|
325
|
+
category,
|
|
326
|
+
severity: typeof value.severity === 'string' && SEVERITIES.has(value.severity)
|
|
327
|
+
? value.severity
|
|
328
|
+
: 'unknown',
|
|
329
|
+
confidence: typeof value.confidence === 'string' && CONFIDENCES.has(value.confidence)
|
|
330
|
+
? value.confidence
|
|
331
|
+
: 'unknown',
|
|
332
|
+
providerVerdict: boundedString(value.providerVerdict, MAX_VERDICT_LENGTH),
|
|
333
|
+
detail: boundedString(value.detail, MAX_DETAIL_LENGTH),
|
|
334
|
+
firstObservedAt,
|
|
335
|
+
lastObservedAt,
|
|
336
|
+
referenceUrl,
|
|
337
|
+
tags: normalizeTags(value.tags),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function findingKey(value) {
|
|
341
|
+
return value.id || JSON.stringify([
|
|
342
|
+
value.category,
|
|
343
|
+
value.severity,
|
|
344
|
+
value.providerVerdict,
|
|
345
|
+
value.firstObservedAt,
|
|
346
|
+
value.lastObservedAt,
|
|
347
|
+
value.referenceUrl,
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
function compareFindings(left, right) {
|
|
351
|
+
const timeOrder = (right.lastObservedAt || '').localeCompare(left.lastObservedAt || '');
|
|
352
|
+
if (timeOrder)
|
|
353
|
+
return timeOrder;
|
|
354
|
+
const keyOrder = findingKey(left).localeCompare(findingKey(right));
|
|
355
|
+
return keyOrder || JSON.stringify(left).localeCompare(JSON.stringify(right));
|
|
356
|
+
}
|
|
357
|
+
function normalizeLimitations(value) {
|
|
358
|
+
return [...new Set((Array.isArray(value) ? value : [])
|
|
359
|
+
.slice(0, MAX_LIMITATIONS * 2)
|
|
360
|
+
.map((item) => boundedString(item, MAX_DETAIL_LENGTH))
|
|
361
|
+
.filter((item) => item !== null))]
|
|
362
|
+
.slice(0, MAX_LIMITATIONS);
|
|
363
|
+
}
|
|
364
|
+
function createThreatIntelligenceResult(provider, target, input = {}, observedAt = new Date().toISOString()) {
|
|
365
|
+
assertThreatIntelligenceProvider(provider);
|
|
366
|
+
const targetType = isRecord(target) && (target.type === 'domain' || target.type === 'url')
|
|
367
|
+
? target.type
|
|
368
|
+
: null;
|
|
369
|
+
const exposure = targetType ? provider.targets[targetType] : undefined;
|
|
370
|
+
if (!exposure)
|
|
371
|
+
throw new TypeError('Provider does not support this target type');
|
|
372
|
+
const normalizedTarget = normalizeThreatIntelligenceTarget(target, exposure);
|
|
373
|
+
const resultInput = isRecord(input) ? input : {};
|
|
374
|
+
const requestedState = typeof resultInput.state === 'string'
|
|
375
|
+
&& RESULT_STATES.has(resultInput.state)
|
|
376
|
+
? resultInput.state
|
|
377
|
+
: 'error';
|
|
378
|
+
const rawFindings = Array.isArray(resultInput.findings)
|
|
379
|
+
? resultInput.findings.slice(0, MAX_INPUT_FINDINGS)
|
|
380
|
+
: [];
|
|
381
|
+
const normalizedFindings = [];
|
|
382
|
+
let discarded = Array.isArray(resultInput.findings)
|
|
383
|
+
? Math.max(0, resultInput.findings.length - MAX_INPUT_FINDINGS)
|
|
384
|
+
: 0;
|
|
385
|
+
for (const item of rawFindings) {
|
|
386
|
+
const finding = normalizeFinding(item);
|
|
387
|
+
if (!finding) {
|
|
388
|
+
discarded += 1;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
normalizedFindings.push(finding);
|
|
392
|
+
}
|
|
393
|
+
normalizedFindings.sort(compareFindings);
|
|
394
|
+
const findings = [];
|
|
395
|
+
const seen = new Set();
|
|
396
|
+
for (const finding of normalizedFindings) {
|
|
397
|
+
const key = findingKey(finding);
|
|
398
|
+
if (seen.has(key))
|
|
399
|
+
continue;
|
|
400
|
+
seen.add(key);
|
|
401
|
+
findings.push(finding);
|
|
402
|
+
}
|
|
403
|
+
if (findings.length > MAX_FINDINGS) {
|
|
404
|
+
discarded += findings.length - MAX_FINDINGS;
|
|
405
|
+
findings.length = MAX_FINDINGS;
|
|
406
|
+
}
|
|
407
|
+
let state = requestedState;
|
|
408
|
+
const sourceTruncated = resultInput.truncated === true;
|
|
409
|
+
if (TERMINAL_STATES_WITHOUT_FINDINGS.has(state) && findings.length)
|
|
410
|
+
state = 'partial';
|
|
411
|
+
if (state === 'success' && findings.length === 0)
|
|
412
|
+
state = 'error';
|
|
413
|
+
if ((discarded > 0 || sourceTruncated) && !['error', 'unavailable', 'rate_limited'].includes(state))
|
|
414
|
+
state = 'partial';
|
|
415
|
+
const limitations = normalizeLimitations(resultInput.limitations);
|
|
416
|
+
if (state === 'not_found')
|
|
417
|
+
limitations.unshift(NO_MATCH_LIMITATION);
|
|
418
|
+
if (discarded > 0)
|
|
419
|
+
limitations.unshift(`${discarded} invalid or over-limit provider finding${discarded === 1 ? ' was' : 's were'} omitted.`);
|
|
420
|
+
limitations.unshift(BASE_LIMITATION);
|
|
421
|
+
const boundedLimitations = [...new Set(limitations)].slice(0, MAX_LIMITATIONS);
|
|
422
|
+
const complete = ['success', 'not_found'].includes(state) && discarded === 0 && !sourceTruncated;
|
|
423
|
+
const observationStatus = state === 'rate_limited' || state === 'unavailable'
|
|
424
|
+
? 'error'
|
|
425
|
+
: state;
|
|
426
|
+
const upstreamStatus = typeof resultInput.upstreamStatus === 'number'
|
|
427
|
+
&& Number.isInteger(resultInput.upstreamStatus)
|
|
428
|
+
&& resultInput.upstreamStatus >= 100
|
|
429
|
+
&& resultInput.upstreamStatus <= 599
|
|
430
|
+
? resultInput.upstreamStatus
|
|
431
|
+
: null;
|
|
432
|
+
const retryAfterSeconds = typeof resultInput.retryAfterSeconds === 'number'
|
|
433
|
+
&& Number.isInteger(resultInput.retryAfterSeconds)
|
|
434
|
+
&& resultInput.retryAfterSeconds >= 1
|
|
435
|
+
&& resultInput.retryAfterSeconds <= 86_400
|
|
436
|
+
? resultInput.retryAfterSeconds
|
|
437
|
+
: null;
|
|
438
|
+
return {
|
|
439
|
+
schema: THREAT_INTELLIGENCE_SCHEMA,
|
|
440
|
+
version: THREAT_INTELLIGENCE_CONTRACT_VERSION,
|
|
441
|
+
provider: { id: provider.id, label: provider.label },
|
|
442
|
+
target: normalizedTarget,
|
|
443
|
+
state,
|
|
444
|
+
detail: boundedString(resultInput.detail, MAX_DETAIL_LENGTH),
|
|
445
|
+
upstreamStatus,
|
|
446
|
+
retryAfterSeconds,
|
|
447
|
+
findings,
|
|
448
|
+
observation: createObservation({
|
|
449
|
+
status: observationStatus,
|
|
450
|
+
observedAt: isoTimestamp(observedAt) || new Date().toISOString(),
|
|
451
|
+
source: provider.id,
|
|
452
|
+
complete,
|
|
453
|
+
truncated: discarded > 0 || sourceTruncated,
|
|
454
|
+
limitations: boundedLimitations,
|
|
455
|
+
diagnostics: { discarded },
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function createCuratedConnectorResult(connector, target, input = {}, observedAt = new Date().toISOString()) {
|
|
460
|
+
assertCuratedConnectorDefinition(connector);
|
|
461
|
+
const normalizedObservedAt = isoTimestamp(observedAt);
|
|
462
|
+
if (!normalizedObservedAt)
|
|
463
|
+
throw new TypeError('Connector observation timestamp is invalid');
|
|
464
|
+
const targetType = isRecord(target)
|
|
465
|
+
&& typeof target.type === 'string'
|
|
466
|
+
&& CURATED_CONNECTOR_ENTITY_TYPES.has(target.type)
|
|
467
|
+
? target.type
|
|
468
|
+
: null;
|
|
469
|
+
const inputDeclaration = targetType
|
|
470
|
+
? connector.inputs.find((item) => item.type === targetType)
|
|
471
|
+
: null;
|
|
472
|
+
if (!inputDeclaration)
|
|
473
|
+
throw new TypeError('Connector does not support this target type');
|
|
474
|
+
const normalizedTarget = normalizeCuratedConnectorTarget(target, inputDeclaration.exposure);
|
|
475
|
+
const resultInput = isRecord(input) ? input : {};
|
|
476
|
+
const requestedState = typeof resultInput.state === 'string'
|
|
477
|
+
&& RESULT_STATES.has(resultInput.state)
|
|
478
|
+
? resultInput.state
|
|
479
|
+
: 'error';
|
|
480
|
+
const rawEntities = Array.isArray(resultInput.entities)
|
|
481
|
+
? resultInput.entities.slice(0, MAX_CONNECTOR_INPUT_ENTITIES)
|
|
482
|
+
: [];
|
|
483
|
+
let discardedEntities = Array.isArray(resultInput.entities)
|
|
484
|
+
? Math.max(0, resultInput.entities.length - MAX_CONNECTOR_INPUT_ENTITIES)
|
|
485
|
+
: 0;
|
|
486
|
+
let discardedAttributes = 0;
|
|
487
|
+
const entityCandidates = [];
|
|
488
|
+
const allowedEntityTypes = new Set(connector.outputs.entities);
|
|
489
|
+
for (const item of rawEntities) {
|
|
490
|
+
const candidate = normalizeConnectorEntity(item, allowedEntityTypes);
|
|
491
|
+
if (!candidate) {
|
|
492
|
+
discardedEntities += 1;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
discardedAttributes += candidate.discardedAttributes;
|
|
496
|
+
entityCandidates.push(candidate);
|
|
497
|
+
}
|
|
498
|
+
entityCandidates.sort(compareConnectorEntities);
|
|
499
|
+
const entitiesByCanonical = new Map();
|
|
500
|
+
const entityCanonicalById = new Map();
|
|
501
|
+
const entitiesByKey = new Map();
|
|
502
|
+
const seenEntityKeys = new Set();
|
|
503
|
+
for (const candidate of entityCandidates) {
|
|
504
|
+
if (seenEntityKeys.has(candidate.key)) {
|
|
505
|
+
discardedEntities += 1;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
seenEntityKeys.add(candidate.key);
|
|
509
|
+
const retained = entitiesByCanonical.get(candidate.canonicalKey);
|
|
510
|
+
if (retained) {
|
|
511
|
+
entitiesByKey.set(candidate.key, { id: retained.id, type: retained.type });
|
|
512
|
+
if (JSON.stringify(retained) !== JSON.stringify(candidate.entity))
|
|
513
|
+
discardedEntities += 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (entityCanonicalById.has(candidate.entity.id)
|
|
517
|
+
|| entitiesByCanonical.size >= connector.limits.maxEntities) {
|
|
518
|
+
discardedEntities += 1;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
entitiesByCanonical.set(candidate.canonicalKey, candidate.entity);
|
|
522
|
+
entityCanonicalById.set(candidate.entity.id, candidate.canonicalKey);
|
|
523
|
+
entitiesByKey.set(candidate.key, { id: candidate.entity.id, type: candidate.entity.type });
|
|
524
|
+
}
|
|
525
|
+
const entities = [...entitiesByCanonical.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
526
|
+
const rawRelationships = Array.isArray(resultInput.relationships)
|
|
527
|
+
? resultInput.relationships.slice(0, MAX_CONNECTOR_INPUT_RELATIONSHIPS)
|
|
528
|
+
: [];
|
|
529
|
+
let discardedRelationships = Array.isArray(resultInput.relationships)
|
|
530
|
+
? Math.max(0, resultInput.relationships.length - MAX_CONNECTOR_INPUT_RELATIONSHIPS)
|
|
531
|
+
: 0;
|
|
532
|
+
const relationshipCandidates = [];
|
|
533
|
+
let discardedRelationshipMetadata = 0;
|
|
534
|
+
const allowedRelationshipTypes = new Set(connector.outputs.relationships);
|
|
535
|
+
for (const item of rawRelationships) {
|
|
536
|
+
const relationship = normalizeConnectorRelationship(item, allowedRelationshipTypes, entitiesByKey);
|
|
537
|
+
if (!relationship) {
|
|
538
|
+
discardedRelationships += 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
discardedRelationshipMetadata += relationship.discardedMetadata;
|
|
542
|
+
relationshipCandidates.push(relationship);
|
|
543
|
+
}
|
|
544
|
+
relationshipCandidates.sort(compareConnectorRelationships);
|
|
545
|
+
const relationships = [];
|
|
546
|
+
const relationshipsById = new Map();
|
|
547
|
+
for (const candidate of relationshipCandidates) {
|
|
548
|
+
const { relationship } = candidate;
|
|
549
|
+
const retained = relationshipsById.get(relationship.id);
|
|
550
|
+
if (retained?.canonical === candidate.canonical) {
|
|
551
|
+
if (JSON.stringify(retained.relationship) !== JSON.stringify(relationship))
|
|
552
|
+
discardedRelationships += 1;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (retained) {
|
|
556
|
+
discardedRelationships += 1;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (relationships.length >= connector.limits.maxRelationships) {
|
|
560
|
+
discardedRelationships += 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
relationshipsById.set(relationship.id, candidate);
|
|
564
|
+
relationships.push(relationship);
|
|
565
|
+
}
|
|
566
|
+
let state = requestedState;
|
|
567
|
+
const sourceTruncated = resultInput.truncated === true;
|
|
568
|
+
const relationshipPartial = relationships.some((item) => item.complete === false || item.truncated === true);
|
|
569
|
+
const hasOutput = entities.length > 0 || relationships.length > 0;
|
|
570
|
+
if (TERMINAL_STATES_WITHOUT_FINDINGS.has(state) && hasOutput)
|
|
571
|
+
state = 'partial';
|
|
572
|
+
if (state === 'success' && !hasOutput)
|
|
573
|
+
state = 'error';
|
|
574
|
+
const resultLimitations = normalizeConnectorLimitations(resultInput.limitations);
|
|
575
|
+
discardedRelationshipMetadata += resultLimitations.discarded;
|
|
576
|
+
if ((discardedEntities > 0
|
|
577
|
+
|| discardedAttributes > 0
|
|
578
|
+
|| discardedRelationships > 0
|
|
579
|
+
|| discardedRelationshipMetadata > 0
|
|
580
|
+
|| sourceTruncated
|
|
581
|
+
|| relationshipPartial)
|
|
582
|
+
&& !['error', 'unavailable', 'rate_limited'].includes(state))
|
|
583
|
+
state = 'partial';
|
|
584
|
+
const limitations = resultLimitations.values;
|
|
585
|
+
if (state === 'not_found')
|
|
586
|
+
limitations.unshift(CONNECTOR_NO_MATCH_LIMITATION);
|
|
587
|
+
if (discardedRelationships > 0) {
|
|
588
|
+
limitations.unshift(`${discardedRelationships} invalid, dangling, or over-limit connector relationship${discardedRelationships === 1 ? ' was' : 's were'} omitted.`);
|
|
589
|
+
}
|
|
590
|
+
if (discardedEntities > 0) {
|
|
591
|
+
limitations.unshift(`${discardedEntities} invalid, duplicate-key, hash-colliding, or over-limit connector entit${discardedEntities === 1 ? 'y was' : 'ies were'} omitted.`);
|
|
592
|
+
}
|
|
593
|
+
if (discardedAttributes > 0) {
|
|
594
|
+
limitations.unshift(`${discardedAttributes} invalid or over-limit connector attribute value${discardedAttributes === 1 ? ' was' : 's were'} omitted.`);
|
|
595
|
+
}
|
|
596
|
+
if (discardedRelationshipMetadata > 0) {
|
|
597
|
+
limitations.unshift(`${discardedRelationshipMetadata} invalid or over-limit connector limitation value${discardedRelationshipMetadata === 1 ? ' was' : 's were'} omitted.`);
|
|
598
|
+
}
|
|
599
|
+
limitations.unshift(CONNECTOR_BASE_LIMITATION);
|
|
600
|
+
const boundedLimitations = [...new Set(limitations)].slice(0, MAX_LIMITATIONS);
|
|
601
|
+
const complete = ['success', 'not_found'].includes(state)
|
|
602
|
+
&& discardedEntities === 0
|
|
603
|
+
&& discardedAttributes === 0
|
|
604
|
+
&& discardedRelationships === 0
|
|
605
|
+
&& discardedRelationshipMetadata === 0
|
|
606
|
+
&& !sourceTruncated
|
|
607
|
+
&& !relationshipPartial;
|
|
608
|
+
const observationStatus = state === 'rate_limited' || state === 'unavailable'
|
|
609
|
+
? 'error'
|
|
610
|
+
: state;
|
|
611
|
+
const upstreamStatus = typeof resultInput.upstreamStatus === 'number'
|
|
612
|
+
&& Number.isInteger(resultInput.upstreamStatus)
|
|
613
|
+
&& resultInput.upstreamStatus >= 100
|
|
614
|
+
&& resultInput.upstreamStatus <= 599
|
|
615
|
+
? resultInput.upstreamStatus
|
|
616
|
+
: null;
|
|
617
|
+
const retryAfterSeconds = typeof resultInput.retryAfterSeconds === 'number'
|
|
618
|
+
&& Number.isInteger(resultInput.retryAfterSeconds)
|
|
619
|
+
&& resultInput.retryAfterSeconds >= 1
|
|
620
|
+
&& resultInput.retryAfterSeconds <= 86_400
|
|
621
|
+
? resultInput.retryAfterSeconds
|
|
622
|
+
: null;
|
|
623
|
+
return {
|
|
624
|
+
schema: CURATED_CONNECTOR_RESULT_SCHEMA,
|
|
625
|
+
version: CURATED_CONNECTOR_CONTRACT_VERSION,
|
|
626
|
+
connector: {
|
|
627
|
+
id: connector.id,
|
|
628
|
+
label: connector.label,
|
|
629
|
+
kinds: [...connector.kinds],
|
|
630
|
+
collection: connector.collection,
|
|
631
|
+
},
|
|
632
|
+
target: normalizedTarget,
|
|
633
|
+
state,
|
|
634
|
+
detail: boundedString(resultInput.detail, MAX_DETAIL_LENGTH),
|
|
635
|
+
upstreamStatus,
|
|
636
|
+
retryAfterSeconds,
|
|
637
|
+
entities,
|
|
638
|
+
relationships,
|
|
639
|
+
observation: createObservation({
|
|
640
|
+
status: observationStatus,
|
|
641
|
+
observedAt: normalizedObservedAt,
|
|
642
|
+
source: connector.id,
|
|
643
|
+
complete,
|
|
644
|
+
truncated: discardedEntities > 0
|
|
645
|
+
|| discardedAttributes > 0
|
|
646
|
+
|| discardedRelationships > 0
|
|
647
|
+
|| discardedRelationshipMetadata > 0
|
|
648
|
+
|| sourceTruncated
|
|
649
|
+
|| relationships.some((item) => item.truncated === true),
|
|
650
|
+
limitations: boundedLimitations,
|
|
651
|
+
diagnostics: {
|
|
652
|
+
entities: entities.length,
|
|
653
|
+
relationships: relationships.length,
|
|
654
|
+
discarded_entities: discardedEntities,
|
|
655
|
+
discarded_attributes: discardedAttributes,
|
|
656
|
+
discarded_relationships: discardedRelationships,
|
|
657
|
+
discarded_metadata: discardedRelationshipMetadata,
|
|
658
|
+
},
|
|
659
|
+
}),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
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, };
|