@slicedearth/whoisleuth-cli 1.45.0 → 1.47.4
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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +59 -6
- package/bin/whoisleuth.mjs +5 -3
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +2 -3
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +16 -8
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-scan-command-runner.mjs +4 -5
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/terminal.mjs +3 -0
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +147 -185
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +444 -70
- package/docs/cli.md +24 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +29 -5
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +8 -0
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +42 -12
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +61 -6
- package/frontend/src/lib/analysis/lookup-presentation.js +62 -0
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +26 -3
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive.js +104 -14
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +13 -0
- package/lib/bounded-json-response.mjs +17 -4
- package/lib/bounded-json.mjs +249 -0
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +59 -10
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +3 -11
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
package/lib/evidence-export.mjs
CHANGED
|
@@ -1,25 +1,441 @@
|
|
|
1
1
|
import { compareRdapPublications, compareRegistrySources } from "./registry-comparison.mjs";
|
|
2
2
|
import { buildRegistryInsights } from "./registry-insights.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { buildPortableGeneratorMetadata } from "./portable-generator.mjs";
|
|
4
|
+
import { assertBoundedJsonStructure, isSafeJsonObjectKey } from "./bounded-json.mjs";
|
|
5
5
|
export const LOOKUP_EVIDENCE_SCHEMA = 'whoisleuth.lookup-evidence';
|
|
6
|
-
export const
|
|
6
|
+
export const LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION = 25;
|
|
7
|
+
export const LOOKUP_EVIDENCE_SCHEMA_VERSION = 26;
|
|
8
|
+
export const SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS = Object.freeze([
|
|
9
|
+
LEGACY_LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
10
|
+
LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
11
|
+
]);
|
|
12
|
+
export const LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
export const LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES = 20_000;
|
|
14
|
+
export const LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH = 24;
|
|
15
|
+
export const LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS = 10_000;
|
|
16
|
+
export const LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH = 256;
|
|
17
|
+
export const LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH = 1024 * 1024;
|
|
7
18
|
const REGISTRAR_RDAP_STATUSES = new Set([
|
|
8
19
|
'success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
9
20
|
]);
|
|
21
|
+
const LOOKUP_QUERY_TYPES = new Set(['domain', 'ipv4', 'ipv6', 'asn']);
|
|
22
|
+
const RDAP_SOURCE_STATUSES = new Set([
|
|
23
|
+
'success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
24
|
+
]);
|
|
25
|
+
const WHOIS_SOURCE_STATUSES = new Set([
|
|
26
|
+
'complete', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
|
|
27
|
+
]);
|
|
28
|
+
const AVAILABILITY_DIAGNOSTIC_STATUSES = new Set(['complete', 'error', 'disabled', 'not_applicable']);
|
|
10
29
|
const NETWORK_CONTEXT_STATUSES = new Set(['success', 'partial', 'not_found', 'unsupported', 'error']);
|
|
11
30
|
const NETWORK_ADDRESS_SOURCES = new Set(['tls_connection', 'dns_a', 'dns_aaaa']);
|
|
12
31
|
const REVERSE_DNS_STATUSES = new Set(['success', 'partial', 'not_found', 'unsupported', 'skipped', 'error']);
|
|
13
32
|
const SECURITY_TXT_STATES = new Set(['present', 'stale', 'partial', 'absent', 'malformed', 'unsupported', 'unavailable']);
|
|
14
33
|
const SSLBL_STATES = new Set(['success', 'stale', 'unavailable']);
|
|
15
34
|
const SSLBL_VERDICTS = new Set(['listed', 'not_listed', 'inconclusive']);
|
|
35
|
+
const RDAP_ATTEMPT_OUTCOMES = new Set([
|
|
36
|
+
'success', 'not_found', 'no_results', 'unsupported', 'rate_limited',
|
|
37
|
+
'server_error', 'client_error', 'invalid_json', 'invalid_response',
|
|
38
|
+
'timeout', 'network_error',
|
|
39
|
+
]);
|
|
40
|
+
const WHOIS_QUERY_PROFILES = new Set([
|
|
41
|
+
'plain-domain', 'denic-domain-ace', 'jprs-domain-english',
|
|
42
|
+
'registry-domain-unicode', 'not-issued',
|
|
43
|
+
]);
|
|
44
|
+
const LOOKUP_TIMING_SOURCES = new Set([
|
|
45
|
+
'rdap', 'whois', 'domain_evidence', 'reverse_dns', 'registrar_rdap',
|
|
46
|
+
'network_context', 'security_txt', 'external_intelligence',
|
|
47
|
+
'malware_host_intelligence', 'malware_ioc_intelligence',
|
|
48
|
+
]);
|
|
49
|
+
const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
50
|
+
'applicable', 'type', 'domain', 'state', 'confidence', 'detail', 'source',
|
|
51
|
+
'rdapServer', 'nameservers', 'statuses', 'registrar', 'registrant', 'abuse',
|
|
52
|
+
'createdDate', 'expiryDate', 'createdDateIso', 'expiryDateIso', 'domainAgeDays',
|
|
53
|
+
'expiresInDays', 'privacyProtected', 'dnssec', 'activityStatus',
|
|
54
|
+
'websiteProbeStatus', 'websiteProbeDetail', 'http', 'deepScanComplete',
|
|
55
|
+
'faviconHash', 'faviconPHash', 'pageTitle', 'hasPasswordField',
|
|
56
|
+
'phishingLanguageMatch', 'hasExternalFormAction', 'externalAssetHosts',
|
|
57
|
+
'pageIdentity', 'credentialSurfaceProfile', 'structuredDataIdentity',
|
|
58
|
+
'technologyProfile', 'pageRoleProfile', 'clientBehaviorProfile',
|
|
59
|
+
'securityPosture', 'dns', 'tls', 'hasMx', 'hasNullMx', 'mxHosts', 'hasSpf',
|
|
60
|
+
'hasDmarc', 'bulkComparison', 'limitations',
|
|
61
|
+
]);
|
|
62
|
+
const LOOKUP_IDN_ANALYSIS_KEYS = new Set([
|
|
63
|
+
'version', 'mappingVersion', 'asciiDomain', 'unicodeDomain', 'hasIdn',
|
|
64
|
+
'scripts', 'labels', 'mixedScript', 'skeleton', 'referenceMatches', 'findings',
|
|
65
|
+
'truncated', 'limitations',
|
|
66
|
+
]);
|
|
67
|
+
const PRIVATE_EVIDENCE_KEYS = new Set([
|
|
68
|
+
'authorization', 'proxyauthorization', 'cookie', 'cookies', 'setcookie',
|
|
69
|
+
'session', 'sessionid', 'sessiontoken', 'token', 'accesstoken', 'refreshtoken',
|
|
70
|
+
'apikey', 'password', 'secret', 'credentials', 'requestheaders', 'responseheaders',
|
|
71
|
+
'headers', 'rawbody', 'responsebody',
|
|
72
|
+
]);
|
|
73
|
+
const SAFE_EVIDENCE_KEYS = new Set([
|
|
74
|
+
'credentialsurfaceprofile', 'credentialsurfaceversion', 'haspasswordfield', 'tokencount',
|
|
75
|
+
]);
|
|
76
|
+
const PORTABLE_URL_KEYS = new Set([
|
|
77
|
+
'endpoint', 'url', 'uri', 'href', 'rdapserver', 'finalurl', 'requestedurl',
|
|
78
|
+
'requesturl', 'officiallookupurl', 'declaredorigin', 'actionurl', 'from', 'to',
|
|
79
|
+
]);
|
|
80
|
+
const PORTABLE_ORIGIN_COLLECTION_KEYS = new Set([
|
|
81
|
+
'embeddedorigins', 'externalactionorigins', 'externalorigins',
|
|
82
|
+
]);
|
|
83
|
+
// Availability is assembled from a finite set of versioned, bounded analysis
|
|
84
|
+
// models. Portable exports retain only their declared output vocabulary rather
|
|
85
|
+
// than recursively copying arbitrary browser-local extensions. This set is
|
|
86
|
+
// intentionally exact and case-sensitive; new model fields must be reviewed at
|
|
87
|
+
// this privacy boundary before they can enter a portable artefact.
|
|
88
|
+
const LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS = new Set([
|
|
89
|
+
// Shared observation and summary fields.
|
|
90
|
+
'version', 'profileVersion', 'status', 'state', 'source', 'observedAt',
|
|
91
|
+
'scanMode', 'durationMs', 'complete', 'truncated', 'limitations',
|
|
92
|
+
'diagnostics', 'detail', 'error', 'count', 'total', 'discarded', 'summary',
|
|
93
|
+
'findings', 'id', 'label', 'tone', 'category', 'confidence', 'evidence',
|
|
94
|
+
'value', 'values', 'name', 'type', 'types', 'mode', 'owner', 'target',
|
|
95
|
+
'url', 'uri', 'href',
|
|
96
|
+
'priority', 'ttl', 'records',
|
|
97
|
+
// HTTP observation and retained response metadata.
|
|
98
|
+
'requestUrl', 'finalUrl', 'transportSecurity', 'redirectCount',
|
|
99
|
+
'redirectLimitReached', 'redirects', 'crossOriginRedirect', 'httpsDowngrade',
|
|
100
|
+
'attempts', 'response', 'contentType', 'contentLanguage', 'server',
|
|
101
|
+
'declaredContentLength', 'capturedBodyBytes', 'bodyInspected', 'bodyTruncated',
|
|
102
|
+
'bodyHash', 'securityHeaders', 'strictTransportSecurity',
|
|
103
|
+
'contentSecurityPolicy', 'xFrameOptions', 'xContentTypeOptions',
|
|
104
|
+
'referrerPolicy', 'algorithm', 'scope', 'bytes', 'from', 'to',
|
|
105
|
+
'queryOmitted', 'pathTruncated', 'outcome', 'httpStatus', 'attemptCount',
|
|
106
|
+
// DNS observation, effective CAA, and HTTPS/SVCB parameter summaries.
|
|
107
|
+
'a', 'aaaa', 'caa', 'cname', 'dmarc', 'https', 'mx', 'ns', 'ptr', 'soa',
|
|
108
|
+
'spf', 'delegation', 'caaPolicy', 'policyVersion', 'queryLimit',
|
|
109
|
+
'queriedOwners', 'effectiveOwner', 'inherited', 'tree', 'registryEvidence',
|
|
110
|
+
'exchange', 'nsname', 'hostmaster', 'serial', 'refresh', 'retry', 'expire', 'minttl',
|
|
111
|
+
'tag', 'critical', 'parameters', 'parametersIgnored', 'mandatory', 'alpn',
|
|
112
|
+
'noDefaultAlpn', 'port', 'ipv4hint', 'ipv6hint', 'opaque', 'key', 'length',
|
|
113
|
+
'unknownKeys', 'unsupportedMandatoryKeys', 'targetIsOwner',
|
|
114
|
+
'serviceUnavailable', 'compatible', 'effectiveOwner', 'queriedOwners',
|
|
115
|
+
// TLS profile, certificate, extension, and connection summaries. Runtime
|
|
116
|
+
// authorisation objects are deliberately absent from this portable set.
|
|
117
|
+
'connectedAddress', 'connectedFamily', 'sniHost', 'protocol', 'alpnProtocol',
|
|
118
|
+
'cipher', 'ephemeralKey', 'hostname', 'matches', 'validity', 'certificate',
|
|
119
|
+
'chain', 'chainTruncated', 'standardName', 'size', 'subject', 'issuer',
|
|
120
|
+
'serialNumber', 'validFrom', 'validTo', 'fingerprintSha1',
|
|
121
|
+
'fingerprintSha256', 'isCertificateAuthority', 'subjectAltNames',
|
|
122
|
+
'dnsNames', 'ipAddresses', 'otherNames', 'publicKey', 'bits', 'curve',
|
|
123
|
+
'signature', 'oid', 'extendedKeyUsage', 'authorityInformationAccess',
|
|
124
|
+
'ocsp', 'caIssuers', 'unknownMethods', 'extensionProfile', 'parsed', 'partial',
|
|
125
|
+
'certificatePolicies', 'crlDistributionPoints', 'commonNames',
|
|
126
|
+
'organizations', 'organizationalUnits', 'countries', 'localities', 'states',
|
|
127
|
+
'connectionAttempts', 'resolvedAddressCount', 'discardedFields',
|
|
128
|
+
// Page identity, relationships, and bounded fingerprints.
|
|
129
|
+
'identityVersion', 'documentLanguage', 'canonical', 'metaRefresh',
|
|
130
|
+
'openGraph', 'title', 'siteName', 'generator', 'forms', 'postCount',
|
|
131
|
+
'insecureActionCount', 'externalActionOrigins', 'resources', 'byType',
|
|
132
|
+
'image', 'script', 'stylesheet', 'link', 'frame', 'media', 'object',
|
|
133
|
+
'externalOrigins', 'embeddedOrigins', 'contactDomains', 'downloads',
|
|
134
|
+
'explicitCount', 'riskyCount', 'riskyFileTypes', 'trackingIdentifiers',
|
|
135
|
+
'fingerprints', 'fingerprintVersion', 'exact', 'normalizedHtml',
|
|
136
|
+
'visibleText', 'domStructure', 'formStructure', 'resourceHosts',
|
|
137
|
+
'identifiers', 'featureCount', 'nodeCount', 'formCount', 'controlCount',
|
|
138
|
+
'parser', 'tokenCount', 'tagsExamined', 'discardedUrls', 'formsObserved',
|
|
139
|
+
'relationshipTagsExamined', 'relationshipUrlsDiscarded',
|
|
140
|
+
'trackingIdentifiersTruncated',
|
|
141
|
+
// Credential, structured identity, role, behaviour, technology, and posture
|
|
142
|
+
// projections. Only fixed categories/counts and bounded descriptions enter.
|
|
143
|
+
'credentialSurfaceVersion', 'methods', 'actions', 'missing', 'get', 'post',
|
|
144
|
+
'dialog', 'other', 'sameOrigin', 'external', 'cleartext', 'unclassified',
|
|
145
|
+
'inputs', 'classifiedCount', 'categories', 'password', 'email', 'username',
|
|
146
|
+
'one_time_code', 'payment', 'inputsObserved', 'classifiedInputs',
|
|
147
|
+
'unclassifiedActions', 'structuredDataVersion', 'scriptsObserved',
|
|
148
|
+
'scriptsExamined', 'charactersExamined', 'documentsParsed', 'malformedScripts',
|
|
149
|
+
'externalScriptsSkipped', 'objectsExamined', 'arrayItemsExamined',
|
|
150
|
+
'discardedProperties', 'entities', 'declaredOrigin', 'sameAsHosts',
|
|
151
|
+
'pageRoleProfileVersion', 'primaryRole', 'role', 'rolesObserved',
|
|
152
|
+
'clientBehaviorProfileVersion', 'scriptSummary', 'elementsObserved',
|
|
153
|
+
'referencedScripts', 'inlineScripts', 'moduleScripts', 'indicators',
|
|
154
|
+
'evidenceClass', 'occurrences', 'explanation', 'indicatorsObserved',
|
|
155
|
+
'scriptElementsExamined', 'inlineCharactersExamined', 'technologyProfile',
|
|
156
|
+
'browserLibraryProfile', 'catalog', 'knownExploitedCatalog', 'releasedAt',
|
|
157
|
+
'sourceRevision', 'apparentVersion', 'detectionMethods', 'advisoryCount',
|
|
158
|
+
'highestSeverity', 'advisoryIdentifiers', 'knownExploitedCount',
|
|
159
|
+
'knownExploitedIdentifiers', 'weaknessClasses', 'htmlEvaluated',
|
|
160
|
+
'generatorEvaluated', 'serverEvaluated', 'resourceOriginsEvaluated',
|
|
161
|
+
'passiveHeadersEvaluated', 'tagLimitReached', 'referencesExamined',
|
|
162
|
+
'inlineScriptsExamined', 'inlineSignatureCharactersExamined',
|
|
163
|
+
'inlineSignatureTimedOut', 'inlineSignatureUnavailable', 'catalogComponents',
|
|
164
|
+
'advisoryMatches', 'knownExploitedMatches', 'postureVersion',
|
|
165
|
+
'potentialExposure', 'observedAbsence', 'unavailable',
|
|
166
|
+
// Bounded cross-row comparison evidence.
|
|
167
|
+
'bulkComparison', 'technology', 'tls', 'ids', 'issuerLabel', 'spkiSha256',
|
|
168
|
+
]);
|
|
16
169
|
function recordOrNull(value) {
|
|
17
170
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
18
171
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
172
|
+
function normalizedEvidenceKey(value) {
|
|
173
|
+
return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
174
|
+
}
|
|
175
|
+
function privateEvidenceKey(value, item) {
|
|
176
|
+
const normalized = normalizedEvidenceKey(value);
|
|
177
|
+
if (SAFE_EVIDENCE_KEYS.has(normalized))
|
|
178
|
+
return false;
|
|
179
|
+
if (normalized === 'password' && (typeof item === 'number' || typeof item === 'boolean'))
|
|
180
|
+
return false;
|
|
181
|
+
return PRIVATE_EVIDENCE_KEYS.has(normalized)
|
|
182
|
+
|| normalized.includes('authorization')
|
|
183
|
+
|| normalized.includes('authheader')
|
|
184
|
+
|| normalized.includes('cookie')
|
|
185
|
+
|| normalized.includes('apikey')
|
|
186
|
+
|| normalized.includes('secret')
|
|
187
|
+
|| normalized.includes('sessionkey')
|
|
188
|
+
|| normalized.includes('passwordvalue')
|
|
189
|
+
|| normalized.endsWith('password')
|
|
190
|
+
|| /(?:access|auth|bearer|csrf|refresh|session)token$/u.test(normalized)
|
|
191
|
+
|| /(?:request|response|authorization|proxy)headers?$/u.test(normalized)
|
|
192
|
+
|| (normalized.includes('credential') && !normalized.startsWith('credentialsurface'));
|
|
193
|
+
}
|
|
194
|
+
function portableUri(value) {
|
|
195
|
+
const text = boundedString(value, 2048);
|
|
196
|
+
if (!text)
|
|
197
|
+
return null;
|
|
198
|
+
try {
|
|
199
|
+
const url = new URL(text);
|
|
200
|
+
if (!['http:', 'https:', 'mailto:', 'tel:', 'dns:', 'openpgp4fpr:'].includes(url.protocol)
|
|
201
|
+
|| url.username
|
|
202
|
+
|| url.password)
|
|
203
|
+
return null;
|
|
204
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
205
|
+
url.search = '';
|
|
206
|
+
url.hash = '';
|
|
207
|
+
}
|
|
208
|
+
return url.toString().slice(0, 2048);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function portableOrigin(value) {
|
|
215
|
+
const text = boundedString(value, 2048);
|
|
216
|
+
if (!text)
|
|
217
|
+
return null;
|
|
218
|
+
try {
|
|
219
|
+
const url = new URL(text);
|
|
220
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password
|
|
221
|
+
? url.origin
|
|
222
|
+
: null;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
21
225
|
return null;
|
|
22
|
-
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function consumePortableProjectionEntry(state, depth) {
|
|
229
|
+
state.entries += 1;
|
|
230
|
+
if (state.entries > LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES) {
|
|
231
|
+
throw new TypeError(`Lookup evidence projection exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES.toLocaleString('en')}-entry portable limit.`);
|
|
232
|
+
}
|
|
233
|
+
if (depth > LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH) {
|
|
234
|
+
throw new TypeError(`Lookup evidence projection exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH}-level portable nesting limit.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function projectPortableString(value) {
|
|
238
|
+
const sanitized = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, ' ');
|
|
239
|
+
return sanitized.length <= LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH
|
|
240
|
+
? sanitized
|
|
241
|
+
: sanitized.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH);
|
|
242
|
+
}
|
|
243
|
+
function projectLookupEvidencePrivacySafeTreeValue(value, state, depth) {
|
|
244
|
+
consumePortableProjectionEntry(state, depth);
|
|
245
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
246
|
+
return value;
|
|
247
|
+
if (typeof value === 'string')
|
|
248
|
+
return projectPortableString(value);
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
251
|
+
.map((item) => projectLookupEvidencePrivacySafeTreeValue(item, state, depth + 1));
|
|
252
|
+
}
|
|
253
|
+
const source = recordOrNull(value);
|
|
254
|
+
if (!source)
|
|
255
|
+
return null;
|
|
256
|
+
const output = {};
|
|
257
|
+
for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
|
|
258
|
+
if (!isSafeJsonObjectKey(key))
|
|
259
|
+
throw new TypeError('Lookup evidence projection contains an unsafe object key.');
|
|
260
|
+
const normalized = normalizedEvidenceKey(key);
|
|
261
|
+
if (privateEvidenceKey(normalized, item))
|
|
262
|
+
continue;
|
|
263
|
+
if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized)) {
|
|
264
|
+
consumePortableProjectionEntry(state, depth + 1);
|
|
265
|
+
output[key] = portableUri(item);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
output[key] = projectLookupEvidencePrivacySafeTreeValue(item, state, depth + 1);
|
|
269
|
+
}
|
|
270
|
+
return output;
|
|
271
|
+
}
|
|
272
|
+
export function projectLookupEvidencePrivacySafeTree(value) {
|
|
273
|
+
return projectLookupEvidencePrivacySafeTreeValue(value, { entries: 0 }, 0);
|
|
274
|
+
}
|
|
275
|
+
function projectLookupEvidenceAvailabilityValue(value, parentKey, state, depth) {
|
|
276
|
+
consumePortableProjectionEntry(state, depth);
|
|
277
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
278
|
+
return value;
|
|
279
|
+
if (typeof value === 'string') {
|
|
280
|
+
const normalizedParent = normalizedEvidenceKey(parentKey || '');
|
|
281
|
+
if (PORTABLE_URL_KEYS.has(normalizedParent))
|
|
282
|
+
return portableUri(value);
|
|
283
|
+
if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
|
|
284
|
+
return portableOrigin(value);
|
|
285
|
+
return projectPortableString(value);
|
|
286
|
+
}
|
|
287
|
+
if (Array.isArray(value)) {
|
|
288
|
+
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
289
|
+
.map((item) => projectLookupEvidenceAvailabilityValue(item, parentKey, state, depth + 1));
|
|
290
|
+
}
|
|
291
|
+
const source = recordOrNull(value);
|
|
292
|
+
if (!source)
|
|
293
|
+
return null;
|
|
294
|
+
const output = {};
|
|
295
|
+
for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
|
|
296
|
+
if (!LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS.has(key)
|
|
297
|
+
|| privateEvidenceKey(key, item))
|
|
298
|
+
continue;
|
|
299
|
+
output[key] = projectLookupEvidenceAvailabilityValue(item, key, state, depth + 1);
|
|
300
|
+
}
|
|
301
|
+
return output;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Canonical portable projection for the bounded Lookup availability models.
|
|
305
|
+
* The root and every nested record are positive allowlists, so an imported
|
|
306
|
+
* extension cannot become authenticated evidence merely by avoiding a
|
|
307
|
+
* credential-key denylist.
|
|
308
|
+
*/
|
|
309
|
+
export function projectLookupEvidenceAvailability(value) {
|
|
310
|
+
const source = recordOrNull(value);
|
|
311
|
+
if (!source)
|
|
312
|
+
return null;
|
|
313
|
+
const state = { entries: 0 };
|
|
314
|
+
consumePortableProjectionEntry(state, 0);
|
|
315
|
+
const output = {};
|
|
316
|
+
for (const key of LOOKUP_AVAILABILITY_ANALYSIS_KEYS) {
|
|
317
|
+
if (!Object.hasOwn(source, key))
|
|
318
|
+
continue;
|
|
319
|
+
const item = source[key];
|
|
320
|
+
if (privateEvidenceKey(key, item))
|
|
321
|
+
continue;
|
|
322
|
+
output[key] = projectLookupEvidenceAvailabilityValue(item, key, state, 1);
|
|
323
|
+
}
|
|
324
|
+
return output;
|
|
325
|
+
}
|
|
326
|
+
export function assertLookupEvidencePrivacySafeTree(value) {
|
|
327
|
+
assertLookupEvidencePortableTree(value);
|
|
328
|
+
const pending = [value];
|
|
329
|
+
while (pending.length) {
|
|
330
|
+
const current = pending.pop();
|
|
331
|
+
if (current === null || typeof current === 'boolean' || typeof current === 'number')
|
|
332
|
+
continue;
|
|
333
|
+
if (typeof current === 'string') {
|
|
334
|
+
if (projectLookupEvidencePrivacySafeTree(current) !== current) {
|
|
335
|
+
throw new TypeError('Lookup evidence contains a non-portable string value.');
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(current)) {
|
|
340
|
+
for (const item of current)
|
|
341
|
+
pending.push(item);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const source = recordOrNull(current);
|
|
345
|
+
if (!source)
|
|
346
|
+
throw new TypeError('Lookup evidence contains a non-JSON value.');
|
|
347
|
+
for (const [key, item] of Object.entries(source)) {
|
|
348
|
+
const normalized = normalizedEvidenceKey(key);
|
|
349
|
+
if (privateEvidenceKey(normalized, item)) {
|
|
350
|
+
throw new TypeError('Lookup evidence contains excluded private request or session material.');
|
|
351
|
+
}
|
|
352
|
+
if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized) && portableUri(item) !== item) {
|
|
353
|
+
throw new TypeError('Lookup evidence contains a credential-bearing or non-portable URL.');
|
|
354
|
+
}
|
|
355
|
+
pending.push(item);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function projectedKnownRecord(value, keys) {
|
|
360
|
+
const source = recordOrNull(value);
|
|
361
|
+
if (!source)
|
|
362
|
+
return null;
|
|
363
|
+
const output = {};
|
|
364
|
+
for (const key of keys) {
|
|
365
|
+
if (Object.hasOwn(source, key))
|
|
366
|
+
output[key] = projectLookupEvidencePrivacySafeTree(source[key]);
|
|
367
|
+
}
|
|
368
|
+
return output;
|
|
369
|
+
}
|
|
370
|
+
export function assertLookupEvidencePortableTree(value) {
|
|
371
|
+
const pending = [{ value, depth: 0 }];
|
|
372
|
+
let entries = 0;
|
|
373
|
+
while (pending.length) {
|
|
374
|
+
const current = pending.pop();
|
|
375
|
+
if (!current)
|
|
376
|
+
break;
|
|
377
|
+
entries += 1;
|
|
378
|
+
if (entries > LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES) {
|
|
379
|
+
throw new TypeError(`Lookup evidence exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES.toLocaleString('en')}-entry portable limit.`);
|
|
380
|
+
}
|
|
381
|
+
if (current.depth > LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH) {
|
|
382
|
+
throw new TypeError(`Lookup evidence exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH}-level portable nesting limit.`);
|
|
383
|
+
}
|
|
384
|
+
if (typeof current.value === 'string') {
|
|
385
|
+
if (current.value.length > LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH) {
|
|
386
|
+
throw new TypeError('Lookup evidence contains an over-bound string.');
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (current.value === null || typeof current.value === 'boolean')
|
|
391
|
+
continue;
|
|
392
|
+
if (typeof current.value === 'number') {
|
|
393
|
+
if (!Number.isFinite(current.value)) {
|
|
394
|
+
throw new TypeError('Lookup evidence contains a non-finite number.');
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (Array.isArray(current.value)) {
|
|
399
|
+
if (current.value.length > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS) {
|
|
400
|
+
throw new TypeError('Lookup evidence contains an over-bound array.');
|
|
401
|
+
}
|
|
402
|
+
for (const item of current.value)
|
|
403
|
+
pending.push({ value: item, depth: current.depth + 1 });
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (current.value && typeof current.value === 'object') {
|
|
407
|
+
const source = current.value;
|
|
408
|
+
const prototype = Object.getPrototypeOf(source);
|
|
409
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
410
|
+
throw new TypeError('Lookup evidence contains a non-JSON object.');
|
|
411
|
+
}
|
|
412
|
+
const keys = Object.keys(source);
|
|
413
|
+
if (keys.length > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS
|
|
414
|
+
|| keys.some((key) => key.length > LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH
|
|
415
|
+
|| /[\u0000-\u001f\u007f]/u.test(key)
|
|
416
|
+
|| !isSafeJsonObjectKey(key))) {
|
|
417
|
+
throw new TypeError('Lookup evidence contains an over-bound object.');
|
|
418
|
+
}
|
|
419
|
+
for (const item of Object.values(source))
|
|
420
|
+
pending.push({ value: item, depth: current.depth + 1 });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
throw new TypeError('Lookup evidence contains a non-JSON value.');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
export function serializeLookupEvidence(value, pretty = false) {
|
|
427
|
+
assertLookupEvidencePortableTree(value);
|
|
428
|
+
let output;
|
|
429
|
+
try {
|
|
430
|
+
output = JSON.stringify(value, null, pretty ? 2 : 0);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
throw new TypeError('Lookup evidence could not be serialized as JSON.');
|
|
434
|
+
}
|
|
435
|
+
if (new TextEncoder().encode(output).byteLength > LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES) {
|
|
436
|
+
throw new TypeError('Lookup evidence exceeds the 5 MiB portable file limit.');
|
|
437
|
+
}
|
|
438
|
+
return output;
|
|
23
439
|
}
|
|
24
440
|
function boundedString(value, maximum) {
|
|
25
441
|
if (typeof value !== 'string' || value.length > maximum || /[\u0000-\u001f\u007f]/.test(value))
|
|
@@ -39,14 +455,6 @@ function boundedTimestamp(value) {
|
|
|
39
455
|
const time = Date.parse(text);
|
|
40
456
|
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
41
457
|
}
|
|
42
|
-
function boundedSemanticVersion(value) {
|
|
43
|
-
try {
|
|
44
|
-
return normalizeBoundedSemanticVersion(value, 'Evidence application');
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
458
|
function boundedEndpoint(value) {
|
|
51
459
|
const text = boundedString(value, 2048);
|
|
52
460
|
if (!text)
|
|
@@ -76,6 +484,74 @@ function boundedHostname(value) {
|
|
|
76
484
|
}
|
|
77
485
|
return text;
|
|
78
486
|
}
|
|
487
|
+
function canonicalIpv4(value) {
|
|
488
|
+
const text = boundedString(value, 64);
|
|
489
|
+
if (!text || !/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(text))
|
|
490
|
+
return null;
|
|
491
|
+
const octets = text.split('.').map(Number);
|
|
492
|
+
if (octets.some((octet) => octet > 255))
|
|
493
|
+
return null;
|
|
494
|
+
return octets.join('.');
|
|
495
|
+
}
|
|
496
|
+
function canonicalIpv6(value) {
|
|
497
|
+
const text = boundedString(value, 64)?.toLowerCase();
|
|
498
|
+
if (!text || !text.includes(':') || text.includes('%'))
|
|
499
|
+
return null;
|
|
500
|
+
try {
|
|
501
|
+
const hostname = new URL(`http://[${text}]/`).hostname;
|
|
502
|
+
return hostname.startsWith('[') && hostname.endsWith(']')
|
|
503
|
+
? hostname.slice(1, -1).toLowerCase()
|
|
504
|
+
: null;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function canonicalAsn(value) {
|
|
511
|
+
const text = boundedString(value, 32);
|
|
512
|
+
const match = text?.match(/^(?:AS)?(\d+)$/iu);
|
|
513
|
+
if (!match)
|
|
514
|
+
return null;
|
|
515
|
+
const number = Number(match[1]);
|
|
516
|
+
return Number.isInteger(number) && number >= 0 && number <= 4_294_967_295
|
|
517
|
+
? `AS${number}`
|
|
518
|
+
: null;
|
|
519
|
+
}
|
|
520
|
+
export function projectLookupEvidenceQuery(value) {
|
|
521
|
+
const source = recordOrNull(value) || {};
|
|
522
|
+
const type = typeof source.type === 'string' && LOOKUP_QUERY_TYPES.has(source.type)
|
|
523
|
+
? source.type
|
|
524
|
+
: 'domain';
|
|
525
|
+
if (type === 'domain') {
|
|
526
|
+
const inputHostname = [source.inputHostname, source.registrableDomain, source.submitted, source.query]
|
|
527
|
+
.map(boundedHostname)
|
|
528
|
+
.find((item) => Boolean(item)) || 'unknown.invalid';
|
|
529
|
+
const suppliedRegistrableDomain = boundedHostname(source.registrableDomain);
|
|
530
|
+
const registrableDomain = suppliedRegistrableDomain
|
|
531
|
+
&& (inputHostname === suppliedRegistrableDomain || inputHostname.endsWith(`.${suppliedRegistrableDomain}`))
|
|
532
|
+
? suppliedRegistrableDomain
|
|
533
|
+
: inputHostname;
|
|
534
|
+
return {
|
|
535
|
+
submitted: inputHostname,
|
|
536
|
+
type,
|
|
537
|
+
inputHostname,
|
|
538
|
+
registrableDomain,
|
|
539
|
+
isSubdomain: inputHostname !== registrableDomain,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const candidate = source.submitted ?? source.query;
|
|
543
|
+
const submitted = type === 'ipv4'
|
|
544
|
+
? canonicalIpv4(candidate)
|
|
545
|
+
: type === 'ipv6'
|
|
546
|
+
? canonicalIpv6(candidate)
|
|
547
|
+
: canonicalAsn(candidate);
|
|
548
|
+
if (!submitted)
|
|
549
|
+
throw new TypeError(`Lookup evidence contains an invalid ${type} query.`);
|
|
550
|
+
return { submitted, type, inputHostname: null, registrableDomain: null, isSubdomain: false };
|
|
551
|
+
}
|
|
552
|
+
function completeTerminalObservation(status, truncated) {
|
|
553
|
+
return truncated !== true && (status === 'success' || status === 'not_found');
|
|
554
|
+
}
|
|
79
555
|
function boundedPublishedUri(value, protocols) {
|
|
80
556
|
const text = boundedString(value, 2048);
|
|
81
557
|
if (!text)
|
|
@@ -204,18 +680,189 @@ function sslblSource(value) {
|
|
|
204
680
|
limitations: boundedStringList(source.limitations, 10, 300),
|
|
205
681
|
};
|
|
206
682
|
}
|
|
207
|
-
function
|
|
683
|
+
function rdapAttempt(value) {
|
|
208
684
|
const attempt = recordOrNull(value) || {};
|
|
209
685
|
const endpoint = boundedEndpoint(attempt.endpoint);
|
|
210
686
|
return {
|
|
211
687
|
endpoint,
|
|
212
688
|
transportSecurity: endpoint ? endpoint.startsWith('https:') ? 'https' : 'http' : null,
|
|
213
689
|
status: boundedHttpStatus(attempt.status),
|
|
214
|
-
outcome:
|
|
690
|
+
outcome: typeof attempt.outcome === 'string' && RDAP_ATTEMPT_OUTCOMES.has(attempt.outcome)
|
|
691
|
+
? attempt.outcome
|
|
692
|
+
: 'unknown',
|
|
215
693
|
detail: boundedString(attempt.detail, 240),
|
|
216
694
|
selected: attempt.selected === true,
|
|
217
695
|
};
|
|
218
696
|
}
|
|
697
|
+
function boundedRdapAttempts(value, maximum) {
|
|
698
|
+
return (Array.isArray(value) ? value : [])
|
|
699
|
+
.slice(0, maximum)
|
|
700
|
+
.map(rdapAttempt);
|
|
701
|
+
}
|
|
702
|
+
function whoisChainHop(value) {
|
|
703
|
+
const hop = recordOrNull(value) || {};
|
|
704
|
+
const queryProfile = typeof hop.queryProfile === 'string' && WHOIS_QUERY_PROFILES.has(hop.queryProfile)
|
|
705
|
+
? hop.queryProfile
|
|
706
|
+
: null;
|
|
707
|
+
const error = boundedString(hop.error, 240);
|
|
708
|
+
return {
|
|
709
|
+
server: boundedHostname(hop.server),
|
|
710
|
+
address: boundedString(hop.address, 64),
|
|
711
|
+
queriedAt: boundedTimestamp(hop.queriedAt),
|
|
712
|
+
queryProfile,
|
|
713
|
+
responseEncoding: hop.responseEncoding === 'utf-8' ? 'utf-8' : null,
|
|
714
|
+
status: queryProfile === 'not-issued'
|
|
715
|
+
? 'not_issued'
|
|
716
|
+
: error
|
|
717
|
+
? 'error'
|
|
718
|
+
: typeof hop.response === 'string'
|
|
719
|
+
? 'success'
|
|
720
|
+
: 'unknown',
|
|
721
|
+
detail: error,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function boundedWhoisChain(value) {
|
|
725
|
+
return (Array.isArray(value) ? value : [])
|
|
726
|
+
.slice(0, 16)
|
|
727
|
+
.map(whoisChainHop);
|
|
728
|
+
}
|
|
729
|
+
function lookupTimingDiagnostics(value) {
|
|
730
|
+
const source = recordOrNull(value);
|
|
731
|
+
if (source?.version !== 1)
|
|
732
|
+
return null;
|
|
733
|
+
return {
|
|
734
|
+
version: 1,
|
|
735
|
+
totalMs: boundedInteger(source.totalMs, 120_000),
|
|
736
|
+
sources: (Array.isArray(source.sources) ? source.sources : [])
|
|
737
|
+
.slice(0, LOOKUP_TIMING_SOURCES.size)
|
|
738
|
+
.flatMap((item) => {
|
|
739
|
+
const entry = recordOrNull(item);
|
|
740
|
+
if (!entry
|
|
741
|
+
|| typeof entry.source !== 'string'
|
|
742
|
+
|| !LOOKUP_TIMING_SOURCES.has(entry.source)
|
|
743
|
+
|| (entry.outcome !== 'fulfilled' && entry.outcome !== 'rejected'))
|
|
744
|
+
return [];
|
|
745
|
+
return [{
|
|
746
|
+
source: entry.source,
|
|
747
|
+
outcome: entry.outcome,
|
|
748
|
+
durationMs: boundedInteger(entry.durationMs, 120_000),
|
|
749
|
+
completedAfterMs: boundedInteger(entry.completedAfterMs, 120_000),
|
|
750
|
+
}];
|
|
751
|
+
}),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function registryAccessDiagnostics(value) {
|
|
755
|
+
const source = recordOrNull(value);
|
|
756
|
+
if (!source)
|
|
757
|
+
return null;
|
|
758
|
+
const officialLookupUrl = portableUri(source.officialLookupUrl);
|
|
759
|
+
return {
|
|
760
|
+
suffix: boundedHostname(source.suffix),
|
|
761
|
+
coverageState: boundedString(source.coverageState, 40),
|
|
762
|
+
whoisAccessProfile: boundedString(source.whoisAccessProfile, 80),
|
|
763
|
+
rdapAccessProfile: boundedString(source.rdapAccessProfile, 80),
|
|
764
|
+
...(officialLookupUrl ? { officialLookupUrl } : {}),
|
|
765
|
+
limitation: boundedString(source.limitation, 500),
|
|
766
|
+
authority: source.authority === 'context_only' ? 'context_only' : null,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function registrarRdapDiagnostics(value) {
|
|
770
|
+
const source = recordOrNull(value);
|
|
771
|
+
if (!source)
|
|
772
|
+
return null;
|
|
773
|
+
const endpoint = boundedEndpoint(source.endpoint);
|
|
774
|
+
return {
|
|
775
|
+
status: sourceStatus(source.status, REGISTRAR_RDAP_STATUSES, 'error'),
|
|
776
|
+
endpoint,
|
|
777
|
+
transportSecurity: endpoint ? endpoint.startsWith('https:') ? 'https' : 'http' : null,
|
|
778
|
+
httpStatus: boundedHttpStatus(source.httpStatus),
|
|
779
|
+
fetchedAt: boundedTimestamp(source.fetchedAt),
|
|
780
|
+
attempt: source.attempt === null || source.attempt === undefined ? null : rdapAttempt(source.attempt),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function lookupDiagnostics(value) {
|
|
784
|
+
const source = recordOrNull(value) || {};
|
|
785
|
+
const rdap = recordOrNull(source.rdap) || {};
|
|
786
|
+
const whois = recordOrNull(source.whois) || {};
|
|
787
|
+
const availability = recordOrNull(source.availability) || {};
|
|
788
|
+
const reverseDns = recordOrNull(source.reverseDns);
|
|
789
|
+
const network = recordOrNull(source.network);
|
|
790
|
+
const securityTxt = recordOrNull(source.securityTxt);
|
|
791
|
+
const sslbl = recordOrNull(source.sslbl);
|
|
792
|
+
const rdapEndpoint = boundedEndpoint(rdap.endpoint);
|
|
793
|
+
const networkEndpoint = boundedEndpoint(network?.endpoint);
|
|
794
|
+
const networkPublicationAvailable = network?.status === 'success' || network?.status === 'partial';
|
|
795
|
+
const securityTxtEndpoint = boundedPublishedUri(securityTxt?.endpoint, ['https:']);
|
|
796
|
+
const version = boundedInteger(source.version, 10);
|
|
797
|
+
const timing = lookupTimingDiagnostics(source.timing);
|
|
798
|
+
const registryAccess = registryAccessDiagnostics(source.registryAccess);
|
|
799
|
+
const registrar = registrarRdapDiagnostics(rdap.registrar);
|
|
800
|
+
return {
|
|
801
|
+
...(version === null ? {} : { version }),
|
|
802
|
+
...(timing ? { timing } : {}),
|
|
803
|
+
...(registryAccess ? { registryAccess } : {}),
|
|
804
|
+
rdap: {
|
|
805
|
+
status: sourceStatus(rdap.status, RDAP_SOURCE_STATUSES, 'error'),
|
|
806
|
+
errorCode: boundedString(rdap.errorCode, 80),
|
|
807
|
+
endpoint: rdapEndpoint,
|
|
808
|
+
transportSecurity: rdapEndpoint ? rdapEndpoint.startsWith('https:') ? 'https' : 'http' : null,
|
|
809
|
+
httpStatus: boundedHttpStatus(rdap.httpStatus),
|
|
810
|
+
fetchedAt: boundedTimestamp(rdap.fetchedAt),
|
|
811
|
+
attempts: boundedRdapAttempts(rdap.attempts, 16),
|
|
812
|
+
...(registrar ? { registrar } : {}),
|
|
813
|
+
},
|
|
814
|
+
whois: {
|
|
815
|
+
status: sourceStatus(whois.status, WHOIS_SOURCE_STATUSES, 'error'),
|
|
816
|
+
errorCode: boundedString(whois.errorCode, 80),
|
|
817
|
+
queriedAt: boundedTimestamp(whois.queriedAt),
|
|
818
|
+
authoritativeHop: boundedHostname(whois.authoritativeHop),
|
|
819
|
+
failedHop: boundedHostname(whois.failedHop),
|
|
820
|
+
conflictingHop: boundedHostname(whois.conflictingHop),
|
|
821
|
+
},
|
|
822
|
+
...(recordOrNull(source.availability) ? { availability: {
|
|
823
|
+
status: sourceStatus(availability.status, AVAILABILITY_DIAGNOSTIC_STATUSES, 'error'),
|
|
824
|
+
errorCode: boundedString(availability.errorCode, 80),
|
|
825
|
+
resultState: boundedString(availability.resultState, 40),
|
|
826
|
+
} } : {}),
|
|
827
|
+
...(reverseDns ? { reverseDns: {
|
|
828
|
+
status: sourceStatus(reverseDns.status, REVERSE_DNS_STATUSES, 'error'),
|
|
829
|
+
observedAt: boundedTimestamp(reverseDns.observedAt),
|
|
830
|
+
complete: completeTerminalObservation(sourceStatus(reverseDns.status, REVERSE_DNS_STATUSES, 'error'), reverseDns.truncated),
|
|
831
|
+
truncated: reverseDns.truncated === true,
|
|
832
|
+
} } : {}),
|
|
833
|
+
...(network ? { network: {
|
|
834
|
+
status: sourceStatus(network.status, NETWORK_CONTEXT_STATUSES, 'error'),
|
|
835
|
+
address: networkPublicationAvailable ? boundedString(network.address, 64) : null,
|
|
836
|
+
family: networkPublicationAvailable && (network.family === 4 || network.family === 6) ? network.family : null,
|
|
837
|
+
addressSource: networkPublicationAvailable && typeof network.addressSource === 'string' && NETWORK_ADDRESS_SOURCES.has(network.addressSource)
|
|
838
|
+
? network.addressSource
|
|
839
|
+
: null,
|
|
840
|
+
endpoint: networkPublicationAvailable ? networkEndpoint : null,
|
|
841
|
+
transportSecurity: networkPublicationAvailable && networkEndpoint ? networkEndpoint.startsWith('https:') ? 'https' : 'http' : null,
|
|
842
|
+
httpStatus: networkPublicationAvailable ? boundedHttpStatus(network.httpStatus) : null,
|
|
843
|
+
fetchedAt: networkPublicationAvailable ? boundedTimestamp(network.fetchedAt) : null,
|
|
844
|
+
attempts: networkPublicationAvailable ? boundedRdapAttempts(network.attempts, 3) : [],
|
|
845
|
+
} } : {}),
|
|
846
|
+
...(securityTxt ? { securityTxt: {
|
|
847
|
+
status: boundedString(securityTxt.status, 40),
|
|
848
|
+
state: typeof securityTxt.state === 'string' && SECURITY_TXT_STATES.has(securityTxt.state)
|
|
849
|
+
? securityTxt.state
|
|
850
|
+
: null,
|
|
851
|
+
endpoint: securityTxtEndpoint,
|
|
852
|
+
httpStatus: boundedHttpStatus(securityTxt.httpStatus),
|
|
853
|
+
observedAt: boundedTimestamp(securityTxt.observedAt),
|
|
854
|
+
complete: securityTxt.complete === true,
|
|
855
|
+
truncated: securityTxt.truncated === true,
|
|
856
|
+
} } : {}),
|
|
857
|
+
...(sslbl ? { sslbl: {
|
|
858
|
+
status: typeof sslbl.status === 'string' && SSLBL_STATES.has(sslbl.status) ? sslbl.status : null,
|
|
859
|
+
verdict: typeof sslbl.verdict === 'string' && SSLBL_VERDICTS.has(sslbl.verdict) ? sslbl.verdict : null,
|
|
860
|
+
observedAt: boundedTimestamp(sslbl.observedAt),
|
|
861
|
+
complete: sslbl.complete === true,
|
|
862
|
+
snapshotUpdatedAt: boundedTimestamp(sslbl.snapshotUpdatedAt),
|
|
863
|
+
} } : {}),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
219
866
|
function networkSource(value) {
|
|
220
867
|
const source = recordOrNull(value);
|
|
221
868
|
if (!source
|
|
@@ -228,6 +875,7 @@ function networkSource(value) {
|
|
|
228
875
|
const network = recordOrNull(source.network);
|
|
229
876
|
const diagnostics = recordOrNull(source.diagnostics);
|
|
230
877
|
const rdapEndpoint = boundedEndpoint(rdap?.endpoint);
|
|
878
|
+
const publicationAvailable = source.status === 'success' || source.status === 'partial';
|
|
231
879
|
return {
|
|
232
880
|
contextVersion: 1,
|
|
233
881
|
version: source.version === 1 ? 1 : null,
|
|
@@ -236,7 +884,7 @@ function networkSource(value) {
|
|
|
236
884
|
scanMode: source.scanMode === 'deep' ? 'deep' : null,
|
|
237
885
|
source: source.source === 'ip_rdap' ? 'ip_rdap' : null,
|
|
238
886
|
durationMs: boundedInteger(source.durationMs, 120_000),
|
|
239
|
-
complete: source.
|
|
887
|
+
complete: completeTerminalObservation(source.status, source.truncated),
|
|
240
888
|
truncated: source.truncated === true,
|
|
241
889
|
limitations: boundedStringList(source.limitations, 10, 300),
|
|
242
890
|
diagnostics: diagnostics ? {
|
|
@@ -249,7 +897,7 @@ function networkSource(value) {
|
|
|
249
897
|
cidrCount: boundedInteger(diagnostics.cidrCount, 16),
|
|
250
898
|
} : null,
|
|
251
899
|
detail: boundedString(source.detail, 300),
|
|
252
|
-
endpoint: endpoint ? {
|
|
900
|
+
endpoint: publicationAvailable && endpoint ? {
|
|
253
901
|
address: boundedString(endpoint.address, 64),
|
|
254
902
|
family: endpoint.family === 4 || endpoint.family === 6 ? endpoint.family : null,
|
|
255
903
|
selectedFrom: typeof endpoint.selectedFrom === 'string'
|
|
@@ -257,14 +905,14 @@ function networkSource(value) {
|
|
|
257
905
|
? endpoint.selectedFrom
|
|
258
906
|
: null,
|
|
259
907
|
} : null,
|
|
260
|
-
rdap: rdap ? {
|
|
908
|
+
rdap: publicationAvailable && rdap ? {
|
|
261
909
|
endpoint: rdapEndpoint,
|
|
262
910
|
transportSecurity: rdapEndpoint ? rdapEndpoint.startsWith('https:') ? 'https' : 'http' : null,
|
|
263
911
|
httpStatus: boundedHttpStatus(rdap.httpStatus),
|
|
264
912
|
fetchedAt: boundedTimestamp(rdap.fetchedAt),
|
|
265
|
-
attempts: (
|
|
913
|
+
attempts: boundedRdapAttempts(rdap.attempts, 3),
|
|
266
914
|
} : null,
|
|
267
|
-
network: network ? {
|
|
915
|
+
network: publicationAvailable && network ? {
|
|
268
916
|
handle: boundedString(network.handle, 300),
|
|
269
917
|
name: boundedString(network.name, 300),
|
|
270
918
|
holder: boundedString(network.holder, 300),
|
|
@@ -289,7 +937,8 @@ function reverseDnsSource(value) {
|
|
|
289
937
|
const records = recordOrNull(source.records);
|
|
290
938
|
const diagnostics = recordOrNull(source.diagnostics);
|
|
291
939
|
const ptrDiagnostics = recordOrNull(diagnostics?.ptr);
|
|
292
|
-
const
|
|
940
|
+
const publicationAvailable = source.status === 'success' || source.status === 'partial';
|
|
941
|
+
const ptr = [...new Set((publicationAvailable && Array.isArray(records?.ptr) ? records.ptr : [])
|
|
293
942
|
.slice(0, 8)
|
|
294
943
|
.map(boundedHostname)
|
|
295
944
|
.filter((item) => item !== null))].sort();
|
|
@@ -300,7 +949,7 @@ function reverseDnsSource(value) {
|
|
|
300
949
|
scanMode: source.scanMode === 'deep' ? 'deep' : null,
|
|
301
950
|
source: 'reverse_dns',
|
|
302
951
|
durationMs: boundedInteger(source.durationMs, 120_000),
|
|
303
|
-
complete: source.
|
|
952
|
+
complete: completeTerminalObservation(source.status, source.truncated),
|
|
304
953
|
truncated: source.truncated === true,
|
|
305
954
|
limitations: boundedStringList(source.limitations, 10, 300),
|
|
306
955
|
diagnostics: ptrDiagnostics ? {
|
|
@@ -314,40 +963,62 @@ function reverseDnsSource(value) {
|
|
|
314
963
|
records: { ptr },
|
|
315
964
|
};
|
|
316
965
|
}
|
|
317
|
-
function
|
|
966
|
+
function boundedSourceError(value, fallback) {
|
|
967
|
+
const normalized = String(value ?? '')
|
|
968
|
+
.replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
|
969
|
+
.replace(/\s+/gu, ' ')
|
|
970
|
+
.trim();
|
|
971
|
+
return normalized.slice(0, 10_000) || fallback;
|
|
972
|
+
}
|
|
973
|
+
function sourceStatus(value, allowed, fallback) {
|
|
974
|
+
return typeof value === 'string' && allowed.has(value) ? value : fallback;
|
|
975
|
+
}
|
|
976
|
+
function rdapSource(rdap, diagnostics) {
|
|
318
977
|
const source = recordOrNull(rdap) || {};
|
|
319
|
-
|
|
978
|
+
const status = sourceStatus(diagnostics?.status, RDAP_SOURCE_STATUSES, 'error');
|
|
979
|
+
const attempts = boundedRdapAttempts(source.attempts ?? diagnostics?.attempts, 16);
|
|
980
|
+
if (status === 'error')
|
|
320
981
|
return {
|
|
321
982
|
status: 'error',
|
|
322
|
-
error:
|
|
323
|
-
attempts
|
|
983
|
+
error: boundedSourceError(source.error ?? diagnostics?.errorCode, 'RDAP source reported an error.'),
|
|
984
|
+
attempts,
|
|
324
985
|
};
|
|
986
|
+
const hasPublication = status === 'success' || status === 'partial';
|
|
325
987
|
return {
|
|
326
|
-
status
|
|
327
|
-
endpoint: source.rdapServer
|
|
328
|
-
transportSecurity: source.transportSecurity ||
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
988
|
+
status,
|
|
989
|
+
endpoint: boundedEndpoint(source.rdapServer ?? diagnostics?.endpoint),
|
|
990
|
+
transportSecurity: source.transportSecurity === 'http' || source.transportSecurity === 'https'
|
|
991
|
+
? source.transportSecurity
|
|
992
|
+
: diagnostics?.transportSecurity === 'http' || diagnostics?.transportSecurity === 'https'
|
|
993
|
+
? diagnostics.transportSecurity
|
|
994
|
+
: null,
|
|
995
|
+
httpStatus: boundedHttpStatus(source.upstreamStatus ?? diagnostics?.httpStatus),
|
|
996
|
+
fetchedAt: boundedTimestamp(source.fetchedAt ?? diagnostics?.fetchedAt),
|
|
997
|
+
attempts,
|
|
998
|
+
parsed: hasPublication ? projectLookupEvidencePrivacySafeTree(source.parsed) : null,
|
|
999
|
+
raw: hasPublication ? projectLookupEvidencePrivacySafeTree(source.data) : null,
|
|
334
1000
|
};
|
|
335
1001
|
}
|
|
336
|
-
function whoisSource(whois) {
|
|
1002
|
+
function whoisSource(whois, diagnostics) {
|
|
337
1003
|
const source = recordOrNull(whois) || {};
|
|
338
|
-
|
|
339
|
-
|
|
1004
|
+
const status = sourceStatus(diagnostics?.status, WHOIS_SOURCE_STATUSES, 'error');
|
|
1005
|
+
if (status === 'error')
|
|
1006
|
+
return {
|
|
1007
|
+
status: 'error',
|
|
1008
|
+
error: boundedSourceError(source.error ?? diagnostics?.errorCode, 'WHOIS source reported an error.'),
|
|
1009
|
+
};
|
|
340
1010
|
const parsed = recordOrNull(source.parsed);
|
|
341
|
-
const
|
|
1011
|
+
const hasPublication = status === 'complete' || status === 'partial';
|
|
1012
|
+
const chain = hasPublication ? boundedWhoisChain(source.chain) : [];
|
|
342
1013
|
const firstHop = recordOrNull(chain[0]);
|
|
343
1014
|
return {
|
|
344
|
-
status
|
|
345
|
-
queriedAt: firstHop?.queriedAt
|
|
346
|
-
authoritativeHop: parsed?.authoritativeHop
|
|
347
|
-
failedHop: parsed?.failedHop
|
|
348
|
-
conflictingHop: parsed?.conflictingHop
|
|
349
|
-
parsed:
|
|
350
|
-
chain
|
|
1015
|
+
status,
|
|
1016
|
+
queriedAt: boundedTimestamp(firstHop?.queriedAt ?? diagnostics?.queriedAt),
|
|
1017
|
+
authoritativeHop: boundedHostname(parsed?.authoritativeHop ?? diagnostics?.authoritativeHop),
|
|
1018
|
+
failedHop: boundedHostname(parsed?.failedHop ?? diagnostics?.failedHop),
|
|
1019
|
+
conflictingHop: boundedHostname(parsed?.conflictingHop ?? diagnostics?.conflictingHop),
|
|
1020
|
+
parsed: hasPublication ? projectLookupEvidencePrivacySafeTree(parsed) : null,
|
|
1021
|
+
chain,
|
|
351
1022
|
};
|
|
352
1023
|
}
|
|
353
1024
|
function registrarPublicationComparison(body, registryParsed) {
|
|
@@ -359,7 +1030,9 @@ function registrarPublicationComparison(body, registryParsed) {
|
|
|
359
1030
|
if (!registrar && !registrarDiagnostics)
|
|
360
1031
|
return null;
|
|
361
1032
|
const reportedStatus = registrar?.status ?? registrarDiagnostics?.status;
|
|
362
|
-
const parsed =
|
|
1033
|
+
const parsed = ['success', 'partial'].includes(String(reportedStatus))
|
|
1034
|
+
? recordOrNull(projectLookupEvidencePrivacySafeTree(registrar?.parsed))
|
|
1035
|
+
: null;
|
|
363
1036
|
const registrarStatus = typeof reportedStatus === 'string' && REGISTRAR_RDAP_STATUSES.has(reportedStatus)
|
|
364
1037
|
? (reportedStatus === 'success' && !parsed ? 'partial' : reportedStatus)
|
|
365
1038
|
: 'error';
|
|
@@ -369,17 +1042,25 @@ function registrarPublicationComparison(body, registryParsed) {
|
|
|
369
1042
|
});
|
|
370
1043
|
}
|
|
371
1044
|
export function buildLookupEvidence(response, options = {}) {
|
|
1045
|
+
assertBoundedJsonStructure(response, 'Lookup response');
|
|
372
1046
|
const { generatedAt = new Date().toISOString(), idnAnalysis = null, applicationVersion = null } = options;
|
|
373
1047
|
const body = recordOrNull(response) || {};
|
|
374
1048
|
const rdap = recordOrNull(body.rdap);
|
|
375
1049
|
const whois = recordOrNull(body.whois);
|
|
376
1050
|
const diagnostics = recordOrNull(body.diagnostics);
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
const
|
|
1051
|
+
const projectedDiagnostics = lookupDiagnostics(diagnostics);
|
|
1052
|
+
const rdapDiagnostics = recordOrNull(projectedDiagnostics.rdap);
|
|
1053
|
+
const whoisDiagnostics = recordOrNull(projectedDiagnostics.whois);
|
|
1054
|
+
const projectedRdap = rdapSource(rdap, rdapDiagnostics);
|
|
1055
|
+
const projectedWhois = whoisSource(whois, whoisDiagnostics);
|
|
1056
|
+
const projectedRdapRecord = recordOrNull(projectedRdap) || {};
|
|
1057
|
+
const projectedWhoisRecord = recordOrNull(projectedWhois) || {};
|
|
1058
|
+
const rdapParsed = ['success', 'partial'].includes(String(projectedRdapRecord.status))
|
|
1059
|
+
? recordOrNull(projectedRdapRecord.parsed)
|
|
1060
|
+
: null;
|
|
1061
|
+
const whoisParsed = ['complete', 'partial'].includes(String(projectedWhoisRecord.status))
|
|
1062
|
+
? recordOrNull(projectedWhoisRecord.parsed)
|
|
1063
|
+
: null;
|
|
383
1064
|
// Rebuild the pure interpretation from bounded source fields instead of
|
|
384
1065
|
// trusting a supplied derived object. This adds no collection or network
|
|
385
1066
|
// work and lets saved or fast responses carry current source-health-aware
|
|
@@ -391,46 +1072,38 @@ export function buildLookupEvidence(response, options = {}) {
|
|
|
391
1072
|
whoisParsed,
|
|
392
1073
|
whoisStatus: whoisDiagnostics?.status,
|
|
393
1074
|
whoisQueriedAt: recordOrNull(Array.isArray(whois?.chain) ? whois.chain[0] : null)?.queriedAt,
|
|
394
|
-
registrarRdapParsed,
|
|
395
|
-
registrarRdapStatus: registrarRdap?.status ?? recordOrNull(rdapDiagnostics?.registrar)?.status,
|
|
396
|
-
registrarRdapFetchedAt: registrarRdap?.fetchedAt,
|
|
397
1075
|
});
|
|
398
|
-
|
|
1076
|
+
const query = projectLookupEvidenceQuery(body);
|
|
1077
|
+
const availabilityAnalysis = projectLookupEvidenceAvailability(body.availability);
|
|
1078
|
+
const idn = projectedKnownRecord(idnAnalysis, LOOKUP_IDN_ANALYSIS_KEYS);
|
|
1079
|
+
const evidence = {
|
|
399
1080
|
schema: LOOKUP_EVIDENCE_SCHEMA,
|
|
400
1081
|
schemaVersion: LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
401
1082
|
generatedAt,
|
|
402
|
-
application:
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
|
|
406
|
-
},
|
|
407
|
-
query: {
|
|
408
|
-
submitted: body.query || null,
|
|
409
|
-
type: body.type || null,
|
|
410
|
-
inputHostname: body.inputHostname || null,
|
|
411
|
-
registrableDomain: body.registrableDomain || null,
|
|
412
|
-
isSubdomain: body.isSubdomain === true,
|
|
413
|
-
},
|
|
414
|
-
diagnostics: cloneJson(body.diagnostics),
|
|
1083
|
+
application: buildPortableGeneratorMetadata(applicationVersion),
|
|
1084
|
+
query,
|
|
1085
|
+
diagnostics: projectedDiagnostics,
|
|
415
1086
|
sources: {
|
|
416
|
-
rdap:
|
|
417
|
-
whois:
|
|
1087
|
+
rdap: projectedRdap,
|
|
1088
|
+
whois: projectedWhois,
|
|
418
1089
|
reverseDns: reverseDnsSource(body.reverseDns),
|
|
419
1090
|
network: networkSource(body.networkContext),
|
|
420
1091
|
securityTxt: securityTxtSource(body.securityTxt),
|
|
421
1092
|
sslbl: sslblSource(body.sslbl),
|
|
422
1093
|
},
|
|
423
1094
|
analysis: {
|
|
424
|
-
availability:
|
|
425
|
-
idn
|
|
426
|
-
registryInsights,
|
|
427
|
-
registryComparison: compareRegistrySources(rdapParsed, whoisParsed, {
|
|
1095
|
+
availability: availabilityAnalysis,
|
|
1096
|
+
idn,
|
|
1097
|
+
registryInsights: projectLookupEvidencePrivacySafeTree(registryInsights),
|
|
1098
|
+
registryComparison: projectLookupEvidencePrivacySafeTree(compareRegistrySources(rdapParsed, whoisParsed, {
|
|
428
1099
|
rdapStatus: rdapDiagnostics?.status,
|
|
429
1100
|
whoisStatus: whoisDiagnostics?.status,
|
|
430
|
-
}),
|
|
431
|
-
registrarPublicationComparison: registrarPublicationComparison(body, rdapParsed),
|
|
1101
|
+
})),
|
|
1102
|
+
registrarPublicationComparison: projectLookupEvidencePrivacySafeTree(registrarPublicationComparison(body, rdapParsed)),
|
|
432
1103
|
},
|
|
433
1104
|
};
|
|
1105
|
+
assertLookupEvidencePortableTree(evidence);
|
|
1106
|
+
return evidence;
|
|
434
1107
|
}
|
|
435
1108
|
export function evidenceFilename(response, now = Date.now()) {
|
|
436
1109
|
const record = recordOrNull(response);
|