@slicedearth/whoisleuth-cli 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { registerCuratedConnector, registerThreatIntelligenceProvider, isCuratedConnectorDefinition, isThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
|
|
2
2
|
import { CURATED_CONNECTOR_CONTRACT_VERSION, MAX_CACHE_TTL_MS, MAX_CONNECTOR_ENTITIES, MAX_CONNECTOR_RELATIONSHIPS, MAX_RESPONSE_BYTES, MAX_TIMEOUT_MS, THREAT_INTELLIGENCE_CONTRACT_VERSION, } from "./threat-intelligence-runtime.mjs";
|
|
3
|
+
import { boundedInteger, boundedHttpsUrl, enumValue, exactKeys, isRecord, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
|
|
4
|
+
import { THREAT_INTELLIGENCE_CAPABILITIES, THREAT_INTELLIGENCE_TARGET_EXPOSURES, CURATED_CONNECTOR_KINDS, CURATED_CONNECTOR_COLLECTIONS, CURATED_CONNECTOR_CREDENTIAL_MODES, CURATED_CONNECTOR_ENTITY_VALUES, CURATED_CONNECTOR_RELATIONSHIP_TYPES, CURATED_CONNECTOR_TARGET_EXPOSURES, CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS as CONNECTOR_RELATIONSHIP_ENDPOINTS, } from "./threat-intelligence-types.mjs";
|
|
3
5
|
import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
4
6
|
const MAX_PROVIDER_ID_LENGTH = 40;
|
|
5
7
|
const MAX_PROVIDER_LABEL_LENGTH = 100;
|
|
@@ -7,17 +9,8 @@ const MAX_URL_LENGTH = 2_048;
|
|
|
7
9
|
const MAX_REQUEST_BUDGET = 1_000_000;
|
|
8
10
|
const MAX_CONNECTOR_SCOPES = 20;
|
|
9
11
|
const MAX_CONNECTOR_SCOPE_LENGTH = 80;
|
|
10
|
-
const CAPABILITIES = new Set(
|
|
11
|
-
|
|
12
|
-
'url_lookup',
|
|
13
|
-
'indicator_search',
|
|
14
|
-
]);
|
|
15
|
-
const URL_EXPOSURES = new Set([
|
|
16
|
-
'registrable_domain',
|
|
17
|
-
'hostname',
|
|
18
|
-
'origin',
|
|
19
|
-
'full_url',
|
|
20
|
-
]);
|
|
12
|
+
const CAPABILITIES = new Set(THREAT_INTELLIGENCE_CAPABILITIES);
|
|
13
|
+
const URL_EXPOSURES = new Set(THREAT_INTELLIGENCE_TARGET_EXPOSURES);
|
|
21
14
|
const COMMERCIAL_USE = new Set(['allowed', 'restricted', 'unknown']);
|
|
22
15
|
const ATTRIBUTION = new Set(['required', 'not_required', 'unknown']);
|
|
23
16
|
const CACHING = new Set([
|
|
@@ -39,137 +32,16 @@ const REDISTRIBUTION = new Set([
|
|
|
39
32
|
'prohibited',
|
|
40
33
|
'unknown',
|
|
41
34
|
]);
|
|
42
|
-
const CONNECTOR_KINDS = new Set(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
'passive',
|
|
48
|
-
'active',
|
|
49
|
-
'third_party',
|
|
50
|
-
]);
|
|
51
|
-
const CONNECTOR_CREDENTIAL_MODES = new Set([
|
|
52
|
-
'none',
|
|
53
|
-
'optional',
|
|
54
|
-
'required',
|
|
55
|
-
]);
|
|
56
|
-
const CONNECTOR_ENTITY_TYPES = new Set([
|
|
57
|
-
'domain',
|
|
58
|
-
'hostname',
|
|
59
|
-
'url',
|
|
60
|
-
'ipv4',
|
|
61
|
-
'ipv6',
|
|
62
|
-
'asn',
|
|
63
|
-
'certificate',
|
|
64
|
-
]);
|
|
65
|
-
const CONNECTOR_RELATIONSHIP_TYPES = new Set([
|
|
66
|
-
'domain_resolves_to_ip',
|
|
67
|
-
'domain_uses_nameserver',
|
|
68
|
-
'domain_uses_mail_server',
|
|
69
|
-
'domain_presented_certificate',
|
|
70
|
-
'certificate_names_domain',
|
|
71
|
-
'ip_hosts_domain',
|
|
72
|
-
'domain_related_to_domain',
|
|
73
|
-
]);
|
|
74
|
-
const CONNECTOR_TARGET_EXPOSURES = Object.freeze({
|
|
75
|
-
domain: new Set(['registrable_domain']),
|
|
76
|
-
hostname: new Set(['hostname']),
|
|
77
|
-
url: new Set([
|
|
78
|
-
'registrable_domain',
|
|
79
|
-
'hostname',
|
|
80
|
-
'origin',
|
|
81
|
-
'full_url',
|
|
82
|
-
]),
|
|
83
|
-
ipv4: new Set(['ip_address']),
|
|
84
|
-
ipv6: new Set(['ip_address']),
|
|
85
|
-
asn: new Set(['asn']),
|
|
86
|
-
certificate: new Set([
|
|
87
|
-
'certificate_fingerprint',
|
|
88
|
-
]),
|
|
89
|
-
});
|
|
90
|
-
const CONNECTOR_RELATIONSHIP_ENDPOINTS = Object.freeze({
|
|
91
|
-
domain_resolves_to_ip: {
|
|
92
|
-
from: new Set(['domain', 'hostname']),
|
|
93
|
-
to: new Set(['ipv4', 'ipv6']),
|
|
94
|
-
},
|
|
95
|
-
domain_uses_nameserver: {
|
|
96
|
-
from: new Set(['domain']),
|
|
97
|
-
to: new Set(['hostname']),
|
|
98
|
-
},
|
|
99
|
-
domain_uses_mail_server: {
|
|
100
|
-
from: new Set(['domain']),
|
|
101
|
-
to: new Set(['hostname']),
|
|
102
|
-
},
|
|
103
|
-
domain_presented_certificate: {
|
|
104
|
-
from: new Set(['domain', 'hostname']),
|
|
105
|
-
to: new Set(['certificate']),
|
|
106
|
-
},
|
|
107
|
-
certificate_names_domain: {
|
|
108
|
-
from: new Set(['certificate']),
|
|
109
|
-
to: new Set(['domain', 'hostname']),
|
|
110
|
-
},
|
|
111
|
-
ip_hosts_domain: {
|
|
112
|
-
from: new Set(['ipv4', 'ipv6']),
|
|
113
|
-
to: new Set(['domain', 'hostname']),
|
|
114
|
-
},
|
|
115
|
-
domain_related_to_domain: {
|
|
116
|
-
from: new Set(['domain', 'hostname']),
|
|
117
|
-
to: new Set(['domain', 'hostname']),
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
function isRecord(value) {
|
|
121
|
-
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
122
|
-
}
|
|
123
|
-
function exactKeys(value, allowed, label) {
|
|
124
|
-
if (!isRecord(value))
|
|
125
|
-
throw new TypeError(`${label} must be an object`);
|
|
126
|
-
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
127
|
-
if (unknown) {
|
|
128
|
-
throw new TypeError(`${label} contains an unknown field: ${unknown}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function strictBoundedString(value, maximum) {
|
|
132
|
-
if (typeof value !== 'string' ||
|
|
133
|
-
value.length > maximum ||
|
|
134
|
-
/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
const normalized = value.trim().replace(/\s+/gu, ' ');
|
|
138
|
-
return normalized && normalized.length <= maximum ? normalized : null;
|
|
139
|
-
}
|
|
35
|
+
const CONNECTOR_KINDS = new Set(CURATED_CONNECTOR_KINDS);
|
|
36
|
+
const CONNECTOR_COLLECTIONS = new Set(CURATED_CONNECTOR_COLLECTIONS);
|
|
37
|
+
const CONNECTOR_CREDENTIAL_MODES = new Set(CURATED_CONNECTOR_CREDENTIAL_MODES);
|
|
38
|
+
const CONNECTOR_ENTITY_TYPES = new Set(CURATED_CONNECTOR_ENTITY_VALUES);
|
|
39
|
+
const CONNECTOR_RELATIONSHIP_TYPES = new Set(CURATED_CONNECTOR_RELATIONSHIP_TYPES);
|
|
140
40
|
function isoTimestamp(value) {
|
|
141
41
|
return normalizeLegacyIsoTimestamp(value);
|
|
142
42
|
}
|
|
143
43
|
function httpsUrl(value) {
|
|
144
|
-
|
|
145
|
-
if (!raw)
|
|
146
|
-
return null;
|
|
147
|
-
try {
|
|
148
|
-
const parsed = new URL(raw);
|
|
149
|
-
if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
const normalized = parsed.toString();
|
|
153
|
-
return normalized.length <= MAX_URL_LENGTH ? normalized : null;
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
function enumValue(value, allowed, label) {
|
|
160
|
-
if (typeof value !== 'string' || !allowed.has(value)) {
|
|
161
|
-
throw new TypeError(`${label} is invalid`);
|
|
162
|
-
}
|
|
163
|
-
return value;
|
|
164
|
-
}
|
|
165
|
-
function boundedInteger(value, minimum, maximum, label) {
|
|
166
|
-
if (typeof value !== 'number' ||
|
|
167
|
-
!Number.isSafeInteger(value) ||
|
|
168
|
-
value < minimum ||
|
|
169
|
-
value > maximum) {
|
|
170
|
-
throw new TypeError(`${label} must be an integer between ${minimum} and ${maximum}`);
|
|
171
|
-
}
|
|
172
|
-
return value;
|
|
44
|
+
return boundedHttpsUrl(value, MAX_URL_LENGTH);
|
|
173
45
|
}
|
|
174
46
|
function normalizeTargets(value) {
|
|
175
47
|
exactKeys(value, new Set(['domain', 'url']), 'Provider targets');
|
|
@@ -323,7 +195,7 @@ function normalizeConnectorInputs(value) {
|
|
|
323
195
|
const inputs = value.map((item) => {
|
|
324
196
|
exactKeys(item, new Set(['type', 'exposure']), 'Connector input');
|
|
325
197
|
const type = enumValue(item.type, CONNECTOR_ENTITY_TYPES, 'Connector input type');
|
|
326
|
-
const exposure = enumValue(item.exposure,
|
|
198
|
+
const exposure = enumValue(item.exposure, new Set(CURATED_CONNECTOR_TARGET_EXPOSURES[type]), 'Connector input exposure');
|
|
327
199
|
if (seen.has(type)) {
|
|
328
200
|
throw new TypeError('Connector input types must be unique');
|
|
329
201
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// must declare exact input exposure, reviewed terms, and bounded request and
|
|
5
5
|
// output budgets before producing separately attributed normalized evidence.
|
|
6
6
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { latestObservationCohort } from "../packages/evidence/latest-observations.mjs";
|
|
8
|
+
import { CURATED_CONNECTOR_RELATIONSHIP_TYPES, CURATED_CONNECTOR_RELATIONSHIP_CLASSIFICATIONS, CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS as CONNECTOR_RELATIONSHIP_ENDPOINTS, THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "./threat-intelligence-types.mjs";
|
|
8
9
|
import { assertCuratedConnectorDefinition, assertThreatIntelligenceProvider, } from "./threat-intelligence-definition-registry.mjs";
|
|
9
10
|
import { boundedHttpsUrl, boundedString, isRecord, isoTimestamp, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
|
|
10
11
|
import { CURATED_CONNECTOR_ENTITY_TYPES, MAX_THREAT_INTELLIGENCE_URL_LENGTH, normalizeCuratedConnectorTarget, normalizeThreatIntelligenceTarget, } from "./threat-intelligence-targets.mjs";
|
|
@@ -22,50 +23,8 @@ const TERMINAL_STATES_WITHOUT_FINDINGS = new Set([
|
|
|
22
23
|
const CATEGORIES = new Set(THREAT_INTELLIGENCE_CATEGORIES);
|
|
23
24
|
const SEVERITIES = new Set(THREAT_INTELLIGENCE_SEVERITIES);
|
|
24
25
|
const CONFIDENCES = new Set(THREAT_INTELLIGENCE_CONFIDENCES);
|
|
25
|
-
const CONNECTOR_RELATIONSHIP_TYPES = new Set(
|
|
26
|
-
|
|
27
|
-
'domain_uses_nameserver',
|
|
28
|
-
'domain_uses_mail_server',
|
|
29
|
-
'domain_presented_certificate',
|
|
30
|
-
'certificate_names_domain',
|
|
31
|
-
'ip_hosts_domain',
|
|
32
|
-
'domain_related_to_domain',
|
|
33
|
-
]);
|
|
34
|
-
const CONNECTOR_RELATIONSHIP_CLASSIFICATIONS = new Set([
|
|
35
|
-
'direct',
|
|
36
|
-
'normalized',
|
|
37
|
-
'derived',
|
|
38
|
-
]);
|
|
39
|
-
const CONNECTOR_RELATIONSHIP_ENDPOINTS = Object.freeze({
|
|
40
|
-
domain_resolves_to_ip: {
|
|
41
|
-
from: new Set(['domain', 'hostname']),
|
|
42
|
-
to: new Set(['ipv4', 'ipv6']),
|
|
43
|
-
},
|
|
44
|
-
domain_uses_nameserver: {
|
|
45
|
-
from: new Set(['domain']),
|
|
46
|
-
to: new Set(['hostname']),
|
|
47
|
-
},
|
|
48
|
-
domain_uses_mail_server: {
|
|
49
|
-
from: new Set(['domain']),
|
|
50
|
-
to: new Set(['hostname']),
|
|
51
|
-
},
|
|
52
|
-
domain_presented_certificate: {
|
|
53
|
-
from: new Set(['domain', 'hostname']),
|
|
54
|
-
to: new Set(['certificate']),
|
|
55
|
-
},
|
|
56
|
-
certificate_names_domain: {
|
|
57
|
-
from: new Set(['certificate']),
|
|
58
|
-
to: new Set(['domain', 'hostname']),
|
|
59
|
-
},
|
|
60
|
-
ip_hosts_domain: {
|
|
61
|
-
from: new Set(['ipv4', 'ipv6']),
|
|
62
|
-
to: new Set(['domain', 'hostname']),
|
|
63
|
-
},
|
|
64
|
-
domain_related_to_domain: {
|
|
65
|
-
from: new Set(['domain', 'hostname']),
|
|
66
|
-
to: new Set(['domain', 'hostname']),
|
|
67
|
-
},
|
|
68
|
-
});
|
|
26
|
+
const CONNECTOR_RELATIONSHIP_TYPES = new Set(CURATED_CONNECTOR_RELATIONSHIP_TYPES);
|
|
27
|
+
const CONNECTOR_RELATIONSHIP_CLASSIFICATIONS = new Set(CURATED_CONNECTOR_RELATIONSHIP_CLASSIFICATIONS);
|
|
69
28
|
// createObservation() retains at most 40 source characters, so provider IDs use
|
|
70
29
|
// the same ceiling and can never be truncated at the provenance boundary.
|
|
71
30
|
const MAX_PROVIDER_LABEL_LENGTH = 100;
|
|
@@ -247,7 +206,7 @@ function normalizeConnectorRelationship(value, allowedTypes, entitiesByKey) {
|
|
|
247
206
|
const lastObservedAt = value.lastObservedAt == null ? null : isoTimestamp(value.lastObservedAt);
|
|
248
207
|
const endpoints = type ? CONNECTOR_RELATIONSHIP_ENDPOINTS[type] : null;
|
|
249
208
|
if (!type || !fromEntity || !toEntity || fromEntity.id === toEntity.id
|
|
250
|
-
|| !endpoints?.from.
|
|
209
|
+
|| !endpoints?.from.includes(fromEntity.type) || !endpoints.to.includes(toEntity.type)
|
|
251
210
|
|| !classification || !method
|
|
252
211
|
|| (value.firstObservedAt != null && !firstObservedAt)
|
|
253
212
|
|| (value.lastObservedAt != null && !lastObservedAt)
|
|
@@ -381,13 +340,24 @@ function createThreatIntelligenceResult(provider, target, input = {}, observedAt
|
|
|
381
340
|
}
|
|
382
341
|
normalizedFindings.sort(compareFindings);
|
|
383
342
|
const findings = [];
|
|
384
|
-
const
|
|
343
|
+
const byIdentity = new Map();
|
|
385
344
|
for (const finding of normalizedFindings) {
|
|
386
345
|
const key = findingKey(finding);
|
|
387
|
-
|
|
346
|
+
const group = byIdentity.get(key) ?? [];
|
|
347
|
+
group.push(finding);
|
|
348
|
+
byIdentity.set(key, group);
|
|
349
|
+
}
|
|
350
|
+
let conflictingIdentities = 0;
|
|
351
|
+
for (const group of byIdentity.values()) {
|
|
352
|
+
const cohort = latestObservationCohort(group, (finding) => finding.lastObservedAt);
|
|
353
|
+
const candidates = [...cohort.latest, ...cohort.undated];
|
|
354
|
+
if (new Set(candidates.map((finding) => JSON.stringify(finding))).size > 1) {
|
|
355
|
+
conflictingIdentities += 1;
|
|
356
|
+
discarded += candidates.length;
|
|
388
357
|
continue;
|
|
389
|
-
|
|
390
|
-
|
|
358
|
+
}
|
|
359
|
+
if (candidates[0])
|
|
360
|
+
findings.push(candidates[0]);
|
|
391
361
|
}
|
|
392
362
|
if (findings.length > MAX_FINDINGS) {
|
|
393
363
|
discarded += findings.length - MAX_FINDINGS;
|
|
@@ -398,14 +368,16 @@ function createThreatIntelligenceResult(provider, target, input = {}, observedAt
|
|
|
398
368
|
if (TERMINAL_STATES_WITHOUT_FINDINGS.has(state) && findings.length)
|
|
399
369
|
state = 'partial';
|
|
400
370
|
if (state === 'success' && findings.length === 0)
|
|
401
|
-
state = 'error';
|
|
371
|
+
state = conflictingIdentities > 0 ? 'partial' : 'error';
|
|
402
372
|
if ((discarded > 0 || sourceTruncated) && !['error', 'unavailable', 'rate_limited'].includes(state))
|
|
403
373
|
state = 'partial';
|
|
404
374
|
const limitations = normalizeLimitations(resultInput.limitations);
|
|
405
375
|
if (state === 'not_found')
|
|
406
376
|
limitations.unshift(NO_MATCH_LIMITATION);
|
|
407
377
|
if (discarded > 0)
|
|
408
|
-
limitations.unshift(`${discarded} invalid or over-limit provider finding${discarded === 1 ? ' was' : 's were'} omitted.`);
|
|
378
|
+
limitations.unshift(`${discarded} invalid, conflicting or over-limit provider finding${discarded === 1 ? ' was' : 's were'} omitted.`);
|
|
379
|
+
if (conflictingIdentities > 0)
|
|
380
|
+
limitations.unshift(`${conflictingIdentities} provider identit${conflictingIdentities === 1 ? 'y has' : 'ies have'} conflicting records at the latest or unknown observation time. No stronger record was selected from those conflicts.`);
|
|
409
381
|
limitations.unshift(BASE_LIMITATION);
|
|
410
382
|
const boundedLimitations = [...new Set(limitations)].slice(0, MAX_LIMITATIONS);
|
|
411
383
|
const complete = ['success', 'not_found'].includes(state) && discarded === 0 && !sourceTruncated;
|
|
@@ -2,41 +2,13 @@
|
|
|
2
2
|
// and curated connectors. These helpers never perform network requests.
|
|
3
3
|
import { classifyQuery } from "./classify.mjs";
|
|
4
4
|
import { enumValue, exactKeys, strictBoundedString, } from "./bounded-contract-normalizers.mjs";
|
|
5
|
+
import { CURATED_CONNECTOR_ENTITY_VALUES, CURATED_CONNECTOR_TARGET_EXPOSURES, THREAT_INTELLIGENCE_TARGET_EXPOSURES, } from "./threat-intelligence-types.mjs";
|
|
5
6
|
export const MAX_THREAT_INTELLIGENCE_URL_LENGTH = 2048;
|
|
6
7
|
const TARGET_EXPOSURES = Object.freeze({
|
|
7
|
-
domain:
|
|
8
|
-
url:
|
|
9
|
-
'registrable_domain',
|
|
10
|
-
'hostname',
|
|
11
|
-
'origin',
|
|
12
|
-
'full_url',
|
|
13
|
-
]),
|
|
14
|
-
});
|
|
15
|
-
export const CURATED_CONNECTOR_ENTITY_TYPES = new Set([
|
|
16
|
-
'domain',
|
|
17
|
-
'hostname',
|
|
18
|
-
'url',
|
|
19
|
-
'ipv4',
|
|
20
|
-
'ipv6',
|
|
21
|
-
'asn',
|
|
22
|
-
'certificate',
|
|
23
|
-
]);
|
|
24
|
-
const CONNECTOR_TARGET_EXPOSURES = Object.freeze({
|
|
25
|
-
domain: new Set(['registrable_domain']),
|
|
26
|
-
hostname: new Set(['hostname']),
|
|
27
|
-
url: new Set([
|
|
28
|
-
'registrable_domain',
|
|
29
|
-
'hostname',
|
|
30
|
-
'origin',
|
|
31
|
-
'full_url',
|
|
32
|
-
]),
|
|
33
|
-
ipv4: new Set(['ip_address']),
|
|
34
|
-
ipv6: new Set(['ip_address']),
|
|
35
|
-
asn: new Set(['asn']),
|
|
36
|
-
certificate: new Set([
|
|
37
|
-
'certificate_fingerprint',
|
|
38
|
-
]),
|
|
8
|
+
domain: Object.freeze(['registrable_domain']),
|
|
9
|
+
url: THREAT_INTELLIGENCE_TARGET_EXPOSURES,
|
|
39
10
|
});
|
|
11
|
+
export const CURATED_CONNECTOR_ENTITY_TYPES = new Set(CURATED_CONNECTOR_ENTITY_VALUES);
|
|
40
12
|
function normalizeCertificateFingerprint(value) {
|
|
41
13
|
const raw = strictBoundedString(value, 128);
|
|
42
14
|
if (!raw || !/^[0-9a-f:]+$/iu.test(raw))
|
|
@@ -49,7 +21,7 @@ export function normalizeThreatIntelligenceTarget(input, exposure) {
|
|
|
49
21
|
const type = input.type;
|
|
50
22
|
if ((type !== 'domain' && type !== 'url')
|
|
51
23
|
|| typeof exposure !== 'string'
|
|
52
|
-
|| !TARGET_EXPOSURES[type].
|
|
24
|
+
|| !TARGET_EXPOSURES[type].includes(exposure)) {
|
|
53
25
|
throw new TypeError('Threat-intelligence target exposure is invalid');
|
|
54
26
|
}
|
|
55
27
|
if (type === 'domain') {
|
|
@@ -103,7 +75,7 @@ export function normalizeThreatIntelligenceTarget(input, exposure) {
|
|
|
103
75
|
export function normalizeCuratedConnectorTarget(input, exposure) {
|
|
104
76
|
exactKeys(input, new Set(['type', 'value']), 'Connector target');
|
|
105
77
|
const type = enumValue(input.type, CURATED_CONNECTOR_ENTITY_TYPES, 'Connector target type');
|
|
106
|
-
const normalizedExposure = enumValue(exposure,
|
|
78
|
+
const normalizedExposure = enumValue(exposure, new Set(CURATED_CONNECTOR_TARGET_EXPOSURES[type]), 'Connector target exposure');
|
|
107
79
|
if (type === 'url') {
|
|
108
80
|
const normalized = normalizeThreatIntelligenceTarget({ type: 'url', value: input.value }, normalizedExposure);
|
|
109
81
|
return Object.freeze({
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
export const THREAT_INTELLIGENCE_SCHEMA = 'whoisleuth.threat-intelligence-result';
|
|
4
4
|
export const THREAT_INTELLIGENCE_CONTRACT_VERSION = 1;
|
|
5
5
|
export const THREAT_INTELLIGENCE_ENVELOPE_VERSION = 1;
|
|
6
|
+
export const THREAT_INTELLIGENCE_TARGET_EXPOSURES = Object.freeze([
|
|
7
|
+
'registrable_domain', 'hostname', 'origin', 'full_url',
|
|
8
|
+
]);
|
|
9
|
+
export const THREAT_INTELLIGENCE_CAPABILITIES = Object.freeze([
|
|
10
|
+
'domain_lookup', 'url_lookup', 'indicator_search',
|
|
11
|
+
]);
|
|
6
12
|
export const THREAT_INTELLIGENCE_CATEGORIES = Object.freeze([
|
|
7
13
|
'phishing',
|
|
8
14
|
'malware',
|
|
@@ -34,3 +40,36 @@ export const THREAT_INTELLIGENCE_RESULT_STATES = Object.freeze([
|
|
|
34
40
|
'unavailable',
|
|
35
41
|
'error',
|
|
36
42
|
]);
|
|
43
|
+
export const CURATED_CONNECTOR_KINDS = Object.freeze(['discovery', 'enrichment']);
|
|
44
|
+
export const CURATED_CONNECTOR_COLLECTIONS = Object.freeze(['passive', 'active', 'third_party']);
|
|
45
|
+
export const CURATED_CONNECTOR_CREDENTIAL_MODES = Object.freeze(['none', 'optional', 'required']);
|
|
46
|
+
export const CURATED_CONNECTOR_ENTITY_VALUES = Object.freeze([
|
|
47
|
+
'domain', 'hostname', 'url', 'ipv4', 'ipv6', 'asn', 'certificate',
|
|
48
|
+
]);
|
|
49
|
+
export const CURATED_CONNECTOR_RELATIONSHIP_TYPES = Object.freeze([
|
|
50
|
+
'domain_resolves_to_ip', 'domain_uses_nameserver', 'domain_uses_mail_server',
|
|
51
|
+
'domain_presented_certificate', 'certificate_names_domain', 'ip_hosts_domain',
|
|
52
|
+
'domain_related_to_domain',
|
|
53
|
+
]);
|
|
54
|
+
export const CURATED_CONNECTOR_RELATIONSHIP_CLASSIFICATIONS = Object.freeze(['direct', 'normalized', 'derived']);
|
|
55
|
+
export const CURATED_CONNECTOR_TARGET_EXPOSURES = Object.freeze({
|
|
56
|
+
domain: Object.freeze(['registrable_domain']),
|
|
57
|
+
hostname: Object.freeze(['hostname']),
|
|
58
|
+
url: THREAT_INTELLIGENCE_TARGET_EXPOSURES,
|
|
59
|
+
ipv4: Object.freeze(['ip_address']),
|
|
60
|
+
ipv6: Object.freeze(['ip_address']),
|
|
61
|
+
asn: Object.freeze(['asn']),
|
|
62
|
+
certificate: Object.freeze(['certificate_fingerprint']),
|
|
63
|
+
});
|
|
64
|
+
function relationshipEndpoints(from, to) {
|
|
65
|
+
return Object.freeze({ from: Object.freeze(from), to: Object.freeze(to) });
|
|
66
|
+
}
|
|
67
|
+
export const CURATED_CONNECTOR_RELATIONSHIP_ENDPOINTS = Object.freeze({
|
|
68
|
+
domain_resolves_to_ip: relationshipEndpoints(['domain', 'hostname'], ['ipv4', 'ipv6']),
|
|
69
|
+
domain_uses_nameserver: relationshipEndpoints(['domain'], ['hostname']),
|
|
70
|
+
domain_uses_mail_server: relationshipEndpoints(['domain'], ['hostname']),
|
|
71
|
+
domain_presented_certificate: relationshipEndpoints(['domain', 'hostname'], ['certificate']),
|
|
72
|
+
certificate_names_domain: relationshipEndpoints(['certificate'], ['domain', 'hostname']),
|
|
73
|
+
ip_hosts_domain: relationshipEndpoints(['ipv4', 'ipv6'], ['domain', 'hostname']),
|
|
74
|
+
domain_related_to_domain: relationshipEndpoints(['domain', 'hostname'], ['domain', 'hostname']),
|
|
75
|
+
});
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// objects so a later scan can explain why each domain was generated; callers
|
|
3
3
|
// that only need the historical string list can use the compatibility wrapper.
|
|
4
4
|
import { advancedConfusableVariantsForAscii, confusableCharactersForAscii, estimateAdvancedConfusableVariants, MAX_ADVANCED_CONFUSABLE_VARIANTS, wholeLabelConfusableVariantsForAscii, } from "./idn-confusables.mjs";
|
|
5
|
+
import { canonicalRegistrableDomain, normalizeDiscoverySuffix } from "./registrable-domain.mjs";
|
|
6
|
+
import { MAX_DOMAIN_NAME_LENGTH, MAX_DOMAIN_LABEL_LENGTH } from "../packages/contracts/domain-name.mjs";
|
|
5
7
|
export { MAX_ADVANCED_CONFUSABLE_VARIANTS };
|
|
6
8
|
function buildKeyboardAdjacency(rows) {
|
|
7
9
|
const positions = new Map();
|
|
@@ -109,16 +111,15 @@ const TLD_TYPOS = {
|
|
|
109
111
|
export const MAX_GENERATION_TLDS = 20;
|
|
110
112
|
export const MAX_NAME_VARIANTS = 1_500;
|
|
111
113
|
export const MAX_GENERATED_CANDIDATES = 2_000;
|
|
112
|
-
export const MAX_GENERATION_INPUT_LENGTH =
|
|
114
|
+
export const MAX_GENERATION_INPUT_LENGTH = MAX_DOMAIN_NAME_LENGTH;
|
|
113
115
|
export const MAX_CUSTOM_DICTIONARY_TERMS = 100;
|
|
114
116
|
export const MAX_CUSTOM_DICTIONARY_TERM_LENGTH = 32;
|
|
115
117
|
export const MAX_CUSTOM_DICTIONARY_TEXT_LENGTH = 4_096;
|
|
116
|
-
const MAX_LABEL_LENGTH =
|
|
118
|
+
const MAX_LABEL_LENGTH = MAX_DOMAIN_LABEL_LENGTH;
|
|
117
119
|
const MAX_TLD_INPUTS_INSPECTED = MAX_GENERATION_TLDS * 4;
|
|
118
120
|
const MAX_CUSTOM_DICTIONARY_INPUTS_INSPECTED = MAX_CUSTOM_DICTIONARY_TERMS * 4;
|
|
119
121
|
const CONTROL_CHARACTER_RE = /[\x00-\x1f\x7f]/;
|
|
120
122
|
const DOMAIN_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
121
|
-
const TLD_RE = /^[a-z]{2,63}$/;
|
|
122
123
|
const CUSTOM_DICTIONARY_TERM_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
123
124
|
const FAMILY_NEW_VARIANT_LIMITS = Object.freeze({
|
|
124
125
|
character_addition: 128,
|
|
@@ -246,10 +247,11 @@ function splitDomainParts(input) {
|
|
|
246
247
|
if (!trimmed)
|
|
247
248
|
return null;
|
|
248
249
|
if (trimmed.includes('.')) {
|
|
249
|
-
const
|
|
250
|
-
const name =
|
|
251
|
-
const
|
|
252
|
-
|
|
250
|
+
const separator = trimmed.indexOf('.');
|
|
251
|
+
const name = trimmed.slice(0, separator);
|
|
252
|
+
const suffix = trimmed.slice(separator + 1);
|
|
253
|
+
const tld = normalizeDiscoverySuffix(suffix);
|
|
254
|
+
if (!isValidDomainLabel(name) || !tld || tld !== suffix || canonicalRegistrableDomain(trimmed) !== trimmed)
|
|
253
255
|
return null;
|
|
254
256
|
const tokens = name.split('-');
|
|
255
257
|
return { name, tld, wordTokens: tokens.length >= 2 && tokens.length <= 4 && tokens.every(Boolean) ? tokens : [] };
|
|
@@ -294,17 +296,15 @@ function distinctWordOrders(tokens) {
|
|
|
294
296
|
visit();
|
|
295
297
|
return orders;
|
|
296
298
|
}
|
|
297
|
-
function
|
|
299
|
+
export function normalizeGenerationTlds(values) {
|
|
298
300
|
if (!Array.isArray(values))
|
|
299
301
|
return { values: [], truncated: false };
|
|
300
302
|
const normalized = [];
|
|
301
303
|
const seen = new Set();
|
|
302
304
|
let truncated = values.length > MAX_TLD_INPUTS_INSPECTED;
|
|
303
305
|
for (const raw of values.slice(0, MAX_TLD_INPUTS_INSPECTED)) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const value = raw.trim().toLowerCase().replace(/^\./, '');
|
|
307
|
-
if (!TLD_RE.test(value) || seen.has(value))
|
|
306
|
+
const value = normalizeDiscoverySuffix(raw);
|
|
307
|
+
if (!value || seen.has(value))
|
|
308
308
|
continue;
|
|
309
309
|
if (normalized.length >= MAX_GENERATION_TLDS) {
|
|
310
310
|
truncated = true;
|
|
@@ -415,7 +415,7 @@ function selectCandidateTlds(sourceTld, fallbackTlds, enabledFamilies) {
|
|
|
415
415
|
if (sourceTld && !enabledFamilies.has('tld_substitution')) {
|
|
416
416
|
return { values: [sourceTld], truncated: false };
|
|
417
417
|
}
|
|
418
|
-
const normalizedFallbackTlds =
|
|
418
|
+
const normalizedFallbackTlds = normalizeGenerationTlds(fallbackTlds);
|
|
419
419
|
const combinedTlds = sourceTld
|
|
420
420
|
? [sourceTld, ...normalizedFallbackTlds.values.filter((value) => value !== sourceTld)]
|
|
421
421
|
: normalizedFallbackTlds.values;
|
|
@@ -673,8 +673,8 @@ export function generateTyposquatCandidateSet(rawInput, fallbackTlds, options =
|
|
|
673
673
|
addVariant(state, variant, 'pluralization');
|
|
674
674
|
}
|
|
675
675
|
if (enabledFamilies.has('tld_embedding') && tld) {
|
|
676
|
-
addVariant(state, `${name}${tld}`, 'tld_embedding');
|
|
677
|
-
addVariant(state, `${name}-${tld}`, 'tld_embedding');
|
|
676
|
+
addVariant(state, `${name}${tld.replaceAll('.', '')}`, 'tld_embedding');
|
|
677
|
+
addVariant(state, `${name}-${tld.replaceAll('.', '-')}`, 'tld_embedding');
|
|
678
678
|
}
|
|
679
679
|
if (enabledFamilies.has('www_prefix')) {
|
|
680
680
|
addVariant(state, `ww${name}`, 'www_prefix');
|
|
@@ -821,6 +821,10 @@ export function generateTyposquatCandidateSet(rawInput, fallbackTlds, options =
|
|
|
821
821
|
const byDomain = new Map();
|
|
822
822
|
const source = tld ? `${name}.${tld}` : name;
|
|
823
823
|
function addCandidate(domain, candidateTld, mutationTypes) {
|
|
824
|
+
if (domain.length > MAX_DOMAIN_NAME_LENGTH) {
|
|
825
|
+
state.rejectedVariantCount += 1;
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
824
828
|
const existing = byDomain.get(domain);
|
|
825
829
|
if (existing) {
|
|
826
830
|
for (const mutationType of mutationTypes) {
|
|
@@ -74,6 +74,11 @@ function boundedText(value, maxLength) {
|
|
|
74
74
|
const normalized = value.trim().replace(/\s+/gu, ' ');
|
|
75
75
|
return normalized ? normalized.slice(0, maxLength) : null;
|
|
76
76
|
}
|
|
77
|
+
function identityText(value, maxLength) {
|
|
78
|
+
if (typeof value !== 'string' || value.length > maxLength || /[\u0000-\u0020\u007f]/u.test(value))
|
|
79
|
+
return null;
|
|
80
|
+
return value || null;
|
|
81
|
+
}
|
|
77
82
|
function isoTimestamp(value) {
|
|
78
83
|
if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value))
|
|
79
84
|
return null;
|
|
@@ -91,7 +96,7 @@ function retryAfterSeconds(response) {
|
|
|
91
96
|
return Number.isSafeInteger(seconds) && seconds >= 1 && seconds <= 86_400 ? seconds : null;
|
|
92
97
|
}
|
|
93
98
|
function exactDomainFromUrl(value) {
|
|
94
|
-
const raw =
|
|
99
|
+
const raw = identityText(value, 2_048);
|
|
95
100
|
if (!raw)
|
|
96
101
|
return null;
|
|
97
102
|
try {
|
|
@@ -106,7 +111,7 @@ function exactDomainFromUrl(value) {
|
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
function exactDomain(value) {
|
|
109
|
-
const raw =
|
|
114
|
+
const raw = identityText(value, 253);
|
|
110
115
|
if (!raw)
|
|
111
116
|
return null;
|
|
112
117
|
try {
|
|
@@ -138,7 +143,7 @@ function normalizedUrlCount(value) {
|
|
|
138
143
|
function normalizeHostFinding(value, targetDomain) {
|
|
139
144
|
if (!isRecord(value))
|
|
140
145
|
return null;
|
|
141
|
-
const id =
|
|
146
|
+
const id = identityText(value.id, MAX_URL_ID_LENGTH);
|
|
142
147
|
const status = boundedText(value.url_status, 16)?.toLowerCase();
|
|
143
148
|
const threat = boundedText(value.threat, 64)?.toLowerCase();
|
|
144
149
|
if (!id
|
|
@@ -299,7 +299,7 @@ function createUrlscanIntelligenceAdapter(dependencies = {}) {
|
|
|
299
299
|
const overLimit = parsed.results.length > URLSCAN_MAX_RESULTS || parsed.has_more === true;
|
|
300
300
|
const invalid = normalized.filter((item) => item === null).length;
|
|
301
301
|
const reviewed = rawResults.length - invalid;
|
|
302
|
-
if (
|
|
302
|
+
if (parsed.results.length === 0 && !overLimit) {
|
|
303
303
|
return result(targetDomain, {
|
|
304
304
|
state: 'not_found',
|
|
305
305
|
detail: `No archived malicious-verdict match was returned for the last ${URLSCAN_SEARCH_DAYS} days.`,
|
|
@@ -3,7 +3,27 @@
|
|
|
3
3
|
// Findings use fixed explanatory text and bounded counts; upstream header
|
|
4
4
|
// values, certificate errors, URLs, and DNS record contents are not copied.
|
|
5
5
|
import { createObservation } from "../packages/evidence/observation.mjs";
|
|
6
|
+
import { RESPONSE_POLICY_VERSION } from "./response-policy.mjs";
|
|
6
7
|
import { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, } from "./lookup-child-profile-contract.mjs";
|
|
8
|
+
// Observation state and review significance are separate. These presentation
|
|
9
|
+
// projections leave retained findings, wire counts and model versions intact.
|
|
10
|
+
function securityPostureReview(value) {
|
|
11
|
+
const item = record(value);
|
|
12
|
+
if (item.state !== 'observed' && item.state !== 'potential_exposure' && item.state !== 'observed_absence') {
|
|
13
|
+
return { kind: 'unavailable', label: 'Could not assess' };
|
|
14
|
+
}
|
|
15
|
+
if (item.state === 'potential_exposure' || item.tone === 'review') {
|
|
16
|
+
return { kind: 'needsReview', label: 'Needs review' };
|
|
17
|
+
}
|
|
18
|
+
return { kind: 'otherFindings', label: null };
|
|
19
|
+
}
|
|
20
|
+
function summarizeSecurityPostureReview(value) {
|
|
21
|
+
const counts = { needsReview: 0, otherFindings: 0, unavailable: 0 };
|
|
22
|
+
for (const item of (Array.isArray(value) ? value : []).slice(0, MAX_SECURITY_POSTURE_FINDINGS)) {
|
|
23
|
+
counts[securityPostureReview(item).kind] += 1;
|
|
24
|
+
}
|
|
25
|
+
return counts;
|
|
26
|
+
}
|
|
7
27
|
const MAX_RETAINED_ORIGINS = 30;
|
|
8
28
|
function record(value) {
|
|
9
29
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
@@ -86,12 +106,16 @@ const RESPONSE_POLICY_FINDINGS = Object.freeze({
|
|
|
86
106
|
detail: () => 'The effective selected script source included a wildcard or broad scheme source. This fixed observation does not establish that an unsafe script is reachable.',
|
|
87
107
|
},
|
|
88
108
|
csp_unsafe_eval: {
|
|
89
|
-
label: 'CSP
|
|
90
|
-
detail: () => "The
|
|
109
|
+
label: 'CSP evaluation allowance observed',
|
|
110
|
+
detail: () => "The selected response policies allow evaluation through script-src or default-src. Element and attribute directives do not govern this allowance; other browser controls were not evaluated.",
|
|
91
111
|
},
|
|
92
112
|
csp_unsafe_inline: {
|
|
93
|
-
label: 'Response CSP
|
|
94
|
-
detail: () => "The selected response
|
|
113
|
+
label: 'Response CSP allows unqualified inline script blocks',
|
|
114
|
+
detail: () => "The selected response policies allow inline script blocks through script-src-elem, script-src or default-src without a valid nonce/hash source or strict-dynamic suppressing unsafe-inline. No earlier page policy qualified this response-scoped finding.",
|
|
115
|
+
},
|
|
116
|
+
csp_unsafe_inline_attributes: {
|
|
117
|
+
label: 'Response CSP allows unqualified inline event handlers',
|
|
118
|
+
detail: () => 'The selected response policies allow inline event attributes through script-src-attr, script-src or default-src. This header-only observation does not establish whether a handler exists or executes; page-policy timing was not evaluated for attributes.',
|
|
95
119
|
},
|
|
96
120
|
csp_inline_constrained_by_meta: {
|
|
97
121
|
label: 'Page CSP further constrains inline script',
|
|
@@ -131,7 +155,7 @@ const RESPONSE_POLICY_FINDINGS = Object.freeze({
|
|
|
131
155
|
});
|
|
132
156
|
function responsePolicyFindings(value) {
|
|
133
157
|
const policy = record(value);
|
|
134
|
-
if (![1, 2].includes(Number(policy.responsePolicyVersion)))
|
|
158
|
+
if (![1, 2, RESPONSE_POLICY_VERSION].includes(Number(policy.responsePolicyVersion)))
|
|
135
159
|
return [];
|
|
136
160
|
const findings = [];
|
|
137
161
|
const components = record(policy.components);
|
|
@@ -328,4 +352,4 @@ function analyzeWebsiteSecurityPosture(input = {}) {
|
|
|
328
352
|
findings,
|
|
329
353
|
};
|
|
330
354
|
}
|
|
331
|
-
export { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, analyzeWebsiteSecurityPosture, };
|
|
355
|
+
export { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, analyzeWebsiteSecurityPosture, securityPostureReview, summarizeSecurityPostureReview, };
|