@slicedearth/whoisleuth-cli 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
// Portable tree bounds, private-field exclusion and availability projection.
|
|
2
|
+
// Source-specific publication assembly remains in evidence-export.mts.
|
|
3
|
+
import { isSafeJsonObjectKey } from "./bounded-json.mjs";
|
|
4
|
+
import { recordOrNull } from "./json-record.mjs";
|
|
5
|
+
import { isUriShapedLabel } from "./portable-generator.mjs";
|
|
6
|
+
import { validHttpDeliveryMetadata, validPagePublicationMetadata } from "./homepage-metadata-contract.mjs";
|
|
7
|
+
import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES, LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS, LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH, LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH, } from "../packages/contracts/lookup-evidence.mjs";
|
|
8
|
+
const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
9
|
+
'applicable', 'type', 'domain', 'state', 'confidence', 'detail', 'source',
|
|
10
|
+
'rdapServer', 'nameservers', 'statuses', 'registrar', 'registrant', 'abuse',
|
|
11
|
+
'createdDate', 'expiryDate', 'createdDateIso', 'expiryDateIso', 'domainAgeDays',
|
|
12
|
+
'expiresInDays', 'privacyProtected', 'dnssec', 'activityStatus',
|
|
13
|
+
'websiteProbeStatus', 'websiteProbeDetail', 'http', 'deepScanComplete',
|
|
14
|
+
'faviconHash', 'faviconPHash', 'pageTitle', 'hasPasswordField',
|
|
15
|
+
'phishingLanguageMatch', 'hasExternalFormAction', 'externalAssetHosts',
|
|
16
|
+
'pageIdentity', 'credentialSurfaceProfile', 'structuredDataIdentity',
|
|
17
|
+
'technologyProfile', 'pageRoleProfile', 'clientBehaviorProfile',
|
|
18
|
+
'securityPosture', 'dns', 'tls', 'hasMx', 'hasNullMx', 'mxHosts', 'hasSpf',
|
|
19
|
+
'hasDmarc', 'bulkComparison', 'limitations',
|
|
20
|
+
]);
|
|
21
|
+
const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
22
|
+
...[...PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS]
|
|
23
|
+
.filter((key) => !['registrar', 'registrant', 'abuse'].includes(key)),
|
|
24
|
+
'registryContactsExcluded', 'observationHostname', 'webObservationMode',
|
|
25
|
+
]);
|
|
26
|
+
const PRIVATE_EVIDENCE_KEYS = new Set([
|
|
27
|
+
'authorization', 'proxyauthorization', 'cookie', 'cookies', 'setcookie',
|
|
28
|
+
'session', 'sessionid', 'sessiontoken', 'token', 'accesstoken', 'refreshtoken',
|
|
29
|
+
'apikey', 'password', 'secret', 'credentials', 'requestheaders', 'responseheaders',
|
|
30
|
+
'headers', 'rawbody', 'responsebody',
|
|
31
|
+
]);
|
|
32
|
+
const SAFE_EVIDENCE_KEYS = new Set([
|
|
33
|
+
'credentialsurfaceprofile', 'credentialsurfaceversion', 'haspasswordfield', 'tokencount',
|
|
34
|
+
]);
|
|
35
|
+
const PORTABLE_URL_KEYS = new Set([
|
|
36
|
+
'endpoint', 'url', 'uri', 'href', 'rdapserver', 'finalurl', 'requestedurl',
|
|
37
|
+
'requesturl', 'officiallookupurl', 'declaredorigin', 'actionurl', 'from', 'to',
|
|
38
|
+
]);
|
|
39
|
+
const PORTABLE_ORIGIN_COLLECTION_KEYS = new Set([
|
|
40
|
+
'embeddedorigins', 'externalactionorigins', 'externalorigins',
|
|
41
|
+
]);
|
|
42
|
+
// Availability is assembled from a finite set of versioned, bounded analysis
|
|
43
|
+
// models. Portable exports retain only their declared output vocabulary rather
|
|
44
|
+
// than recursively copying arbitrary browser-local extensions. This set is
|
|
45
|
+
// intentionally exact and case-sensitive; new model fields must be reviewed at
|
|
46
|
+
// this privacy boundary before they can enter a portable artefact.
|
|
47
|
+
const LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS = new Set([
|
|
48
|
+
// Shared observation and summary fields.
|
|
49
|
+
'version', 'profileVersion', 'status', 'state', 'source', 'observedAt',
|
|
50
|
+
'scanMode', 'durationMs', 'complete', 'truncated', 'limitations',
|
|
51
|
+
'diagnostics', 'detail', 'error', 'count', 'total', 'discarded', 'summary',
|
|
52
|
+
'findings', 'id', 'label', 'tone', 'category', 'confidence', 'compatibility', 'role', 'roles', 'evidence',
|
|
53
|
+
'description',
|
|
54
|
+
'value', 'values', 'name', 'type', 'types', 'mode', 'owner', 'target',
|
|
55
|
+
'url', 'uri', 'href',
|
|
56
|
+
'priority', 'ttl', 'records',
|
|
57
|
+
// HTTP observation and retained response metadata.
|
|
58
|
+
'requestUrl', 'finalUrl', 'transportSecurity', 'redirectCount',
|
|
59
|
+
'redirectLimitReached', 'redirects', 'crossOriginRedirect', 'httpsDowngrade',
|
|
60
|
+
'attempts', 'response', 'contentType', 'contentLanguage', 'server',
|
|
61
|
+
'declaredContentLength', 'capturedBodyBytes', 'bodyInspected', 'bodyTruncated',
|
|
62
|
+
'bodyHash', 'securityHeaders', 'strictTransportSecurity',
|
|
63
|
+
'contentSecurityPolicy', 'xFrameOptions', 'xContentTypeOptions',
|
|
64
|
+
'referrerPolicy', 'algorithm', 'scope', 'bytes', 'from', 'to',
|
|
65
|
+
'queryOmitted', 'pathTruncated', 'outcome', 'httpStatus', 'attemptCount',
|
|
66
|
+
// DNS observation, effective CAA, and HTTPS/SVCB parameter summaries.
|
|
67
|
+
'a', 'aaaa', 'caa', 'cname', 'dmarc', 'https', 'mx', 'ns', 'ptr', 'soa',
|
|
68
|
+
'spf', 'delegation', 'caaPolicy', 'policyVersion', 'queryLimit',
|
|
69
|
+
'queriedOwners', 'effectiveOwner', 'inherited', 'tree', 'registryEvidence',
|
|
70
|
+
'exchange', 'nsname', 'hostmaster', 'serial', 'refresh', 'retry', 'expire', 'minttl',
|
|
71
|
+
'tag', 'critical', 'parameters', 'parametersIgnored', 'mandatory', 'alpn',
|
|
72
|
+
'noDefaultAlpn', 'port', 'ipv4hint', 'ipv6hint', 'opaque', 'key', 'length',
|
|
73
|
+
'unknownKeys', 'unsupportedMandatoryKeys', 'targetIsOwner',
|
|
74
|
+
'serviceUnavailable', 'compatible', 'effectiveOwner', 'queriedOwners',
|
|
75
|
+
// TLS profile, certificate, extension, and connection summaries. Runtime
|
|
76
|
+
// authorisation objects are deliberately absent from this portable set.
|
|
77
|
+
'connectedAddress', 'connectedFamily', 'sniHost', 'protocol', 'alpnProtocol',
|
|
78
|
+
'cipher', 'ephemeralKey', 'hostname', 'matches', 'validity', 'certificate',
|
|
79
|
+
'chain', 'chainTruncated', 'standardName', 'size', 'subject', 'issuer',
|
|
80
|
+
'serialNumber', 'validFrom', 'validTo', 'fingerprintSha1',
|
|
81
|
+
'fingerprintSha256', 'isCertificateAuthority', 'subjectAltNames',
|
|
82
|
+
'dnsNames', 'ipAddresses', 'otherNames', 'classes', 'dns', 'ip', 'directoryName',
|
|
83
|
+
'registeredId', 'otherName', 'publicKey', 'bits', 'curve',
|
|
84
|
+
'signature', 'oid', 'extendedKeyUsage', 'authorityInformationAccess',
|
|
85
|
+
'ocsp', 'caIssuers', 'unknownMethods', 'extensionProfile', 'parsed', 'partial',
|
|
86
|
+
'certificatePolicies', 'crlDistributionPoints', 'commonNames',
|
|
87
|
+
'organizations', 'organizationalUnits', 'countries', 'localities', 'states',
|
|
88
|
+
'connectionAttempts', 'resolvedAddressCount', 'discardedFields',
|
|
89
|
+
// Page identity, relationships, and bounded fingerprints.
|
|
90
|
+
'identityVersion', 'documentLanguage', 'canonical', 'metaRefresh',
|
|
91
|
+
'openGraph', 'title', 'siteName', 'generator', 'forms', 'postCount',
|
|
92
|
+
'insecureActionCount', 'externalActionOrigins', 'resources', 'byType',
|
|
93
|
+
'image', 'script', 'stylesheet', 'link', 'frame', 'media', 'object',
|
|
94
|
+
'externalOrigins', 'embeddedOrigins', 'contactDomains', 'downloads',
|
|
95
|
+
'explicitCount', 'riskyCount', 'riskyFileTypes', 'trackingIdentifiers',
|
|
96
|
+
'fingerprints', 'fingerprintVersion', 'exact', 'normalizedHtml',
|
|
97
|
+
'visibleText', 'domStructure', 'similarity', 'formStructure', 'resourceHosts',
|
|
98
|
+
'identifiers', 'featureCount', 'nodeCount', 'formCount', 'controlCount',
|
|
99
|
+
'parser', 'tokenCount', 'tagsExamined', 'discardedUrls', 'formsObserved',
|
|
100
|
+
'relationshipTagsExamined', 'relationshipUrlsDiscarded',
|
|
101
|
+
'trackingIdentifiersTruncated',
|
|
102
|
+
// Credential, structured identity, role, behaviour, technology, and posture
|
|
103
|
+
// projections. Only fixed categories/counts and bounded descriptions enter.
|
|
104
|
+
'credentialSurfaceVersion', 'methods', 'actions', 'missing', 'get', 'post',
|
|
105
|
+
'dialog', 'other', 'sameOrigin', 'external', 'cleartext', 'unclassified',
|
|
106
|
+
'inputs', 'classifiedCount', 'categories', 'password', 'email', 'username',
|
|
107
|
+
'one_time_code', 'payment', 'inputsObserved', 'classifiedInputs',
|
|
108
|
+
'unclassifiedActions', 'structuredDataVersion', 'scriptsObserved',
|
|
109
|
+
'scriptsExamined', 'charactersExamined', 'documentsParsed', 'malformedScripts',
|
|
110
|
+
'externalScriptsSkipped', 'objectsExamined', 'arrayItemsExamined',
|
|
111
|
+
'discardedProperties', 'entities', 'declaredOrigin', 'sameAsHosts',
|
|
112
|
+
'pageRoleProfileVersion', 'primaryRole', 'role', 'rolesObserved',
|
|
113
|
+
'clientBehaviorProfileVersion', 'scriptSummary', 'elementsObserved',
|
|
114
|
+
'referencedScripts', 'inlineScripts', 'moduleScripts', 'indicators',
|
|
115
|
+
'evidenceClass', 'occurrences', 'explanation', 'indicatorsObserved',
|
|
116
|
+
'scriptElementsExamined', 'inlineCharactersExamined', 'technologyProfile',
|
|
117
|
+
'browserLibraryProfile', 'catalog', 'knownExploitedCatalog', 'releasedAt',
|
|
118
|
+
'sourceRevision', 'apparentVersion', 'detectionMethods', 'advisoryCount',
|
|
119
|
+
'highestSeverity', 'advisoryIdentifiers', 'knownExploitedCount',
|
|
120
|
+
'knownExploitedIdentifiers', 'weaknessClasses', 'htmlEvaluated',
|
|
121
|
+
'generatorEvaluated', 'serverEvaluated', 'resourceOriginsEvaluated',
|
|
122
|
+
'passiveHeadersEvaluated', 'tagLimitReached', 'referencesExamined',
|
|
123
|
+
'inlineScriptsExamined', 'inlineSignatureCharactersExamined',
|
|
124
|
+
'inlineSignatureTimedOut', 'inlineSignatureUnavailable', 'catalogComponents',
|
|
125
|
+
'advisoryMatches', 'knownExploitedMatches', 'postureVersion',
|
|
126
|
+
'potentialExposure', 'observedAbsence', 'unavailable',
|
|
127
|
+
// Bounded cross-row comparison evidence.
|
|
128
|
+
'bulkComparison', 'technology', 'tls', 'ids', 'issuerLabel', 'spkiSha256',
|
|
129
|
+
]);
|
|
130
|
+
const LOOKUP_AVAILABILITY_CREDENTIAL_CATEGORY_KEYS = new Set([
|
|
131
|
+
'password', 'email', 'username', 'one_time_code', 'payment',
|
|
132
|
+
]);
|
|
133
|
+
const LOOKUP_AVAILABILITY_TLS_SAN_CLASS_KEYS = new Set([
|
|
134
|
+
'dns', 'ip', 'email', 'uri', 'directoryName', 'registeredId', 'otherName', 'unclassified',
|
|
135
|
+
]);
|
|
136
|
+
const LOOKUP_AVAILABILITY_VALUE_PATHS = new Set([
|
|
137
|
+
'dns.records.caa',
|
|
138
|
+
'dns.caaPolicy.records',
|
|
139
|
+
'http.response.bodyHash',
|
|
140
|
+
'pageIdentity.trackingIdentifiers',
|
|
141
|
+
'pageIdentity.fingerprints.exact',
|
|
142
|
+
'pageIdentity.fingerprints.normalizedHtml',
|
|
143
|
+
'pageIdentity.fingerprints.visibleText',
|
|
144
|
+
'pageIdentity.fingerprints.domStructure',
|
|
145
|
+
'pageIdentity.fingerprints.domStructure.similarity',
|
|
146
|
+
'pageIdentity.fingerprints.formStructure',
|
|
147
|
+
'pageIdentity.fingerprints.resourceHosts',
|
|
148
|
+
'pageIdentity.fingerprints.identifiers',
|
|
149
|
+
'pageIdentity.fingerprints.identifiers.values',
|
|
150
|
+
]);
|
|
151
|
+
const LOOKUP_AVAILABILITY_OWNER_PATHS = new Set([
|
|
152
|
+
'dns.records.https',
|
|
153
|
+
'dns.caaPolicy.queriedOwners',
|
|
154
|
+
]);
|
|
155
|
+
function normalizedEvidenceKey(value) {
|
|
156
|
+
return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
157
|
+
}
|
|
158
|
+
function privateEvidenceKey(value, item) {
|
|
159
|
+
const normalized = normalizedEvidenceKey(value);
|
|
160
|
+
if (SAFE_EVIDENCE_KEYS.has(normalized))
|
|
161
|
+
return false;
|
|
162
|
+
if (normalized === 'password' && (typeof item === 'number' || typeof item === 'boolean'))
|
|
163
|
+
return false;
|
|
164
|
+
return PRIVATE_EVIDENCE_KEYS.has(normalized)
|
|
165
|
+
|| normalized.includes('authorization')
|
|
166
|
+
|| normalized.includes('authheader')
|
|
167
|
+
|| normalized.includes('cookie')
|
|
168
|
+
|| normalized.includes('apikey')
|
|
169
|
+
|| normalized.includes('secret')
|
|
170
|
+
|| normalized.includes('sessionkey')
|
|
171
|
+
|| normalized.includes('passwordvalue')
|
|
172
|
+
|| normalized.endsWith('password')
|
|
173
|
+
|| /(?:access|auth|bearer|csrf|refresh|session)token$/u.test(normalized)
|
|
174
|
+
|| /(?:request|response|authorization|proxy)headers?$/u.test(normalized)
|
|
175
|
+
|| (normalized.includes('credential') && !normalized.startsWith('credentialsurface'));
|
|
176
|
+
}
|
|
177
|
+
export function portableUri(value) {
|
|
178
|
+
const text = boundedString(value, 2048);
|
|
179
|
+
if (!text)
|
|
180
|
+
return null;
|
|
181
|
+
try {
|
|
182
|
+
const url = new URL(text);
|
|
183
|
+
if (!['http:', 'https:', 'mailto:', 'tel:', 'dns:', 'openpgp4fpr:'].includes(url.protocol)
|
|
184
|
+
|| url.username
|
|
185
|
+
|| url.password)
|
|
186
|
+
return null;
|
|
187
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
188
|
+
url.search = '';
|
|
189
|
+
url.hash = '';
|
|
190
|
+
}
|
|
191
|
+
return url.toString().slice(0, 2048);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function portableOrigin(value) {
|
|
198
|
+
const text = boundedString(value, 2048);
|
|
199
|
+
if (!text)
|
|
200
|
+
return null;
|
|
201
|
+
try {
|
|
202
|
+
const url = new URL(text);
|
|
203
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password
|
|
204
|
+
? url.origin
|
|
205
|
+
: null;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function consumePortableProjectionEntry(state, depth) {
|
|
212
|
+
state.entries += 1;
|
|
213
|
+
if (state.entries > LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES) {
|
|
214
|
+
throw new TypeError(`Lookup evidence projection exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES.toLocaleString('en')}-entry portable limit.`);
|
|
215
|
+
}
|
|
216
|
+
if (depth > LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH) {
|
|
217
|
+
throw new TypeError(`Lookup evidence projection exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH}-level portable nesting limit.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function projectPortableString(value) {
|
|
221
|
+
const sanitized = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, ' ');
|
|
222
|
+
return sanitized.length <= LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH
|
|
223
|
+
? sanitized
|
|
224
|
+
: sanitized.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH);
|
|
225
|
+
}
|
|
226
|
+
function projectLookupEvidenceAvailabilityString(value) {
|
|
227
|
+
const sanitized = projectPortableString(value)
|
|
228
|
+
.replace(/[\u0080-\u009f]/gu, ' ')
|
|
229
|
+
.replace(/\p{Default_Ignorable_Code_Point}/gu, '');
|
|
230
|
+
return isUriShapedLabel(value) ? null : sanitized;
|
|
231
|
+
}
|
|
232
|
+
function projectLookupEvidencePrivacySafeTreeValue(value, state, depth) {
|
|
233
|
+
consumePortableProjectionEntry(state, depth);
|
|
234
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
235
|
+
return value;
|
|
236
|
+
if (typeof value === 'string')
|
|
237
|
+
return projectPortableString(value);
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
240
|
+
.map((item) => projectLookupEvidencePrivacySafeTreeValue(item, state, depth + 1));
|
|
241
|
+
}
|
|
242
|
+
const source = recordOrNull(value);
|
|
243
|
+
if (!source)
|
|
244
|
+
return null;
|
|
245
|
+
const output = {};
|
|
246
|
+
for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
|
|
247
|
+
if (!isSafeJsonObjectKey(key))
|
|
248
|
+
throw new TypeError('Lookup evidence projection contains an unsafe object key.');
|
|
249
|
+
const normalized = normalizedEvidenceKey(key);
|
|
250
|
+
if (privateEvidenceKey(normalized, item))
|
|
251
|
+
continue;
|
|
252
|
+
if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized)) {
|
|
253
|
+
consumePortableProjectionEntry(state, depth + 1);
|
|
254
|
+
output[key] = portableUri(item);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
output[key] = projectLookupEvidencePrivacySafeTreeValue(item, state, depth + 1);
|
|
258
|
+
}
|
|
259
|
+
return output;
|
|
260
|
+
}
|
|
261
|
+
export function projectLookupEvidencePrivacySafeTree(value) {
|
|
262
|
+
return projectLookupEvidencePrivacySafeTreeValue(value, { entries: 0 }, 0);
|
|
263
|
+
}
|
|
264
|
+
function projectLookupEvidenceAvailabilityPublicValue(value, path, state, depth) {
|
|
265
|
+
consumePortableProjectionEntry(state, depth);
|
|
266
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
267
|
+
return value;
|
|
268
|
+
if (typeof value === 'string') {
|
|
269
|
+
const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
|
|
270
|
+
if (PORTABLE_URL_KEYS.has(normalizedParent))
|
|
271
|
+
return portableUri(value);
|
|
272
|
+
if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
|
|
273
|
+
return portableOrigin(value);
|
|
274
|
+
return projectPortableString(value);
|
|
275
|
+
}
|
|
276
|
+
if (Array.isArray(value)) {
|
|
277
|
+
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
278
|
+
.map((item) => projectLookupEvidenceAvailabilityPublicValue(item, path, state, depth + 1));
|
|
279
|
+
}
|
|
280
|
+
const source = recordOrNull(value);
|
|
281
|
+
if (!source)
|
|
282
|
+
return null;
|
|
283
|
+
const output = {};
|
|
284
|
+
for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
|
|
285
|
+
if (!LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS.has(key)
|
|
286
|
+
|| privateEvidenceKey(key, item))
|
|
287
|
+
continue;
|
|
288
|
+
const parentPath = path.join('.');
|
|
289
|
+
const sanClassCount = parentPath === 'tls.certificate.subjectAltNames.classes'
|
|
290
|
+
&& LOOKUP_AVAILABILITY_TLS_SAN_CLASS_KEYS.has(key)
|
|
291
|
+
&& Number.isSafeInteger(item)
|
|
292
|
+
&& Number(item) >= 0
|
|
293
|
+
&& Number(item) <= LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS;
|
|
294
|
+
if (['dns', 'ip', 'directoryName', 'registeredId', 'otherName'].includes(key)
|
|
295
|
+
&& !sanClassCount)
|
|
296
|
+
continue;
|
|
297
|
+
if (parentPath === 'tls.certificate.subjectAltNames.classes'
|
|
298
|
+
&& LOOKUP_AVAILABILITY_TLS_SAN_CLASS_KEYS.has(key)
|
|
299
|
+
&& !sanClassCount)
|
|
300
|
+
continue;
|
|
301
|
+
if (key === 'classes'
|
|
302
|
+
&& (parentPath !== 'tls.certificate.subjectAltNames' || !recordOrNull(item)))
|
|
303
|
+
continue;
|
|
304
|
+
if (key === 'similarity'
|
|
305
|
+
&& (parentPath !== 'pageIdentity.fingerprints.domStructure'
|
|
306
|
+
|| (item !== null && !recordOrNull(item))))
|
|
307
|
+
continue;
|
|
308
|
+
if (key === 'description'
|
|
309
|
+
&& (parentPath !== 'technologyProfile.findings.evidence'
|
|
310
|
+
|| typeof item !== 'string'))
|
|
311
|
+
continue;
|
|
312
|
+
output[key] = projectLookupEvidenceAvailabilityPublicValue(item, [...path, key], state, depth + 1);
|
|
313
|
+
}
|
|
314
|
+
return output;
|
|
315
|
+
}
|
|
316
|
+
function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
|
|
317
|
+
consumePortableProjectionEntry(state, depth);
|
|
318
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
319
|
+
return value;
|
|
320
|
+
if (typeof value === 'string') {
|
|
321
|
+
const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
|
|
322
|
+
if (PORTABLE_URL_KEYS.has(normalizedParent))
|
|
323
|
+
return portableUri(value);
|
|
324
|
+
if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
|
|
325
|
+
return portableOrigin(value);
|
|
326
|
+
return projectLookupEvidenceAvailabilityString(value);
|
|
327
|
+
}
|
|
328
|
+
if (Array.isArray(value)) {
|
|
329
|
+
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
330
|
+
.map((item) => projectLookupEvidenceAvailabilityValue(item, path, state, depth + 1));
|
|
331
|
+
}
|
|
332
|
+
const source = recordOrNull(value);
|
|
333
|
+
if (!source)
|
|
334
|
+
return null;
|
|
335
|
+
const output = {};
|
|
336
|
+
for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
|
|
337
|
+
if (!LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS.has(key)
|
|
338
|
+
|| privateEvidenceKey(key, item))
|
|
339
|
+
continue;
|
|
340
|
+
const parentPath = path.join('.');
|
|
341
|
+
const boundedCount = Number.isSafeInteger(item)
|
|
342
|
+
&& Number(item) >= 0
|
|
343
|
+
&& Number(item) <= LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS;
|
|
344
|
+
const credentialCategoryCount = parentPath === 'credentialSurfaceProfile.inputs.categories'
|
|
345
|
+
&& boundedCount;
|
|
346
|
+
const sanClassCount = parentPath === 'tls.certificate.subjectAltNames.classes'
|
|
347
|
+
&& LOOKUP_AVAILABILITY_TLS_SAN_CLASS_KEYS.has(key)
|
|
348
|
+
&& boundedCount;
|
|
349
|
+
if (LOOKUP_AVAILABILITY_CREDENTIAL_CATEGORY_KEYS.has(key)
|
|
350
|
+
&& !credentialCategoryCount
|
|
351
|
+
&& !sanClassCount)
|
|
352
|
+
continue;
|
|
353
|
+
if (['dns', 'ip', 'directoryName', 'registeredId', 'otherName'].includes(key)
|
|
354
|
+
&& !sanClassCount)
|
|
355
|
+
continue;
|
|
356
|
+
if (key === 'classes'
|
|
357
|
+
&& (parentPath !== 'tls.certificate.subjectAltNames' || !recordOrNull(item)))
|
|
358
|
+
continue;
|
|
359
|
+
if (key === 'similarity'
|
|
360
|
+
&& (parentPath !== 'pageIdentity.fingerprints.domStructure'
|
|
361
|
+
|| (item !== null && !recordOrNull(item))))
|
|
362
|
+
continue;
|
|
363
|
+
if (key === 'description'
|
|
364
|
+
&& (parentPath !== 'technologyProfile.findings.evidence'
|
|
365
|
+
|| typeof item !== 'string'))
|
|
366
|
+
continue;
|
|
367
|
+
if (key === 'owner'
|
|
368
|
+
&& (!LOOKUP_AVAILABILITY_OWNER_PATHS.has(parentPath) || typeof item !== 'string'))
|
|
369
|
+
continue;
|
|
370
|
+
if (key === 'value'
|
|
371
|
+
&& (!LOOKUP_AVAILABILITY_VALUE_PATHS.has(parentPath)
|
|
372
|
+
|| (item !== null && typeof item !== 'string')))
|
|
373
|
+
continue;
|
|
374
|
+
output[key] = projectLookupEvidenceAvailabilityValue(item, [...path, key], state, depth + 1);
|
|
375
|
+
}
|
|
376
|
+
return output;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Canonical portable projection for the bounded Lookup availability models.
|
|
380
|
+
* The root and every nested record are positive allowlists, so an imported
|
|
381
|
+
* extension cannot become authenticated evidence merely by avoiding a
|
|
382
|
+
* credential-key denylist.
|
|
383
|
+
*/
|
|
384
|
+
function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryContactsExcluded, currentPrivacyRules) {
|
|
385
|
+
const source = recordOrNull(value);
|
|
386
|
+
if (!source)
|
|
387
|
+
return null;
|
|
388
|
+
const state = { entries: 0 };
|
|
389
|
+
consumePortableProjectionEntry(state, 0);
|
|
390
|
+
const output = {};
|
|
391
|
+
for (const key of rootKeys) {
|
|
392
|
+
if (!Object.hasOwn(source, key))
|
|
393
|
+
continue;
|
|
394
|
+
const item = source[key];
|
|
395
|
+
if (privateEvidenceKey(key, item))
|
|
396
|
+
continue;
|
|
397
|
+
output[key] = currentPrivacyRules
|
|
398
|
+
? projectLookupEvidenceAvailabilityValue(item, [key], state, 1)
|
|
399
|
+
: projectLookupEvidenceAvailabilityPublicValue(item, [key], state, 1);
|
|
400
|
+
}
|
|
401
|
+
if (registryContactsExcluded)
|
|
402
|
+
output.registryContactsExcluded = true;
|
|
403
|
+
const pageIdentity = recordOrNull(source.pageIdentity);
|
|
404
|
+
const publicationMetadata = pageIdentity?.publicationMetadata;
|
|
405
|
+
if (publicationMetadata !== undefined) {
|
|
406
|
+
if (!['success', 'partial'].includes(String(pageIdentity?.status))
|
|
407
|
+
|| !validPagePublicationMetadata(publicationMetadata)) {
|
|
408
|
+
throw new TypeError('Lookup evidence contains invalid page publication metadata.');
|
|
409
|
+
}
|
|
410
|
+
const projectedPageIdentity = recordOrNull(output.pageIdentity);
|
|
411
|
+
if (projectedPageIdentity) {
|
|
412
|
+
projectedPageIdentity.publicationMetadata = projectLookupEvidencePrivacySafeTree(publicationMetadata);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const httpResponse = recordOrNull(recordOrNull(source.http)?.response);
|
|
416
|
+
const sourceHttp = recordOrNull(source.http);
|
|
417
|
+
const deliveryMetadata = httpResponse?.deliveryMetadata;
|
|
418
|
+
if (deliveryMetadata !== undefined) {
|
|
419
|
+
if (!['success', 'partial'].includes(String(sourceHttp?.status))
|
|
420
|
+
|| !validHttpDeliveryMetadata(deliveryMetadata)) {
|
|
421
|
+
throw new TypeError('Lookup evidence contains invalid HTTP delivery metadata.');
|
|
422
|
+
}
|
|
423
|
+
const projectedHttp = recordOrNull(output.http);
|
|
424
|
+
const projectedResponse = recordOrNull(projectedHttp?.response);
|
|
425
|
+
if (projectedResponse) {
|
|
426
|
+
projectedResponse.deliveryMetadata = projectLookupEvidencePrivacySafeTree(deliveryMetadata);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return output;
|
|
430
|
+
}
|
|
431
|
+
/** Exact availability projection used only to validate public schema 26. */
|
|
432
|
+
export function projectLookupEvidenceAvailabilityPublic(value) {
|
|
433
|
+
return projectLookupEvidenceAvailabilityWithKeys(value, PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, false, false);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Current availability projection. Registry-derived registrar, registrant,
|
|
437
|
+
* and abuse contact routes are deliberately excluded at the root boundary;
|
|
438
|
+
* the marker prevents that privacy omission from being read as source absence.
|
|
439
|
+
*/
|
|
440
|
+
export function projectLookupEvidenceAvailability(value) {
|
|
441
|
+
return projectLookupEvidenceAvailabilityWithKeys(value, LOOKUP_AVAILABILITY_ANALYSIS_KEYS, true, true);
|
|
442
|
+
}
|
|
443
|
+
export function assertLookupEvidencePrivacySafeTree(value) {
|
|
444
|
+
assertLookupEvidencePortableTree(value);
|
|
445
|
+
const pending = [value];
|
|
446
|
+
while (pending.length) {
|
|
447
|
+
const current = pending.pop();
|
|
448
|
+
if (current === null || typeof current === 'boolean' || typeof current === 'number')
|
|
449
|
+
continue;
|
|
450
|
+
if (typeof current === 'string') {
|
|
451
|
+
if (projectLookupEvidencePrivacySafeTree(current) !== current) {
|
|
452
|
+
throw new TypeError('Lookup evidence contains a non-portable string value.');
|
|
453
|
+
}
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (Array.isArray(current)) {
|
|
457
|
+
for (const item of current)
|
|
458
|
+
pending.push(item);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const source = recordOrNull(current);
|
|
462
|
+
if (!source)
|
|
463
|
+
throw new TypeError('Lookup evidence contains a non-JSON value.');
|
|
464
|
+
for (const [key, item] of Object.entries(source)) {
|
|
465
|
+
const normalized = normalizedEvidenceKey(key);
|
|
466
|
+
if (privateEvidenceKey(normalized, item)) {
|
|
467
|
+
throw new TypeError('Lookup evidence contains excluded private request or session material.');
|
|
468
|
+
}
|
|
469
|
+
if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized) && portableUri(item) !== item) {
|
|
470
|
+
throw new TypeError('Lookup evidence contains a credential-bearing or non-portable URL.');
|
|
471
|
+
}
|
|
472
|
+
pending.push(item);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
export function projectedKnownRecord(value, keys) {
|
|
477
|
+
const source = recordOrNull(value);
|
|
478
|
+
if (!source)
|
|
479
|
+
return null;
|
|
480
|
+
const output = {};
|
|
481
|
+
for (const key of keys) {
|
|
482
|
+
if (Object.hasOwn(source, key))
|
|
483
|
+
output[key] = projectLookupEvidencePrivacySafeTree(source[key]);
|
|
484
|
+
}
|
|
485
|
+
return output;
|
|
486
|
+
}
|
|
487
|
+
export function assertLookupEvidencePortableTree(value) {
|
|
488
|
+
const pending = [{ value, depth: 0 }];
|
|
489
|
+
let entries = 0;
|
|
490
|
+
while (pending.length) {
|
|
491
|
+
const current = pending.pop();
|
|
492
|
+
if (!current)
|
|
493
|
+
break;
|
|
494
|
+
entries += 1;
|
|
495
|
+
if (entries > LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES) {
|
|
496
|
+
throw new TypeError(`Lookup evidence exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES.toLocaleString('en')}-entry portable limit.`);
|
|
497
|
+
}
|
|
498
|
+
if (current.depth > LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH) {
|
|
499
|
+
throw new TypeError(`Lookup evidence exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH}-level portable nesting limit.`);
|
|
500
|
+
}
|
|
501
|
+
if (typeof current.value === 'string') {
|
|
502
|
+
if (current.value.length > LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH) {
|
|
503
|
+
throw new TypeError('Lookup evidence contains an over-bound string.');
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (current.value === null || typeof current.value === 'boolean')
|
|
508
|
+
continue;
|
|
509
|
+
if (typeof current.value === 'number') {
|
|
510
|
+
if (!Number.isFinite(current.value)) {
|
|
511
|
+
throw new TypeError('Lookup evidence contains a non-finite number.');
|
|
512
|
+
}
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (Array.isArray(current.value)) {
|
|
516
|
+
if (current.value.length > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS) {
|
|
517
|
+
throw new TypeError('Lookup evidence contains an over-bound array.');
|
|
518
|
+
}
|
|
519
|
+
for (const item of current.value)
|
|
520
|
+
pending.push({ value: item, depth: current.depth + 1 });
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (current.value && typeof current.value === 'object') {
|
|
524
|
+
const source = current.value;
|
|
525
|
+
const prototype = Object.getPrototypeOf(source);
|
|
526
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
527
|
+
throw new TypeError('Lookup evidence contains a non-JSON object.');
|
|
528
|
+
}
|
|
529
|
+
const keys = Object.keys(source);
|
|
530
|
+
if (keys.length > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS
|
|
531
|
+
|| keys.some((key) => key.length > LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH
|
|
532
|
+
|| /[\u0000-\u001f\u007f]/u.test(key)
|
|
533
|
+
|| !isSafeJsonObjectKey(key))) {
|
|
534
|
+
throw new TypeError('Lookup evidence contains an over-bound object.');
|
|
535
|
+
}
|
|
536
|
+
for (const item of Object.values(source))
|
|
537
|
+
pending.push({ value: item, depth: current.depth + 1 });
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
throw new TypeError('Lookup evidence contains a non-JSON value.');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
export function serializeLookupEvidence(value, pretty = false) {
|
|
544
|
+
assertLookupEvidencePortableTree(value);
|
|
545
|
+
let output;
|
|
546
|
+
try {
|
|
547
|
+
output = JSON.stringify(value, null, pretty ? 2 : 0);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
throw new TypeError('Lookup evidence could not be serialized as JSON.');
|
|
551
|
+
}
|
|
552
|
+
if (new TextEncoder().encode(output).byteLength > LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES) {
|
|
553
|
+
throw new TypeError('Lookup evidence exceeds the 5 MiB portable file limit.');
|
|
554
|
+
}
|
|
555
|
+
return output;
|
|
556
|
+
}
|
|
557
|
+
export function boundedString(value, maximum) {
|
|
558
|
+
if (typeof value !== 'string' || value.length > maximum || /[\u0000-\u001f\u007f]/.test(value))
|
|
559
|
+
return null;
|
|
560
|
+
return value.replace(/\s+/g, ' ').trim() || null;
|
|
561
|
+
}
|