@slicedearth/whoisleuth-cli 1.47.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
- package/third-party-notices.txt +14 -14
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -255
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -1,1034 +0,0 @@
|
|
|
1
|
-
// Shared HTTP response boundary for the unified Lookup API.
|
|
2
|
-
//
|
|
3
|
-
// The backend owns the evidence collectors, but an HTTP response is still an
|
|
4
|
-
// untrusted value when it reaches the browser. This module keeps the response
|
|
5
|
-
// envelope typed and validates the small set of fields the Lookup controller
|
|
6
|
-
// relies on before any evidence is derived or rendered. Nested source records
|
|
7
|
-
// stay separately attributed and additive; their own normalizers remain the
|
|
8
|
-
// authority for source-specific fields.
|
|
9
|
-
import { THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_RESULT_STATES, } from "./threat-intelligence-types.mjs";
|
|
10
|
-
import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, MAX_HTTP_PROVENANCE_URL, } from "./http-evidence-bounds.mjs";
|
|
11
|
-
import { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, } from "./observation.mjs";
|
|
12
|
-
import { assertBoundedJsonStructure } from "./bounded-json.mjs";
|
|
13
|
-
import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
|
|
14
|
-
import { MAX_SECURITY_POSTURE_FINDINGS } from "./website-security-posture.mjs";
|
|
15
|
-
const INVALID_LOOKUP_RESPONSE = 'INVALID_LOOKUP_RESPONSE';
|
|
16
|
-
const INVALID_LOOKUP_RESPONSE_MESSAGE = 'Lookup returned an invalid response.';
|
|
17
|
-
const INVALID_COMPACT_LOOKUP_RESPONSE = 'INVALID_COMPACT_LOOKUP_RESPONSE';
|
|
18
|
-
const INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE = 'Bulk lookup returned an invalid response.';
|
|
19
|
-
const MAX_LOOKUP_RESPONSE_QUERY_LENGTH = 4096;
|
|
20
|
-
const MAX_LOOKUP_RESPONSE_HOST_LENGTH = 253;
|
|
21
|
-
const MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 32;
|
|
22
|
-
const MAX_LOOKUP_RESPONSE_ERROR_LENGTH = 240;
|
|
23
|
-
const MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS = 500;
|
|
24
|
-
const MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 8;
|
|
25
|
-
const MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS = 128;
|
|
26
|
-
const MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS = 16;
|
|
27
|
-
const MAX_COMPACT_BULK_TECHNOLOGY_IDS = 12;
|
|
28
|
-
const MAX_THREAT_INTELLIGENCE_PROVIDERS = 10;
|
|
29
|
-
const MAX_THREAT_INTELLIGENCE_FINDINGS = 100;
|
|
30
|
-
const MAX_THREAT_INTELLIGENCE_LIMITATIONS = 10;
|
|
31
|
-
const MAX_THREAT_INTELLIGENCE_TEXT_LENGTH = 500;
|
|
32
|
-
const MAX_LOOKUP_TIMING_MS = 120_000;
|
|
33
|
-
const MAX_LOOKUP_TIMING_SOURCES = 10;
|
|
34
|
-
const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/u;
|
|
35
|
-
const QUERY_TYPES = new Set(['domain', 'ipv4', 'ipv6', 'asn']);
|
|
36
|
-
const COMPACT_AVAILABILITY_STATES = new Set([
|
|
37
|
-
'available',
|
|
38
|
-
'expiring',
|
|
39
|
-
'for_sale',
|
|
40
|
-
'registered',
|
|
41
|
-
'unknown',
|
|
42
|
-
]);
|
|
43
|
-
const COMPACT_CONFIDENCE_LEVELS = new Set(['high', 'low', 'medium']);
|
|
44
|
-
const COMPACT_AVAILABILITY_DIAGNOSTIC_STATES = new Set(['complete', 'disabled', 'error']);
|
|
45
|
-
const COMPACT_BULK_COMPARISON_STATES = new Set([
|
|
46
|
-
'error',
|
|
47
|
-
'not_found',
|
|
48
|
-
'partial',
|
|
49
|
-
'success',
|
|
50
|
-
'unavailable',
|
|
51
|
-
]);
|
|
52
|
-
const COMPACT_BULK_TECHNOLOGY_ID_RE = /^[a-z0-9][a-z0-9_-]{0,79}$/u;
|
|
53
|
-
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
54
|
-
const THREAT_INTELLIGENCE_PROVIDERS = Object.freeze({
|
|
55
|
-
urlscan_search: Object.freeze({ label: 'URLscan archived verdicts', host: 'urlscan.io' }),
|
|
56
|
-
urlhaus_host: Object.freeze({ label: 'URLhaus malware-host records', host: 'urlhaus.abuse.ch' }),
|
|
57
|
-
threatfox_domain_ioc: Object.freeze({ label: 'ThreatFox malware IOCs', host: 'threatfox.abuse.ch' }),
|
|
58
|
-
});
|
|
59
|
-
const THREAT_INTELLIGENCE_STATES = new Set(THREAT_INTELLIGENCE_RESULT_STATES);
|
|
60
|
-
const THREAT_INTELLIGENCE_CATEGORY_SET = new Set(THREAT_INTELLIGENCE_CATEGORIES);
|
|
61
|
-
const LOOKUP_TIMING_SOURCES = new Set([
|
|
62
|
-
'rdap',
|
|
63
|
-
'whois',
|
|
64
|
-
'domain_evidence',
|
|
65
|
-
'reverse_dns',
|
|
66
|
-
'registrar_rdap',
|
|
67
|
-
'network_context',
|
|
68
|
-
'security_txt',
|
|
69
|
-
'external_intelligence',
|
|
70
|
-
'malware_host_intelligence',
|
|
71
|
-
'malware_ioc_intelligence',
|
|
72
|
-
]);
|
|
73
|
-
const LOOKUP_TIMING_OUTCOMES = new Set(['fulfilled', 'rejected']);
|
|
74
|
-
const EMPTY_RECORD = Object.freeze({});
|
|
75
|
-
function isJsonObject(value) {
|
|
76
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
77
|
-
}
|
|
78
|
-
function record(value) {
|
|
79
|
-
return isJsonObject(value) ? value : EMPTY_RECORD;
|
|
80
|
-
}
|
|
81
|
-
function boundedThreatText(value, maximum = MAX_THREAT_INTELLIGENCE_TEXT_LENGTH) {
|
|
82
|
-
if (typeof value !== 'string' || CONTROL_CHAR_RE.test(value))
|
|
83
|
-
return null;
|
|
84
|
-
const text = value.slice(0, maximum * 2).trim();
|
|
85
|
-
return text ? text.slice(0, maximum) : null;
|
|
86
|
-
}
|
|
87
|
-
function threatTimestamp(value) {
|
|
88
|
-
const text = boundedThreatText(value, 40);
|
|
89
|
-
if (!text)
|
|
90
|
-
return null;
|
|
91
|
-
const epoch = Date.parse(text);
|
|
92
|
-
return Number.isFinite(epoch) ? new Date(epoch).toISOString() : null;
|
|
93
|
-
}
|
|
94
|
-
function attributedThreatUrl(value, providerId) {
|
|
95
|
-
const expectedHost = THREAT_INTELLIGENCE_PROVIDERS[providerId]?.host;
|
|
96
|
-
if (!expectedHost || typeof value !== 'string' || value.length > 1_024 || CONTROL_CHAR_RE.test(value))
|
|
97
|
-
return null;
|
|
98
|
-
try {
|
|
99
|
-
const url = new URL(value);
|
|
100
|
-
return url.protocol === 'https:' && !url.username && !url.password && url.hostname === expectedHost
|
|
101
|
-
? url.href
|
|
102
|
-
: null;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function normalizeThreatFinding(value, providerId) {
|
|
109
|
-
const input = record(value);
|
|
110
|
-
const category = boundedThreatText(input.category, 32);
|
|
111
|
-
if (!category || !THREAT_INTELLIGENCE_CATEGORY_SET.has(category))
|
|
112
|
-
return null;
|
|
113
|
-
const referenceUrl = attributedThreatUrl(input.referenceUrl, providerId);
|
|
114
|
-
const tags = Array.isArray(input.tags)
|
|
115
|
-
? [...new Set(input.tags.slice(0, 40).map((tag) => boundedThreatText(tag, 80)).filter((tag) => tag !== null))].slice(0, 20)
|
|
116
|
-
: [];
|
|
117
|
-
return {
|
|
118
|
-
id: boundedThreatText(input.id, 160),
|
|
119
|
-
category,
|
|
120
|
-
providerVerdict: boundedThreatText(input.providerVerdict, 120),
|
|
121
|
-
detail: boundedThreatText(input.detail),
|
|
122
|
-
firstObservedAt: threatTimestamp(input.firstObservedAt),
|
|
123
|
-
lastObservedAt: threatTimestamp(input.lastObservedAt),
|
|
124
|
-
referenceUrl,
|
|
125
|
-
tags,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function normalizeThreatProvider(value) {
|
|
129
|
-
const input = record(value);
|
|
130
|
-
const identity = record(input.provider);
|
|
131
|
-
const providerId = boundedThreatText(identity.id, 80);
|
|
132
|
-
const providerDefinition = providerId ? THREAT_INTELLIGENCE_PROVIDERS[providerId] : null;
|
|
133
|
-
if (!providerId || !providerDefinition)
|
|
134
|
-
return null;
|
|
135
|
-
const state = boundedThreatText(input.state, 32);
|
|
136
|
-
if (!state || !THREAT_INTELLIGENCE_STATES.has(state))
|
|
137
|
-
return null;
|
|
138
|
-
const observationInput = record(input.observation);
|
|
139
|
-
const limitations = Array.isArray(observationInput.limitations)
|
|
140
|
-
? observationInput.limitations
|
|
141
|
-
.slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS * 2)
|
|
142
|
-
.map((limitation) => boundedThreatText(limitation))
|
|
143
|
-
.filter((limitation) => limitation !== null)
|
|
144
|
-
.slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS)
|
|
145
|
-
: [];
|
|
146
|
-
const findings = Array.isArray(input.findings)
|
|
147
|
-
? input.findings
|
|
148
|
-
.slice(0, MAX_THREAT_INTELLIGENCE_FINDINGS * 2)
|
|
149
|
-
.map((finding) => normalizeThreatFinding(finding, providerId))
|
|
150
|
-
.filter((finding) => finding !== null)
|
|
151
|
-
.slice(0, MAX_THREAT_INTELLIGENCE_FINDINGS)
|
|
152
|
-
: [];
|
|
153
|
-
return {
|
|
154
|
-
provider: {
|
|
155
|
-
id: providerId,
|
|
156
|
-
label: providerDefinition.label,
|
|
157
|
-
},
|
|
158
|
-
state,
|
|
159
|
-
detail: boundedThreatText(input.detail),
|
|
160
|
-
findings,
|
|
161
|
-
observation: {
|
|
162
|
-
observedAt: threatTimestamp(observationInput.observedAt),
|
|
163
|
-
limitations,
|
|
164
|
-
complete: typeof observationInput.complete === 'boolean' ? observationInput.complete : null,
|
|
165
|
-
truncated: typeof observationInput.truncated === 'boolean' ? observationInput.truncated : null,
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
function optionalBoundedText(value, maxLength) {
|
|
170
|
-
return value === undefined || (typeof value === 'string'
|
|
171
|
-
&& value.length <= maxLength
|
|
172
|
-
&& !CONTROL_CHAR_RE.test(value));
|
|
173
|
-
}
|
|
174
|
-
function normalizedDomain(value) {
|
|
175
|
-
if (typeof value !== 'string'
|
|
176
|
-
|| !value.trim()
|
|
177
|
-
|| value.length > MAX_LOOKUP_RESPONSE_HOST_LENGTH
|
|
178
|
-
|| CONTROL_CHAR_RE.test(value)
|
|
179
|
-
|| /[\s/?#@\\:]/u.test(value)) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
const parsed = new URL(`https://${value.trim().replace(/\.$/u, '')}/`);
|
|
184
|
-
const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
|
|
185
|
-
const labels = hostname.split('.');
|
|
186
|
-
return labels.length >= 2
|
|
187
|
-
&& hostname.length <= MAX_LOOKUP_RESPONSE_HOST_LENGTH
|
|
188
|
-
&& labels.every((label) => (label.length <= 63
|
|
189
|
-
&& /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)))
|
|
190
|
-
? hostname
|
|
191
|
-
: null;
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
function compactDomainMatches(value, expectedDomain) {
|
|
198
|
-
const domain = normalizedDomain(value);
|
|
199
|
-
const expected = normalizedDomain(expectedDomain);
|
|
200
|
-
return Boolean(domain && expected && (domain === expected || expected.endsWith(`.${domain}`)));
|
|
201
|
-
}
|
|
202
|
-
function validCompactBulkComparison(value) {
|
|
203
|
-
if (!isJsonObject(value)
|
|
204
|
-
|| Object.keys(value).length > 3
|
|
205
|
-
|| value.version !== 1
|
|
206
|
-
|| !isJsonObject(value.technology)
|
|
207
|
-
|| Object.keys(value.technology).length > 3
|
|
208
|
-
|| typeof value.technology.state !== 'string'
|
|
209
|
-
|| !COMPACT_BULK_COMPARISON_STATES.has(value.technology.state)
|
|
210
|
-
|| !Array.isArray(value.technology.ids)
|
|
211
|
-
|| value.technology.ids.length > MAX_COMPACT_BULK_TECHNOLOGY_IDS
|
|
212
|
-
|| typeof value.technology.truncated !== 'boolean'
|
|
213
|
-
|| !isJsonObject(value.tls)
|
|
214
|
-
|| Object.keys(value.tls).length > 3
|
|
215
|
-
|| typeof value.tls.state !== 'string'
|
|
216
|
-
|| !COMPACT_BULK_COMPARISON_STATES.has(value.tls.state)
|
|
217
|
-
|| (value.tls.issuerLabel !== null && !optionalBoundedText(value.tls.issuerLabel, 240))
|
|
218
|
-
|| (value.tls.spkiSha256 !== null
|
|
219
|
-
&& (typeof value.tls.spkiSha256 !== 'string' || !SHA256_RE.test(value.tls.spkiSha256)))) {
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
const ids = value.technology.ids;
|
|
223
|
-
return ids.every((id) => typeof id === 'string' && COMPACT_BULK_TECHNOLOGY_ID_RE.test(id))
|
|
224
|
-
&& new Set(ids).size === ids.length;
|
|
225
|
-
}
|
|
226
|
-
function invalidLookupResponse() {
|
|
227
|
-
return {
|
|
228
|
-
ok: false,
|
|
229
|
-
errorCode: INVALID_LOOKUP_RESPONSE,
|
|
230
|
-
error: INVALID_LOOKUP_RESPONSE_MESSAGE,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
function invalidCompactLookupResponse() {
|
|
234
|
-
return {
|
|
235
|
-
ok: false,
|
|
236
|
-
errorCode: INVALID_COMPACT_LOOKUP_RESPONSE,
|
|
237
|
-
error: INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
function validHttpProvenanceUrl(value) {
|
|
241
|
-
if (typeof value !== 'string'
|
|
242
|
-
|| !value
|
|
243
|
-
|| value.length > MAX_HTTP_PROVENANCE_URL
|
|
244
|
-
|| CONTROL_CHAR_RE.test(value))
|
|
245
|
-
return false;
|
|
246
|
-
try {
|
|
247
|
-
const parsed = new URL(value);
|
|
248
|
-
return ['http:', 'https:'].includes(parsed.protocol)
|
|
249
|
-
&& Boolean(parsed.hostname)
|
|
250
|
-
&& !parsed.username
|
|
251
|
-
&& !parsed.password
|
|
252
|
-
&& !parsed.search
|
|
253
|
-
&& !parsed.hash
|
|
254
|
-
&& parsed.href === value;
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function validOptionalHttpUrl(value) {
|
|
261
|
-
return value === undefined || value === null || validHttpProvenanceUrl(value);
|
|
262
|
-
}
|
|
263
|
-
function validOptionalHttpStatus(value) {
|
|
264
|
-
return value === undefined || value === null
|
|
265
|
-
|| Number.isInteger(value) && Number(value) >= 100 && Number(value) <= 599;
|
|
266
|
-
}
|
|
267
|
-
function validObservationLimitations(value) {
|
|
268
|
-
return value === undefined || Array.isArray(value)
|
|
269
|
-
&& value.length <= MAX_OBSERVATION_LIMITATIONS
|
|
270
|
-
&& value.every((item) => typeof item === 'string'
|
|
271
|
-
&& Boolean(item)
|
|
272
|
-
&& item.length <= MAX_OBSERVATION_LIMITATION_LENGTH
|
|
273
|
-
&& !CONTROL_CHAR_RE.test(item));
|
|
274
|
-
}
|
|
275
|
-
function validObservationDiagnosticValue(value, depth = 0) {
|
|
276
|
-
if (typeof value === 'boolean')
|
|
277
|
-
return true;
|
|
278
|
-
if (typeof value === 'number')
|
|
279
|
-
return Number.isFinite(value);
|
|
280
|
-
if (typeof value === 'string') {
|
|
281
|
-
return value.length <= 240 && !CONTROL_CHAR_RE.test(value);
|
|
282
|
-
}
|
|
283
|
-
if (!isJsonObject(value) || depth >= 2)
|
|
284
|
-
return false;
|
|
285
|
-
const keys = Object.keys(value);
|
|
286
|
-
return keys.length <= 6
|
|
287
|
-
&& keys.every((key) => ['status', 'error', 'detail', 'truncated', 'discarded', 'count'].includes(key))
|
|
288
|
-
&& keys.every((key) => validObservationDiagnosticValue(value[key], depth + 1));
|
|
289
|
-
}
|
|
290
|
-
function validObservationDiagnostics(value) {
|
|
291
|
-
if (value === undefined)
|
|
292
|
-
return true;
|
|
293
|
-
if (!isJsonObject(value))
|
|
294
|
-
return false;
|
|
295
|
-
const entries = Object.entries(value);
|
|
296
|
-
return entries.length <= MAX_OBSERVATION_DIAGNOSTICS
|
|
297
|
-
&& entries.every(([key, item]) => (Boolean(key)
|
|
298
|
-
&& key.length <= 40
|
|
299
|
-
&& /^[a-z0-9_-]+$/iu.test(key)
|
|
300
|
-
&& validObservationDiagnosticValue(item)));
|
|
301
|
-
}
|
|
302
|
-
function validObservationFields(value) {
|
|
303
|
-
if (!isJsonObject(value))
|
|
304
|
-
return false;
|
|
305
|
-
return optionalBoundedText(value.status, 40)
|
|
306
|
-
&& optionalBoundedText(value.source, 40)
|
|
307
|
-
&& optionalBoundedText(value.observedAt, 64)
|
|
308
|
-
&& (value.scanMode === undefined || value.scanMode === null
|
|
309
|
-
|| typeof value.scanMode === 'string' && ['fast', 'deep'].includes(value.scanMode))
|
|
310
|
-
&& (value.durationMs === undefined || value.durationMs === null
|
|
311
|
-
|| Number.isFinite(value.durationMs) && Number(value.durationMs) >= 0
|
|
312
|
-
&& Number(value.durationMs) <= 120_000)
|
|
313
|
-
&& (value.complete === undefined || typeof value.complete === 'boolean')
|
|
314
|
-
&& (value.truncated === undefined || typeof value.truncated === 'boolean')
|
|
315
|
-
&& validObservationLimitations(value.limitations)
|
|
316
|
-
&& validObservationDiagnostics(value.diagnostics);
|
|
317
|
-
}
|
|
318
|
-
function validSourceStatus(value) {
|
|
319
|
-
if (!isJsonObject(value))
|
|
320
|
-
return false;
|
|
321
|
-
return optionalBoundedText(value.status, 40)
|
|
322
|
-
&& (value.error === undefined || value.error === null || optionalBoundedText(value.error, 240))
|
|
323
|
-
&& (value.endpoint === undefined || value.endpoint === null || optionalBoundedText(value.endpoint, 2_048))
|
|
324
|
-
&& (value.detail === undefined || value.detail === null || optionalBoundedText(value.detail, 500))
|
|
325
|
-
&& (value.fetchedAt === undefined || value.fetchedAt === null || optionalBoundedText(value.fetchedAt, 64))
|
|
326
|
-
&& (value.queriedAt === undefined || value.queriedAt === null || optionalBoundedText(value.queriedAt, 64))
|
|
327
|
-
&& (value.attempts === undefined || Array.isArray(value.attempts)
|
|
328
|
-
&& value.attempts.length <= 3
|
|
329
|
-
&& value.attempts.every((item) => isJsonObject(item)
|
|
330
|
-
&& optionalBoundedText(item.outcome, 40)));
|
|
331
|
-
}
|
|
332
|
-
function validNormalizedHttpEvidence(value) {
|
|
333
|
-
if (!validObservationFields(value)
|
|
334
|
-
|| !validOptionalHttpUrl(value.requestUrl)
|
|
335
|
-
|| !validOptionalHttpUrl(value.finalUrl)
|
|
336
|
-
|| !validObservationLimitations(value.limitations))
|
|
337
|
-
return false;
|
|
338
|
-
const redirects = value.redirects;
|
|
339
|
-
if (redirects !== undefined && (!Array.isArray(redirects)
|
|
340
|
-
|| redirects.length > MAX_HTTP_EVIDENCE_REDIRECTS
|
|
341
|
-
|| !redirects.every((item) => isJsonObject(item)
|
|
342
|
-
&& validHttpProvenanceUrl(item.from)
|
|
343
|
-
&& validHttpProvenanceUrl(item.to)
|
|
344
|
-
&& validOptionalHttpStatus(item.status)
|
|
345
|
-
&& (item.queryOmitted === undefined || typeof item.queryOmitted === 'boolean'))))
|
|
346
|
-
return false;
|
|
347
|
-
const attempts = value.attempts;
|
|
348
|
-
if (attempts !== undefined && (!Array.isArray(attempts)
|
|
349
|
-
|| attempts.length > MAX_HTTP_ATTEMPTS
|
|
350
|
-
|| !attempts.every((item) => isJsonObject(item)
|
|
351
|
-
&& validOptionalHttpUrl(item.url)
|
|
352
|
-
&& validOptionalHttpStatus(item.httpStatus)
|
|
353
|
-
&& (item.queryOmitted === undefined || typeof item.queryOmitted === 'boolean')
|
|
354
|
-
&& (item.outcome === undefined || typeof item.outcome === 'string'
|
|
355
|
-
&& ['error', 'response', 'unknown'].includes(item.outcome))
|
|
356
|
-
&& (item.error === undefined || item.error === null || typeof item.error === 'string'
|
|
357
|
-
&& item.error.length <= MAX_HTTP_ERROR_LENGTH && !CONTROL_CHAR_RE.test(item.error)))))
|
|
358
|
-
return false;
|
|
359
|
-
if (value.redirectCount !== undefined
|
|
360
|
-
&& (!Number.isInteger(value.redirectCount)
|
|
361
|
-
|| Number(value.redirectCount) < 0
|
|
362
|
-
|| Number(value.redirectCount) > MAX_HTTP_EVIDENCE_REDIRECTS))
|
|
363
|
-
return false;
|
|
364
|
-
if (Array.isArray(redirects) && value.redirectCount !== undefined
|
|
365
|
-
&& value.redirectCount !== redirects.length)
|
|
366
|
-
return false;
|
|
367
|
-
if (value.response !== undefined && value.response !== null) {
|
|
368
|
-
if (!isJsonObject(value.response) || !validOptionalHttpStatus(value.response.status))
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
function optionalStringArrayWithin(value, maximum) {
|
|
374
|
-
return value === undefined || Array.isArray(value)
|
|
375
|
-
&& value.length <= maximum
|
|
376
|
-
&& value.every((item) => typeof item === 'string');
|
|
377
|
-
}
|
|
378
|
-
function optionalRecordArrayWithin(value, maximum) {
|
|
379
|
-
return value === undefined || Array.isArray(value)
|
|
380
|
-
&& value.length <= maximum
|
|
381
|
-
&& value.every(isJsonObject);
|
|
382
|
-
}
|
|
383
|
-
function validBoundedString(value, maximum, allowEmpty = false) {
|
|
384
|
-
return typeof value === 'string'
|
|
385
|
-
&& (allowEmpty || Boolean(value))
|
|
386
|
-
&& value.length <= maximum
|
|
387
|
-
&& !CONTROL_CHAR_RE.test(value);
|
|
388
|
-
}
|
|
389
|
-
function validUint(value, maximum) {
|
|
390
|
-
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum;
|
|
391
|
-
}
|
|
392
|
-
function validDnsMxRecord(value) {
|
|
393
|
-
return isJsonObject(value)
|
|
394
|
-
&& validUint(value.priority, 0xffff)
|
|
395
|
-
&& validBoundedString(value.exchange, 253, true);
|
|
396
|
-
}
|
|
397
|
-
function validDnsCaaRecord(value) {
|
|
398
|
-
return isJsonObject(value)
|
|
399
|
-
&& validUint(value.critical, 0xff)
|
|
400
|
-
&& typeof value.tag === 'string'
|
|
401
|
-
&& /^[a-z0-9-]{1,15}$/u.test(value.tag)
|
|
402
|
-
&& validBoundedString(value.value, 1_024);
|
|
403
|
-
}
|
|
404
|
-
function validDnsSoaRecord(value) {
|
|
405
|
-
return isJsonObject(value)
|
|
406
|
-
&& validBoundedString(value.nsname, 253)
|
|
407
|
-
&& validBoundedString(value.hostmaster, 253)
|
|
408
|
-
&& ['serial', 'refresh', 'retry', 'expire', 'minttl']
|
|
409
|
-
.every((key) => validUint(value[key], 0xffff_ffff));
|
|
410
|
-
}
|
|
411
|
-
function validUintArray(value, maximumItems) {
|
|
412
|
-
return Array.isArray(value)
|
|
413
|
-
&& value.length <= maximumItems
|
|
414
|
-
&& value.every((item) => validUint(item, 0xffff));
|
|
415
|
-
}
|
|
416
|
-
function validStringArray(value, maximumItems, maximumLength) {
|
|
417
|
-
return Array.isArray(value)
|
|
418
|
-
&& value.length <= maximumItems
|
|
419
|
-
&& value.every((item) => validBoundedString(item, maximumLength));
|
|
420
|
-
}
|
|
421
|
-
function validDnsHttpsRecord(value) {
|
|
422
|
-
if (!isJsonObject(value)
|
|
423
|
-
|| value.type !== 'HTTPS'
|
|
424
|
-
|| !validBoundedString(value.owner, 253)
|
|
425
|
-
|| !validUint(value.ttl, 0xffff_ffff)
|
|
426
|
-
|| !validUint(value.priority, 0xffff)
|
|
427
|
-
|| typeof value.mode !== 'string'
|
|
428
|
-
|| !['alias', 'service'].includes(value.mode)
|
|
429
|
-
|| !(value.target === null || validBoundedString(value.target, 253))
|
|
430
|
-
|| typeof value.targetIsOwner !== 'boolean'
|
|
431
|
-
|| typeof value.serviceUnavailable !== 'boolean'
|
|
432
|
-
|| typeof value.compatible !== 'boolean'
|
|
433
|
-
|| typeof value.parametersIgnored !== 'boolean'
|
|
434
|
-
|| !isJsonObject(value.parameters))
|
|
435
|
-
return false;
|
|
436
|
-
const parameters = value.parameters;
|
|
437
|
-
if (Object.keys(parameters).length > 24
|
|
438
|
-
|| !validUintArray(parameters.mandatory, 24)
|
|
439
|
-
|| !validStringArray(parameters.alpn, 16, 132)
|
|
440
|
-
|| typeof parameters.noDefaultAlpn !== 'boolean'
|
|
441
|
-
|| !(parameters.port === null || validUint(parameters.port, 0xffff))
|
|
442
|
-
|| !validStringArray(parameters.ipv4hint, 8, 64)
|
|
443
|
-
|| !validStringArray(parameters.ipv6hint, 8, 64)
|
|
444
|
-
|| !validUintArray(parameters.unknownKeys, 24)
|
|
445
|
-
|| !validUintArray(parameters.unsupportedMandatoryKeys, 24)
|
|
446
|
-
|| !Array.isArray(parameters.opaque)
|
|
447
|
-
|| parameters.opaque.length > 24)
|
|
448
|
-
return false;
|
|
449
|
-
return parameters.opaque.every((item) => isJsonObject(item)
|
|
450
|
-
&& validUint(item.key, 0xffff)
|
|
451
|
-
&& (item.name === null || typeof item.name === 'string' && /^[a-z0-9-]{1,63}$/u.test(item.name))
|
|
452
|
-
&& validUint(item.length, 0xffff));
|
|
453
|
-
}
|
|
454
|
-
function validTlsFinding(value) {
|
|
455
|
-
return isJsonObject(value)
|
|
456
|
-
&& validBoundedString(value.id, 80)
|
|
457
|
-
&& validBoundedString(value.tone, 32)
|
|
458
|
-
&& validBoundedString(value.label, 160)
|
|
459
|
-
&& validBoundedString(value.detail, 500);
|
|
460
|
-
}
|
|
461
|
-
function validTlsChainCertificate(value) {
|
|
462
|
-
if (!isJsonObject(value))
|
|
463
|
-
return false;
|
|
464
|
-
return (value.fingerprintSha256 === undefined
|
|
465
|
-
|| value.fingerprintSha256 === null
|
|
466
|
-
|| validBoundedString(value.fingerprintSha256, 64))
|
|
467
|
-
&& optionalTlsNameWithin(value.subject);
|
|
468
|
-
}
|
|
469
|
-
function validSecurityPostureFinding(value) {
|
|
470
|
-
return isJsonObject(value)
|
|
471
|
-
&& validBoundedString(value.id, 80)
|
|
472
|
-
&& validBoundedString(value.category, 80)
|
|
473
|
-
&& typeof value.state === 'string'
|
|
474
|
-
&& ['observed', 'potential_exposure', 'observed_absence', 'unavailable'].includes(value.state)
|
|
475
|
-
&& typeof value.tone === 'string'
|
|
476
|
-
&& ['configured', 'review', 'neutral'].includes(value.tone)
|
|
477
|
-
&& validBoundedString(value.label, 160)
|
|
478
|
-
&& validBoundedString(value.detail, 300)
|
|
479
|
-
&& validStringArray(value.evidence, 4, 120);
|
|
480
|
-
}
|
|
481
|
-
function validOptionalStringArray(value, maximumItems, maximumLength) {
|
|
482
|
-
return value === undefined || validStringArray(value, maximumItems, maximumLength);
|
|
483
|
-
}
|
|
484
|
-
function validOptionalRecordArray(value, maximumItems, validate) {
|
|
485
|
-
return value === undefined || Array.isArray(value)
|
|
486
|
-
&& value.length <= maximumItems
|
|
487
|
-
&& value.every(validate);
|
|
488
|
-
}
|
|
489
|
-
function validOptionalNullableText(value, maximumLength) {
|
|
490
|
-
return value === undefined || value === null || validBoundedString(value, maximumLength);
|
|
491
|
-
}
|
|
492
|
-
function validRdapLink(value) {
|
|
493
|
-
return isJsonObject(value)
|
|
494
|
-
&& validBoundedString(value.href, 2_048)
|
|
495
|
-
&& validOptionalNullableText(value.rel, 100)
|
|
496
|
-
&& validOptionalNullableText(value.type, 160)
|
|
497
|
-
&& validOptionalNullableText(value.title, 300);
|
|
498
|
-
}
|
|
499
|
-
function validRdapContact(value) {
|
|
500
|
-
if (!isJsonObject(value))
|
|
501
|
-
return false;
|
|
502
|
-
return validOptionalNullableText(value.handle, 200)
|
|
503
|
-
&& validOptionalNullableText(value.name, 300)
|
|
504
|
-
&& validOptionalNullableText(value.org, 300)
|
|
505
|
-
&& validOptionalNullableText(value.email, 320)
|
|
506
|
-
&& validOptionalNullableText(value.phone, 100)
|
|
507
|
-
&& validOptionalNullableText(value.address, 1_000)
|
|
508
|
-
&& validOptionalStringArray(value.roles, 12, 80)
|
|
509
|
-
&& validOptionalStringArray(value.names, 8, 300)
|
|
510
|
-
&& validOptionalStringArray(value.organizations, 8, 300)
|
|
511
|
-
&& validOptionalStringArray(value.emails, 8, 320)
|
|
512
|
-
&& validOptionalStringArray(value.phones, 8, 100)
|
|
513
|
-
&& validOptionalStringArray(value.addresses, 8, 1_000)
|
|
514
|
-
&& validOptionalRecordArray(value.publicIds, 20, (item) => isJsonObject(item)
|
|
515
|
-
&& validBoundedString(item.type, 160)
|
|
516
|
-
&& validBoundedString(item.identifier, 300))
|
|
517
|
-
&& validOptionalRecordArray(value.links, 10, validRdapLink);
|
|
518
|
-
}
|
|
519
|
-
function validRdapTextBlock(value) {
|
|
520
|
-
return isJsonObject(value)
|
|
521
|
-
&& validBoundedString(value.title, 160)
|
|
522
|
-
&& validOptionalNullableText(value.type, 160)
|
|
523
|
-
&& validStringArray(value.descriptions, 6, 800);
|
|
524
|
-
}
|
|
525
|
-
function validRdapParsed(value) {
|
|
526
|
-
if (!isJsonObject(value))
|
|
527
|
-
return false;
|
|
528
|
-
if (!validOptionalStringArray(value.statuses, 100, 160)
|
|
529
|
-
|| !validOptionalStringArray(value.nameservers, 200, 253)
|
|
530
|
-
|| !validOptionalStringArray(value.conformance, 50, 160)
|
|
531
|
-
|| !validOptionalStringArray(value.serverTruncationReasons, 8, 160)
|
|
532
|
-
|| !validOptionalStringArray(value.truncatedEntityRoles, 11, 80)
|
|
533
|
-
|| !validOptionalRecordArray(value.events, 100, (item) => isJsonObject(item)
|
|
534
|
-
&& validOptionalNullableText(item.action, 160)
|
|
535
|
-
&& validOptionalNullableText(item.date, 64))
|
|
536
|
-
|| !validOptionalRecordArray(value.links, 20, validRdapLink)
|
|
537
|
-
|| !validOptionalRecordArray(value.nameserverDetails, 200, (item) => isJsonObject(item)
|
|
538
|
-
&& validBoundedString(item.name, 253)
|
|
539
|
-
&& validStringArray(item.addresses, 20, 80))
|
|
540
|
-
|| !validOptionalRecordArray(value.dsData, 50, (item) => isJsonObject(item)
|
|
541
|
-
&& validUint(item.keyTag, 0xffff)
|
|
542
|
-
&& validUint(item.algorithm, 0xff)
|
|
543
|
-
&& validUint(item.digestType, 0xff)
|
|
544
|
-
&& validBoundedString(item.digest, 512))
|
|
545
|
-
|| !validOptionalRecordArray(value.notices, 12, validRdapTextBlock)
|
|
546
|
-
|| !validOptionalRecordArray(value.remarks, 12, validRdapTextBlock)
|
|
547
|
-
|| !validOptionalRecordArray(value.redactions, 100, (item) => isJsonObject(item)
|
|
548
|
-
&& ['name', 'method', 'reason'].every((key) => validOptionalNullableText(item[key], 500))
|
|
549
|
-
&& ['prePath', 'postPath', 'replacementPath']
|
|
550
|
-
.every((key) => validOptionalNullableText(item[key], 512)))
|
|
551
|
-
|| !validOptionalRecordArray(value.variants, 20, (group) => isJsonObject(group)
|
|
552
|
-
&& validOptionalNullableText(group.idnTable, 300)
|
|
553
|
-
&& validOptionalStringArray(group.relation, 20, 100)
|
|
554
|
-
&& validOptionalRecordArray(group.variantNames, 50, (name) => isJsonObject(name)
|
|
555
|
-
&& validOptionalNullableText(name.ldhName, 253)
|
|
556
|
-
&& validOptionalNullableText(name.unicodeName, 253))))
|
|
557
|
-
return false;
|
|
558
|
-
const entitiesByRole = value.entitiesByRole;
|
|
559
|
-
if (entitiesByRole !== undefined) {
|
|
560
|
-
if (!isJsonObject(entitiesByRole) || Object.keys(entitiesByRole).length > 11)
|
|
561
|
-
return false;
|
|
562
|
-
for (const [role, contacts] of Object.entries(entitiesByRole)) {
|
|
563
|
-
if (!['registrar', 'registrant', 'administrative', 'technical', 'billing', 'abuse', 'noc', 'reseller', 'sponsor', 'proxy', 'notifications'].includes(role)
|
|
564
|
-
|| !Array.isArray(contacts)
|
|
565
|
-
|| contacts.length > 5
|
|
566
|
-
|| !contacts.every(validRdapContact))
|
|
567
|
-
return false;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
return true;
|
|
571
|
-
}
|
|
572
|
-
function validWhoisParsed(value) {
|
|
573
|
-
if (!isJsonObject(value)
|
|
574
|
-
|| !validOptionalStringArray(value.statuses, 100, 160)
|
|
575
|
-
|| !validOptionalStringArray(value.nameservers, 200, 253)
|
|
576
|
-
|| !validOptionalStringArray(value.fieldsTruncated, 64, 80))
|
|
577
|
-
return false;
|
|
578
|
-
const contactsByRole = value.contactsByRole;
|
|
579
|
-
if (contactsByRole !== undefined) {
|
|
580
|
-
if (!isJsonObject(contactsByRole) || Object.keys(contactsByRole).length > 5)
|
|
581
|
-
return false;
|
|
582
|
-
for (const contacts of Object.values(contactsByRole)) {
|
|
583
|
-
if (!Array.isArray(contacts) || contacts.length > 1 || !contacts.every(validRdapContact))
|
|
584
|
-
return false;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return true;
|
|
588
|
-
}
|
|
589
|
-
function validRegistrationEvidence(value) {
|
|
590
|
-
const rdap = value.rdap;
|
|
591
|
-
const whois = value.whois;
|
|
592
|
-
if (!validSourceStatus(rdap) || !validSourceStatus(whois))
|
|
593
|
-
return false;
|
|
594
|
-
if (rdap.parsed !== undefined && rdap.parsed !== null && !validRdapParsed(rdap.parsed))
|
|
595
|
-
return false;
|
|
596
|
-
if (whois.parsed !== undefined && whois.parsed !== null && !validWhoisParsed(whois.parsed))
|
|
597
|
-
return false;
|
|
598
|
-
const registrar = rdap.registrarRdap;
|
|
599
|
-
return registrar === undefined || registrar === null || validSourceStatus(registrar)
|
|
600
|
-
&& (registrar.parsed === undefined || registrar.parsed === null || validRdapParsed(registrar.parsed));
|
|
601
|
-
}
|
|
602
|
-
function validPageEvidence(value) {
|
|
603
|
-
const availability = value.availability;
|
|
604
|
-
const page = availability.pageIdentity;
|
|
605
|
-
if (page !== undefined && page !== null) {
|
|
606
|
-
if (!validObservationFields(page)
|
|
607
|
-
|| !validOptionalStringArray(page.embeddedOrigins, 20, 2_048)
|
|
608
|
-
|| !validOptionalStringArray(page.contactDomains, 20, 253)
|
|
609
|
-
|| !validOptionalRecordArray(page.trackingIdentifiers, 30, (item) => isJsonObject(item)
|
|
610
|
-
&& validBoundedString(item.type, 80)
|
|
611
|
-
&& validBoundedString(item.value, 240)))
|
|
612
|
-
return false;
|
|
613
|
-
if (page.forms !== undefined && (!isJsonObject(page.forms)
|
|
614
|
-
|| !validOptionalStringArray(page.forms.externalActionOrigins, 10, 2_048)))
|
|
615
|
-
return false;
|
|
616
|
-
if (page.resources !== undefined && (!isJsonObject(page.resources)
|
|
617
|
-
|| !validOptionalStringArray(page.resources.externalOrigins, 30, 2_048)))
|
|
618
|
-
return false;
|
|
619
|
-
if (page.downloads !== undefined && (!isJsonObject(page.downloads)
|
|
620
|
-
|| !validOptionalStringArray(page.downloads.riskyFileTypes, 20, 80)
|
|
621
|
-
|| !validOptionalStringArray(page.downloads.externalOrigins, 20, 2_048)))
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
const technology = availability.technologyProfile;
|
|
625
|
-
if (technology !== undefined && technology !== null) {
|
|
626
|
-
if (!validObservationFields(technology)
|
|
627
|
-
|| !validOptionalRecordArray(technology.findings, 24, (finding) => isJsonObject(finding)
|
|
628
|
-
&& validOptionalRecordArray(finding.evidence, 4, (item) => isJsonObject(item)
|
|
629
|
-
&& validBoundedString(item.source, 80)
|
|
630
|
-
&& validBoundedString(item.description, 180))))
|
|
631
|
-
return false;
|
|
632
|
-
const libraries = technology.browserLibraryProfile;
|
|
633
|
-
if (libraries !== undefined && libraries !== null && (!validObservationFields(libraries)
|
|
634
|
-
|| !validOptionalRecordArray(libraries.findings, 16, (item) => isJsonObject(item))))
|
|
635
|
-
return false;
|
|
636
|
-
}
|
|
637
|
-
const structured = availability.structuredDataIdentity;
|
|
638
|
-
if (structured !== undefined && structured !== null && (!validObservationFields(structured)
|
|
639
|
-
|| !validOptionalRecordArray(structured.entities, 16, (entity) => isJsonObject(entity)
|
|
640
|
-
&& validOptionalStringArray(entity.types, 8, 160)
|
|
641
|
-
&& validOptionalStringArray(entity.sameAsHosts, 12, 253))))
|
|
642
|
-
return false;
|
|
643
|
-
const roles = availability.pageRoleProfile;
|
|
644
|
-
if (roles !== undefined && roles !== null && (!validObservationFields(roles)
|
|
645
|
-
|| !validOptionalRecordArray(roles.findings, 4, (finding) => isJsonObject(finding)
|
|
646
|
-
&& validOptionalStringArray(finding.evidence, 4, 300))))
|
|
647
|
-
return false;
|
|
648
|
-
const behavior = availability.clientBehaviorProfile;
|
|
649
|
-
if (behavior !== undefined && behavior !== null && (!validObservationFields(behavior)
|
|
650
|
-
|| !validOptionalRecordArray(behavior.indicators, 12, (item) => isJsonObject(item))))
|
|
651
|
-
return false;
|
|
652
|
-
const credential = availability.credentialSurfaceProfile;
|
|
653
|
-
return credential === undefined || credential === null || validObservationFields(credential);
|
|
654
|
-
}
|
|
655
|
-
function validAvailabilityScalars(value) {
|
|
656
|
-
return (value.applicable === undefined || typeof value.applicable === 'boolean')
|
|
657
|
-
&& validOptionalNullableText(value.domain, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
|
|
658
|
-
&& validOptionalNullableText(value.state, 40)
|
|
659
|
-
&& validOptionalNullableText(value.confidence, 40)
|
|
660
|
-
&& validOptionalNullableText(value.activityStatus, 40)
|
|
661
|
-
&& validOptionalNullableText(value.websiteProbeDetail, 500)
|
|
662
|
-
&& validOptionalNullableText(value.dnssec, 40)
|
|
663
|
-
&& (value.deepScanComplete === undefined || typeof value.deepScanComplete === 'boolean')
|
|
664
|
-
&& (value.hasMx === undefined || value.hasMx === null || typeof value.hasMx === 'boolean')
|
|
665
|
-
&& (value.hasNullMx === undefined || value.hasNullMx === null || typeof value.hasNullMx === 'boolean')
|
|
666
|
-
&& (value.hasSpf === undefined || value.hasSpf === null || typeof value.hasSpf === 'boolean')
|
|
667
|
-
&& (value.hasDmarc === undefined || value.hasDmarc === null || typeof value.hasDmarc === 'boolean')
|
|
668
|
-
&& validOptionalStringArray(value.mxHosts, MAX_LOOKUP_DNS_RECORDS_PER_TYPE, 253);
|
|
669
|
-
}
|
|
670
|
-
function optionalTlsNameWithin(value) {
|
|
671
|
-
if (value === undefined || value === null)
|
|
672
|
-
return true;
|
|
673
|
-
if (!isJsonObject(value))
|
|
674
|
-
return false;
|
|
675
|
-
return optionalStringArrayWithin(value.commonNames, MAX_LOOKUP_TLS_NAME_VALUES)
|
|
676
|
-
&& (!Array.isArray(value.commonNames)
|
|
677
|
-
|| value.commonNames.every((item) => validBoundedString(item, 256)))
|
|
678
|
-
&& optionalStringArrayWithin(value.organizations, MAX_LOOKUP_TLS_NAME_VALUES)
|
|
679
|
-
&& (!Array.isArray(value.organizations)
|
|
680
|
-
|| value.organizations.every((item) => validBoundedString(item, 256)));
|
|
681
|
-
}
|
|
682
|
-
function validNormalizedNetworkEvidence(value) {
|
|
683
|
-
const availability = value.availability;
|
|
684
|
-
const dns = availability.dns;
|
|
685
|
-
if (dns !== undefined) {
|
|
686
|
-
if (!validObservationFields(dns))
|
|
687
|
-
return false;
|
|
688
|
-
if (dns.records !== undefined) {
|
|
689
|
-
if (!isJsonObject(dns.records))
|
|
690
|
-
return false;
|
|
691
|
-
for (const [key, maximumLength] of [
|
|
692
|
-
['a', 64], ['aaaa', 64], ['cname', 253], ['ns', 253], ['spf', 1_024], ['dmarc', 1_024],
|
|
693
|
-
]) {
|
|
694
|
-
if (!optionalStringArrayWithin(dns.records[key], MAX_LOOKUP_DNS_RECORDS_PER_TYPE)
|
|
695
|
-
|| Array.isArray(dns.records[key])
|
|
696
|
-
&& !dns.records[key].every((item) => validBoundedString(item, maximumLength)))
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
if (dns.records.mx !== undefined && (!Array.isArray(dns.records.mx)
|
|
700
|
-
|| dns.records.mx.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
|
|
701
|
-
|| !dns.records.mx.every(validDnsMxRecord)))
|
|
702
|
-
return false;
|
|
703
|
-
if (dns.records.caa !== undefined && (!Array.isArray(dns.records.caa)
|
|
704
|
-
|| dns.records.caa.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
|
|
705
|
-
|| !dns.records.caa.every(validDnsCaaRecord)))
|
|
706
|
-
return false;
|
|
707
|
-
if (dns.records.soa !== undefined && (!Array.isArray(dns.records.soa)
|
|
708
|
-
|| dns.records.soa.length > 1
|
|
709
|
-
|| !dns.records.soa.every(validDnsSoaRecord)))
|
|
710
|
-
return false;
|
|
711
|
-
if (dns.records.https !== undefined && (!Array.isArray(dns.records.https)
|
|
712
|
-
|| dns.records.https.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
|
|
713
|
-
|| !dns.records.https.every(validDnsHttpsRecord)))
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
const reverseDns = value.reverseDns;
|
|
718
|
-
if (reverseDns !== undefined) {
|
|
719
|
-
if (!validObservationFields(reverseDns)
|
|
720
|
-
|| reverseDns.records !== undefined
|
|
721
|
-
&& (!isJsonObject(reverseDns.records)
|
|
722
|
-
|| !optionalStringArrayWithin(reverseDns.records.ptr, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS)
|
|
723
|
-
|| Array.isArray(reverseDns.records.ptr)
|
|
724
|
-
&& !reverseDns.records.ptr.every((item) => validBoundedString(item, 253))))
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
const tls = availability.tls;
|
|
728
|
-
if (tls !== undefined) {
|
|
729
|
-
if (!validObservationFields(tls)
|
|
730
|
-
|| !optionalRecordArrayWithin(tls.findings, MAX_LOOKUP_TLS_FINDINGS)
|
|
731
|
-
|| Array.isArray(tls.findings) && !tls.findings.every(validTlsFinding)
|
|
732
|
-
|| !optionalRecordArrayWithin(tls.chain, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES)
|
|
733
|
-
|| Array.isArray(tls.chain) && !tls.chain.every(validTlsChainCertificate))
|
|
734
|
-
return false;
|
|
735
|
-
const certificate = tls.certificate;
|
|
736
|
-
if (certificate !== undefined && certificate !== null) {
|
|
737
|
-
if (!isJsonObject(certificate)
|
|
738
|
-
|| !optionalTlsNameWithin(certificate.subject)
|
|
739
|
-
|| !optionalTlsNameWithin(certificate.issuer))
|
|
740
|
-
return false;
|
|
741
|
-
const altNames = certificate.subjectAltNames;
|
|
742
|
-
if (altNames !== undefined && altNames !== null) {
|
|
743
|
-
if (!isJsonObject(altNames)
|
|
744
|
-
|| !optionalStringArrayWithin(altNames.dnsNames, MAX_LOOKUP_TLS_ALT_NAMES)
|
|
745
|
-
|| Array.isArray(altNames.dnsNames)
|
|
746
|
-
&& !altNames.dnsNames.every((item) => validBoundedString(item, 253))
|
|
747
|
-
|| !optionalStringArrayWithin(altNames.ipAddresses, MAX_LOOKUP_TLS_ALT_NAMES)
|
|
748
|
-
|| Array.isArray(altNames.ipAddresses)
|
|
749
|
-
&& !altNames.ipAddresses.every((item) => validBoundedString(item, 64)))
|
|
750
|
-
return false;
|
|
751
|
-
const totalAltNames = (Array.isArray(altNames.dnsNames) ? altNames.dnsNames.length : 0)
|
|
752
|
-
+ (Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses.length : 0);
|
|
753
|
-
if (totalAltNames > MAX_LOOKUP_TLS_ALT_NAMES)
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
const extensionProfile = certificate.extensionProfile;
|
|
757
|
-
if (extensionProfile !== undefined && extensionProfile !== null) {
|
|
758
|
-
if (!isJsonObject(extensionProfile))
|
|
759
|
-
return false;
|
|
760
|
-
const certificatePolicies = extensionProfile.certificatePolicies;
|
|
761
|
-
if (certificatePolicies !== undefined && certificatePolicies !== null
|
|
762
|
-
&& (!isJsonObject(certificatePolicies)
|
|
763
|
-
|| !optionalStringArrayWithin(certificatePolicies.oids, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES)
|
|
764
|
-
|| Array.isArray(certificatePolicies.oids)
|
|
765
|
-
&& !certificatePolicies.oids.every((item) => validBoundedString(item, 128))))
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
for (const child of [tls.authorization, tls.hostname]) {
|
|
770
|
-
if (child !== undefined && child !== null && (!isJsonObject(child)
|
|
771
|
-
|| !validOptionalNullableText(child.error, 240)))
|
|
772
|
-
return false;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
const securityPosture = availability.securityPosture;
|
|
776
|
-
if (securityPosture !== undefined && securityPosture !== null) {
|
|
777
|
-
if (!validObservationFields(securityPosture)
|
|
778
|
-
|| !optionalRecordArrayWithin(securityPosture.findings, MAX_SECURITY_POSTURE_FINDINGS)
|
|
779
|
-
|| Array.isArray(securityPosture.findings)
|
|
780
|
-
&& !securityPosture.findings.every(validSecurityPostureFinding))
|
|
781
|
-
return false;
|
|
782
|
-
}
|
|
783
|
-
return true;
|
|
784
|
-
}
|
|
785
|
-
function parseLookupHttpResponse(value) {
|
|
786
|
-
try {
|
|
787
|
-
assertBoundedJsonStructure(value, 'Lookup response', {
|
|
788
|
-
maximumContainerItems: MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
catch {
|
|
792
|
-
return invalidLookupResponse();
|
|
793
|
-
}
|
|
794
|
-
if (!isJsonObject(value) || Object.keys(value).length > MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
|
|
795
|
-
return invalidLookupResponse();
|
|
796
|
-
}
|
|
797
|
-
if (typeof value.query !== 'string'
|
|
798
|
-
|| !value.query.trim()
|
|
799
|
-
|| value.query.length > MAX_LOOKUP_RESPONSE_QUERY_LENGTH
|
|
800
|
-
|| CONTROL_CHAR_RE.test(value.query)
|
|
801
|
-
|| typeof value.type !== 'string'
|
|
802
|
-
|| !QUERY_TYPES.has(value.type)
|
|
803
|
-
|| !isJsonObject(value.rdap)
|
|
804
|
-
|| !isJsonObject(value.whois)
|
|
805
|
-
|| !isJsonObject(value.availability)
|
|
806
|
-
|| !isJsonObject(value.diagnostics)
|
|
807
|
-
|| !optionalBoundedText(value.inputHostname, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
|
|
808
|
-
|| !optionalBoundedText(value.registrableDomain, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
|
|
809
|
-
|| (value.isSubdomain !== undefined && typeof value.isSubdomain !== 'boolean')) {
|
|
810
|
-
return invalidLookupResponse();
|
|
811
|
-
}
|
|
812
|
-
for (const key of ['reverseDns', 'networkContext', 'securityTxt', 'sslbl', 'threatIntelligence', 'registryInsights']) {
|
|
813
|
-
const section = value[key];
|
|
814
|
-
if (section !== undefined && !isJsonObject(section))
|
|
815
|
-
return invalidLookupResponse();
|
|
816
|
-
}
|
|
817
|
-
const lookupResponse = value;
|
|
818
|
-
if (!validAvailabilityScalars(value.availability)
|
|
819
|
-
|| !validRegistrationEvidence(lookupResponse))
|
|
820
|
-
return invalidLookupResponse();
|
|
821
|
-
if (value.availability.http !== undefined && !validNormalizedHttpEvidence(value.availability.http)) {
|
|
822
|
-
return invalidLookupResponse();
|
|
823
|
-
}
|
|
824
|
-
for (const section of [value.reverseDns, value.networkContext, value.securityTxt]) {
|
|
825
|
-
if (section !== undefined && !validObservationFields(section))
|
|
826
|
-
return invalidLookupResponse();
|
|
827
|
-
}
|
|
828
|
-
if (!validNormalizedNetworkEvidence(lookupResponse)
|
|
829
|
-
|| !validPageEvidence(lookupResponse))
|
|
830
|
-
return invalidLookupResponse();
|
|
831
|
-
return { ok: true, value: lookupResponse };
|
|
832
|
-
}
|
|
833
|
-
function parseCompactLookupHttpResponse(value, expectedDomain) {
|
|
834
|
-
try {
|
|
835
|
-
assertBoundedJsonStructure(value, 'Compact Lookup response');
|
|
836
|
-
}
|
|
837
|
-
catch {
|
|
838
|
-
return invalidCompactLookupResponse();
|
|
839
|
-
}
|
|
840
|
-
if (!isJsonObject(value) || Object.keys(value).length > MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
|
|
841
|
-
return invalidCompactLookupResponse();
|
|
842
|
-
}
|
|
843
|
-
const availability = value.availability;
|
|
844
|
-
const diagnostics = value.diagnostics;
|
|
845
|
-
if (!isJsonObject(availability)
|
|
846
|
-
|| Object.keys(availability).length > MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS
|
|
847
|
-
|| availability.applicable !== true
|
|
848
|
-
|| !compactDomainMatches(availability.domain, expectedDomain)
|
|
849
|
-
|| typeof availability.state !== 'string'
|
|
850
|
-
|| !COMPACT_AVAILABILITY_STATES.has(availability.state)
|
|
851
|
-
|| typeof availability.confidence !== 'string'
|
|
852
|
-
|| !COMPACT_CONFIDENCE_LEVELS.has(availability.confidence)
|
|
853
|
-
|| (availability.deepScanComplete !== undefined && typeof availability.deepScanComplete !== 'boolean')
|
|
854
|
-
|| (availability.bulkComparison !== undefined
|
|
855
|
-
&& !validCompactBulkComparison(availability.bulkComparison))
|
|
856
|
-
|| (value.query !== undefined && !compactDomainMatches(value.query, expectedDomain))
|
|
857
|
-
|| (value.type !== undefined && value.type !== 'domain')
|
|
858
|
-
|| (value.inputHostname !== undefined && !compactDomainMatches(value.inputHostname, expectedDomain))
|
|
859
|
-
|| (value.registrableDomain !== undefined
|
|
860
|
-
&& normalizedDomain(value.registrableDomain) !== normalizedDomain(availability.domain))
|
|
861
|
-
|| (value.isSubdomain !== undefined && typeof value.isSubdomain !== 'boolean')
|
|
862
|
-
|| !isJsonObject(diagnostics)
|
|
863
|
-
|| Object.keys(diagnostics).length > MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS
|
|
864
|
-
|| diagnostics.version !== 7
|
|
865
|
-
|| !isJsonObject(diagnostics.rdap)
|
|
866
|
-
|| !isJsonObject(diagnostics.whois)
|
|
867
|
-
|| !isJsonObject(diagnostics.availability)
|
|
868
|
-
|| typeof diagnostics.availability.status !== 'string'
|
|
869
|
-
|| !COMPACT_AVAILABILITY_DIAGNOSTIC_STATES.has(diagnostics.availability.status)) {
|
|
870
|
-
return invalidCompactLookupResponse();
|
|
871
|
-
}
|
|
872
|
-
return { ok: true, value: value };
|
|
873
|
-
}
|
|
874
|
-
function lookupHttpErrorMessage(value, status) {
|
|
875
|
-
const source = record(value);
|
|
876
|
-
const message = typeof source.error === 'string'
|
|
877
|
-
? source.error
|
|
878
|
-
.replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
|
879
|
-
.replace(/\s+/gu, ' ')
|
|
880
|
-
.trim()
|
|
881
|
-
.slice(0, MAX_LOOKUP_RESPONSE_ERROR_LENGTH)
|
|
882
|
-
: '';
|
|
883
|
-
return message || `Lookup failed (${status})`;
|
|
884
|
-
}
|
|
885
|
-
function isBoundedTimingMs(value, totalMs) {
|
|
886
|
-
return Number.isInteger(value)
|
|
887
|
-
&& Number(value) >= 0
|
|
888
|
-
&& Number(value) <= totalMs
|
|
889
|
-
&& Number(value) <= MAX_LOOKUP_TIMING_MS;
|
|
890
|
-
}
|
|
891
|
-
function normalizeLookupTiming(value) {
|
|
892
|
-
if (!isJsonObject(value)
|
|
893
|
-
|| value.version !== 1
|
|
894
|
-
|| !Number.isInteger(value.totalMs)
|
|
895
|
-
|| Number(value.totalMs) < 0
|
|
896
|
-
|| Number(value.totalMs) > MAX_LOOKUP_TIMING_MS
|
|
897
|
-
|| !Array.isArray(value.sources)
|
|
898
|
-
|| value.sources.length > MAX_LOOKUP_TIMING_SOURCES) {
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
const totalMs = Number(value.totalMs);
|
|
902
|
-
const seen = new Set();
|
|
903
|
-
const sources = [];
|
|
904
|
-
for (const candidate of value.sources) {
|
|
905
|
-
if (!isJsonObject(candidate)
|
|
906
|
-
|| typeof candidate.source !== 'string'
|
|
907
|
-
|| !LOOKUP_TIMING_SOURCES.has(candidate.source)
|
|
908
|
-
|| seen.has(candidate.source)
|
|
909
|
-
|| typeof candidate.outcome !== 'string'
|
|
910
|
-
|| !LOOKUP_TIMING_OUTCOMES.has(candidate.outcome)
|
|
911
|
-
|| !isBoundedTimingMs(candidate.durationMs, totalMs)
|
|
912
|
-
|| !isBoundedTimingMs(candidate.completedAfterMs, totalMs)
|
|
913
|
-
|| Number(candidate.durationMs) > Number(candidate.completedAfterMs)) {
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
const source = candidate.source;
|
|
917
|
-
seen.add(source);
|
|
918
|
-
sources.push({
|
|
919
|
-
source,
|
|
920
|
-
outcome: candidate.outcome,
|
|
921
|
-
durationMs: Number(candidate.durationMs),
|
|
922
|
-
completedAfterMs: Number(candidate.completedAfterMs),
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
return {
|
|
926
|
-
version: 1,
|
|
927
|
-
totalMs,
|
|
928
|
-
sources,
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
function createLookupHttpResponse(query, classified, result) {
|
|
932
|
-
return {
|
|
933
|
-
...result,
|
|
934
|
-
query,
|
|
935
|
-
type: classified.type,
|
|
936
|
-
inputHostname: classified.inputHostname,
|
|
937
|
-
registrableDomain: classified.registrableDomain,
|
|
938
|
-
isSubdomain: classified.isSubdomain,
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
function createLookupViewModel(response) {
|
|
942
|
-
const availability = record(response?.availability);
|
|
943
|
-
const rdap = record(response?.rdap);
|
|
944
|
-
const registrarRdap = record(rdap.registrarRdap);
|
|
945
|
-
const whois = record(response?.whois);
|
|
946
|
-
const diagnostics = record(response?.diagnostics);
|
|
947
|
-
const reverseDns = record(response?.reverseDns);
|
|
948
|
-
const observedNetworkContext = record(response?.networkContext);
|
|
949
|
-
const registryInsights = record(response?.registryInsights);
|
|
950
|
-
const securityTxt = record(response?.securityTxt);
|
|
951
|
-
const sslbl = record(response?.sslbl);
|
|
952
|
-
const threatIntelligence = record(response?.threatIntelligence);
|
|
953
|
-
const seenThreatIntelligenceProviders = new Set();
|
|
954
|
-
const providers = Array.isArray(threatIntelligence.providers)
|
|
955
|
-
? threatIntelligence.providers
|
|
956
|
-
.slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS * 2)
|
|
957
|
-
.map(normalizeThreatProvider)
|
|
958
|
-
.filter((provider) => provider !== null)
|
|
959
|
-
.filter((provider) => {
|
|
960
|
-
const providerId = String(record(provider.provider).id || '');
|
|
961
|
-
if (!providerId || seenThreatIntelligenceProviders.has(providerId))
|
|
962
|
-
return false;
|
|
963
|
-
seenThreatIntelligenceProviders.add(providerId);
|
|
964
|
-
return true;
|
|
965
|
-
})
|
|
966
|
-
.slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS)
|
|
967
|
-
: [];
|
|
968
|
-
const dnsEvidence = record(availability.dns);
|
|
969
|
-
const httpEvidence = record(availability.http);
|
|
970
|
-
const httpResponse = record(httpEvidence.response);
|
|
971
|
-
const tlsEvidence = record(availability.tls);
|
|
972
|
-
const tlsCertificate = record(tlsEvidence.certificate);
|
|
973
|
-
const pageIdentity = record(availability.pageIdentity);
|
|
974
|
-
const pageOpenGraph = record(pageIdentity.openGraph);
|
|
975
|
-
const pageResources = record(pageIdentity.resources);
|
|
976
|
-
const securityPosture = record(availability.securityPosture);
|
|
977
|
-
return {
|
|
978
|
-
availability,
|
|
979
|
-
rdap,
|
|
980
|
-
registrarRdap,
|
|
981
|
-
registrarRdapParsed: record(registrarRdap.parsed),
|
|
982
|
-
whois,
|
|
983
|
-
rdapParsed: record(rdap.parsed),
|
|
984
|
-
whoisParsed: record(whois.parsed),
|
|
985
|
-
diagnostics,
|
|
986
|
-
timing: normalizeLookupTiming(diagnostics.timing),
|
|
987
|
-
registryAccess: record(diagnostics.registryAccess),
|
|
988
|
-
registryInsights,
|
|
989
|
-
reverseDns,
|
|
990
|
-
reverseDnsRecords: record(reverseDns.records),
|
|
991
|
-
observedNetworkContext,
|
|
992
|
-
observedNetworkEndpoint: record(observedNetworkContext.endpoint),
|
|
993
|
-
observedNetworkRdap: record(observedNetworkContext.rdap),
|
|
994
|
-
observedNetwork: record(observedNetworkContext.network),
|
|
995
|
-
securityTxt,
|
|
996
|
-
sslbl,
|
|
997
|
-
threatIntelligence,
|
|
998
|
-
threatIntelligenceProviders: providers,
|
|
999
|
-
dnsEvidence,
|
|
1000
|
-
dnsRecords: record(dnsEvidence.records),
|
|
1001
|
-
httpEvidence,
|
|
1002
|
-
httpResponse,
|
|
1003
|
-
httpSecurityHeaders: record(httpResponse.securityHeaders),
|
|
1004
|
-
tlsEvidence,
|
|
1005
|
-
tlsCertificate,
|
|
1006
|
-
tlsSubject: record(tlsCertificate.subject),
|
|
1007
|
-
tlsIssuer: record(tlsCertificate.issuer),
|
|
1008
|
-
tlsAltNames: record(tlsCertificate.subjectAltNames),
|
|
1009
|
-
tlsPublicKey: record(tlsCertificate.publicKey),
|
|
1010
|
-
tlsCipher: record(tlsEvidence.cipher),
|
|
1011
|
-
tlsAuthorization: record(tlsEvidence.authorization),
|
|
1012
|
-
tlsHostname: record(tlsEvidence.hostname),
|
|
1013
|
-
tlsValidity: record(tlsEvidence.validity),
|
|
1014
|
-
tlsDiagnostics: record(tlsEvidence.diagnostics),
|
|
1015
|
-
pageIdentity,
|
|
1016
|
-
pageCanonical: record(pageIdentity.canonical),
|
|
1017
|
-
pageMetaRefresh: record(pageIdentity.metaRefresh),
|
|
1018
|
-
pageOpenGraph,
|
|
1019
|
-
pageOpenGraphUrl: record(pageOpenGraph.url),
|
|
1020
|
-
pageForms: record(pageIdentity.forms),
|
|
1021
|
-
pageResources,
|
|
1022
|
-
pageResourceTypes: record(pageResources.byType),
|
|
1023
|
-
pageDownloads: record(pageIdentity.downloads),
|
|
1024
|
-
pageFingerprints: record(pageIdentity.fingerprints),
|
|
1025
|
-
credentialSurfaceProfile: record(availability.credentialSurfaceProfile),
|
|
1026
|
-
structuredDataIdentity: record(availability.structuredDataIdentity),
|
|
1027
|
-
technologyProfile: record(availability.technologyProfile),
|
|
1028
|
-
pageRoleProfile: record(availability.pageRoleProfile),
|
|
1029
|
-
clientBehaviorProfile: record(availability.clientBehaviorProfile),
|
|
1030
|
-
securityPosture,
|
|
1031
|
-
securityPostureSummary: record(securityPosture.summary),
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
export { INVALID_COMPACT_LOOKUP_RESPONSE, INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE, INVALID_LOOKUP_RESPONSE, INVALID_LOOKUP_RESPONSE_MESSAGE, MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS, MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS, MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_RESPONSE_ERROR_LENGTH, MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS, MAX_LOOKUP_RESPONSE_HOST_LENGTH, MAX_LOOKUP_RESPONSE_QUERY_LENGTH, MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, MAX_LOOKUP_TIMING_MS, MAX_LOOKUP_TIMING_SOURCES, MAX_THREAT_INTELLIGENCE_PROVIDERS, MAX_THREAT_INTELLIGENCE_FINDINGS, MAX_THREAT_INTELLIGENCE_LIMITATIONS, validNormalizedHttpEvidence, createLookupHttpResponse, createLookupViewModel, isJsonObject, lookupHttpErrorMessage, normalizeLookupTiming, parseCompactLookupHttpResponse, parseLookupHttpResponse, record as lookupRecord, };
|