@slicedearth/whoisleuth-cli 2.0.1 → 2.2.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.
Files changed (97) hide show
  1. package/cli/arguments.mjs +21 -5
  2. package/cli/artifact-validation/case-response.mjs +771 -0
  3. package/cli/artifact-validation/investigation-capsule.mjs +426 -0
  4. package/cli/artifact-validation/lookup-evidence.mjs +718 -0
  5. package/cli/artifact-validation/signed-review.mjs +598 -0
  6. package/cli/artifact-validation/structure-primitives.mjs +105 -0
  7. package/cli/artifact-verify.mjs +1 -1
  8. package/cli/assurance-command-runner.mjs +324 -0
  9. package/cli/command-reference.mjs +1175 -843
  10. package/cli/completion.mjs +56 -9
  11. package/cli/discriminated-command-handlers.mjs +11 -0
  12. package/cli/doctor.mjs +2 -2
  13. package/cli/evidence-command-runner.mjs +41 -31
  14. package/cli/export-evidence.mjs +1 -1
  15. package/cli/fail-policy.mjs +33 -8
  16. package/cli/formatters/terminal-command-formats.mjs +437 -0
  17. package/cli/formatters/terminal-lookup.mjs +758 -0
  18. package/cli/formatters/terminal-metadata.mjs +58 -0
  19. package/cli/formatters/terminal-shared.mjs +83 -0
  20. package/cli/formatters/terminal.mjs +5 -1232
  21. package/cli/history-command-runner.mjs +132 -0
  22. package/cli/inline-command-families.mjs +45 -0
  23. package/cli/inline-command-runner.mjs +55 -0
  24. package/cli/investigation-plan.mjs +9 -2
  25. package/cli/investigation-run.mjs +114 -9
  26. package/cli/lookup-brief.mjs +141 -23
  27. package/cli/lookup-browser-view.mjs +289 -0
  28. package/cli/lookup-browser.mjs +3 -288
  29. package/cli/mail-review.mjs +2 -6
  30. package/cli/offline-artifact-validation.mjs +19 -0
  31. package/cli/review-command-runner.mjs +269 -0
  32. package/cli/runner.mjs +15 -972
  33. package/cli/saved-lookup.mjs +19 -2
  34. package/cli/support-command-runner.mjs +229 -0
  35. package/cli/workflow-command-runner.mjs +129 -0
  36. package/docs/cli-reference.md +16 -9
  37. package/docs/cli.md +16 -2
  38. package/lib/availability.mjs +12 -9
  39. package/lib/bounded-contract-normalizers.mjs +2 -9
  40. package/lib/browser-library-profile.mjs +1 -2
  41. package/lib/client-behavior-profile.mjs +1 -2
  42. package/lib/credential-surface-profile.mjs +1 -1
  43. package/lib/domain-posture-analysis.mjs +10 -1
  44. package/lib/domain-posture.mjs +51 -23
  45. package/lib/evidence-export.mjs +47 -5
  46. package/lib/evidence-report-markdown.mjs +4 -0
  47. package/lib/evidence-report.mjs +50 -0
  48. package/lib/generated/cisa-kev-catalog.mjs +34 -4
  49. package/lib/generated/registrar-standing-catalogue.mjs +367 -0
  50. package/lib/json-record.mjs +12 -0
  51. package/lib/lookup-child-profile-contract.mjs +858 -0
  52. package/lib/lookup-contract-primitives.mjs +98 -0
  53. package/lib/lookup-diagnostics.mjs +96 -0
  54. package/lib/lookup-network-evidence-bounds.mjs +1 -0
  55. package/lib/lookup-response.mjs +367 -0
  56. package/lib/lookup.mjs +28 -443
  57. package/lib/page-role-profile.mjs +1 -3
  58. package/lib/registrar-standing-catalogue-contract.mjs +70 -0
  59. package/lib/registrar-standing-contract.mjs +300 -0
  60. package/lib/registrar-standing.mjs +269 -0
  61. package/lib/registry-insights.mjs +2 -8
  62. package/lib/risk-scoring.mjs +26 -10
  63. package/lib/sslbl-certificates.generated.mjs +138 -136
  64. package/lib/structured-data-identity.mjs +1 -3
  65. package/lib/technology-evidence-role.mjs +51 -0
  66. package/lib/threat-intelligence-runtime.mjs +3 -3
  67. package/lib/threat-intelligence-types.mjs +13 -0
  68. package/lib/threatfox-intelligence.mjs +1 -3
  69. package/lib/tls-intelligence.mjs +1 -2
  70. package/lib/urlhaus-intelligence.mjs +1 -3
  71. package/lib/urlscan-intelligence.mjs +1 -3
  72. package/lib/website-security-posture.mjs +1 -2
  73. package/lib/website-technology.mjs +72 -23
  74. package/lib/whois-common-formats.mjs +276 -0
  75. package/lib/whois-dialects.mjs +534 -0
  76. package/lib/whois-parser-support.mjs +305 -0
  77. package/lib/whois-parser.mjs +135 -0
  78. package/lib/whois.mjs +6 -1187
  79. package/package.json +2 -2
  80. package/packages/cases/case-evidence-model.mjs +9 -0
  81. package/packages/cases/case-migration-model.mjs +17 -11
  82. package/packages/cases/case-record-operations.mjs +235 -0
  83. package/packages/cases/case-response-model.mjs +13 -9
  84. package/packages/cases/case-response-packet.mjs +297 -112
  85. package/packages/comparison/page-similarity.mjs +23 -0
  86. package/packages/contracts/case-portability.mjs +235 -61
  87. package/packages/contracts/cli-lookup.mjs +4 -3
  88. package/packages/contracts/domain-control-manifest.mjs +1 -1
  89. package/packages/contracts/investigation-portability.mjs +1 -1
  90. package/packages/contracts/risk-calibration.mjs +2 -2
  91. package/packages/contracts/workspace-portability.mjs +34 -6
  92. package/packages/interchange/external-findings-import.mjs +113 -45
  93. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  94. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  95. package/packages/workspace/website-snapshot-model.mjs +79 -6
  96. package/third-party-notices.txt +1 -1
  97. package/cli/artifact-structure.mjs +0 -2507
@@ -0,0 +1,858 @@
1
+ // Browser-safe version and collection boundaries shared by Lookup child
2
+ // profile producers and the client response trust boundary.
3
+ import { MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, normalizeExplicitIsoTimestamp, } from "../packages/evidence/observation.mjs";
4
+ import { PAGE_FINGERPRINT_VERSION, PAGE_IDENTITY_VERSION, } from "../packages/contracts/workspace-portability.mjs";
5
+ import { validPagePublicationMetadata } from "./homepage-metadata-contract.mjs";
6
+ import { MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, TLS_PROFILE_VERSION, } from "./lookup-network-evidence-bounds.mjs";
7
+ import { isJsonObject, normalizedDomain, validBoundedString, validHttpProvenanceUrl, validObservationDiagnostics, validOptionalNullableText, validStringArray, validUint, } from "./lookup-contract-primitives.mjs";
8
+ import { TECHNOLOGY_EVIDENCE_ROLE_ORDER } from "./technology-evidence-role.mjs";
9
+ export const TECHNOLOGY_PROFILE_VERSION = 11;
10
+ export const SUPPORTED_TECHNOLOGY_PROFILE_VERSIONS = Object.freeze([10, TECHNOLOGY_PROFILE_VERSION]);
11
+ export const MAX_TECHNOLOGY_FINDINGS = 24;
12
+ export const MAX_EVIDENCE_PER_TECHNOLOGY = 4;
13
+ export const MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH = 180;
14
+ export const BROWSER_LIBRARY_PROFILE_VERSION = 2;
15
+ export const MAX_LIBRARY_FINDINGS = 16;
16
+ export const WEBSITE_SECURITY_POSTURE_VERSION = 2;
17
+ export const MAX_SECURITY_POSTURE_FINDINGS = 32;
18
+ export const CREDENTIAL_SURFACE_PROFILE_VERSION = 1;
19
+ export const STRUCTURED_DATA_IDENTITY_VERSION = 1;
20
+ export const MAX_STRUCTURED_DATA_ENTITIES = 16;
21
+ export const MAX_STRUCTURED_DATA_SAME_AS_HOSTS = 12;
22
+ export const PAGE_ROLE_PROFILE_VERSION = 1;
23
+ export const MAX_PAGE_ROLE_FINDINGS = 4;
24
+ export const MAX_PAGE_ROLE_EVIDENCE = 4;
25
+ export const CLIENT_BEHAVIOR_PROFILE_VERSION = 1;
26
+ export const MAX_CLIENT_BEHAVIOR_INDICATORS = 12;
27
+ const OBSERVATION_FIELDS = Object.freeze([
28
+ 'version', 'status', 'observedAt', 'scanMode', 'source', 'durationMs',
29
+ 'complete', 'truncated', 'limitations', 'diagnostics',
30
+ ]);
31
+ const TECHNOLOGY_CATEGORIES = new Set([
32
+ 'application runtime', 'content management', 'commerce', 'site builder',
33
+ 'web framework', 'static site generator', 'web server', 'delivery platform',
34
+ ]);
35
+ const TECHNOLOGY_CONFIDENCE = new Set(['high', 'medium']);
36
+ const TECHNOLOGY_EVIDENCE_SOURCES = new Set([
37
+ 'generator metadata', 'static HTML', 'resource origin', 'HTTP server header',
38
+ 'passive response header',
39
+ ]);
40
+ const TECHNOLOGY_EVIDENCE_ROLES = new Set([
41
+ 'application_platform', 'embedded_dependency', 'framework_runtime', 'observed_edge',
42
+ ]);
43
+ const TECHNOLOGY_ID_RE = /^[a-z0-9][a-z0-9_-]{0,79}$/u;
44
+ const SECURITY_POSTURE_CATEGORIES = new Set([
45
+ 'transport', 'response headers', 'forms and resources', 'certificate', 'domain controls',
46
+ ]);
47
+ const STRUCTURED_DATA_TYPES = new Set([
48
+ 'Brand', 'Corporation', 'EducationalOrganization', 'GovernmentOrganization',
49
+ 'LocalBusiness', 'NewsMediaOrganization', 'NGO', 'OnlineBusiness', 'Organization',
50
+ 'PerformingGroup', 'Project', 'SportsOrganization', 'WebPage', 'WebSite',
51
+ ]);
52
+ const PAGE_ROLE_VALUES = new Set([
53
+ 'access_challenge', 'authentication', 'commerce', 'support_contact',
54
+ 'parked_sale', 'content', 'unknown',
55
+ ]);
56
+ const CLIENT_BEHAVIOR_IDS = new Set([
57
+ 'inline_event_handlers', 'client_navigation', 'form_interception',
58
+ 'service_worker', 'browser_storage', 'clipboard_access',
59
+ 'dynamic_code_evaluation', 'browser_fingerprinting', 'persistent_connection',
60
+ ]);
61
+ const SHA1_RE = /^[a-f0-9]{40}$/u;
62
+ const SHA256_RE = /^[a-f0-9]{64}$/u;
63
+ const SIMHASH64_RE = /^[a-f0-9]{16}$/u;
64
+ const OID_RE = /^\d{1,10}(?:\.\d{1,10}){1,31}$/u;
65
+ const CREDENTIAL_METHOD_KEYS = Object.freeze(['missing', 'get', 'post', 'dialog', 'other']);
66
+ const CREDENTIAL_ACTION_KEYS = Object.freeze(['sameOrigin', 'external', 'missing', 'cleartext', 'unclassified']);
67
+ const CREDENTIAL_CATEGORY_KEYS = Object.freeze(['password', 'email', 'username', 'one_time_code', 'payment']);
68
+ const TLS_NAME_KEYS = Object.freeze([
69
+ 'commonNames', 'organizations', 'organizationalUnits', 'countries', 'localities', 'states',
70
+ ]);
71
+ const TLS_SAN_CLASS_KEYS = Object.freeze([
72
+ 'dns', 'ip', 'email', 'uri', 'directoryName', 'registeredId', 'otherName', 'unclassified',
73
+ ]);
74
+ function hasOnlyKeys(value, allowed) {
75
+ const keys = Object.keys(value);
76
+ const allowedKeys = new Set(allowed);
77
+ return keys.length <= allowed.length && keys.every((key) => allowedKeys.has(key));
78
+ }
79
+ function childVersionState(value, field, supportedVersions) {
80
+ if (!isJsonObject(value))
81
+ return 'invalid';
82
+ const version = value[field];
83
+ if (Number.isSafeInteger(version) && supportedVersions.includes(Number(version)))
84
+ return 'supported';
85
+ return Number.isSafeInteger(version) && Number(version) > Math.max(...supportedVersions)
86
+ ? 'unsupported'
87
+ : 'invalid';
88
+ }
89
+ function validExactStringArray(value, maximumItems, maximumLength, allowed) {
90
+ return Array.isArray(value)
91
+ && value.length <= maximumItems
92
+ && value.every((item) => validBoundedString(item, maximumLength)
93
+ && (!allowed || allowed.has(String(item))))
94
+ && new Set(value).size === value.length;
95
+ }
96
+ function validProfileObservation(value, statuses, sources) {
97
+ if (value.version !== OBSERVATION_VERSION
98
+ || typeof value.status !== 'string'
99
+ || !statuses.has(value.status)
100
+ || typeof value.source !== 'string'
101
+ || !sources.has(value.source)
102
+ || normalizeExplicitIsoTimestamp(value.observedAt) !== value.observedAt
103
+ || value.scanMode !== 'deep'
104
+ || !(value.durationMs === null
105
+ || Number.isSafeInteger(value.durationMs)
106
+ && Number(value.durationMs) >= 0
107
+ && Number(value.durationMs) <= 120_000)
108
+ || typeof value.complete !== 'boolean'
109
+ || typeof value.truncated !== 'boolean'
110
+ || !validExactStringArray(value.limitations, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH)
111
+ || !isJsonObject(value.diagnostics)
112
+ || !validObservationDiagnostics(value.diagnostics))
113
+ return false;
114
+ if (value.status === 'success' && value.complete !== true)
115
+ return false;
116
+ if (value.status !== 'success' && value.complete !== false)
117
+ return false;
118
+ if (value.complete === true && value.truncated === true)
119
+ return false;
120
+ return true;
121
+ }
122
+ function hasExactKeys(value, expected) {
123
+ return Object.keys(value).length === expected.length && hasOnlyKeys(value, expected);
124
+ }
125
+ function validNullableBoundedString(value, maximum) {
126
+ return value === null || validBoundedString(value, maximum);
127
+ }
128
+ function validNullableDigest(value, expression) {
129
+ return value === null || typeof value === 'string' && expression.test(value);
130
+ }
131
+ function validNullableIsoTimestamp(value) {
132
+ return value === null || typeof value === 'string' && normalizeExplicitIsoTimestamp(value) === value;
133
+ }
134
+ function validExactUintRecord(value, keys, maximum) {
135
+ return isJsonObject(value)
136
+ && hasExactKeys(value, keys)
137
+ && keys.every((key) => validUint(value[key], maximum));
138
+ }
139
+ function validHttpOrigin(value) {
140
+ if (!validBoundedString(value, 2_048))
141
+ return false;
142
+ try {
143
+ const parsed = new URL(String(value));
144
+ return ['http:', 'https:'].includes(parsed.protocol)
145
+ && Boolean(parsed.hostname)
146
+ && !parsed.username
147
+ && !parsed.password
148
+ && !parsed.search
149
+ && !parsed.hash
150
+ && parsed.pathname === '/'
151
+ && [parsed.origin, `${parsed.origin}/`].includes(String(value));
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ }
157
+ function validHttpOriginArray(value, maximumItems) {
158
+ return Array.isArray(value)
159
+ && value.length <= maximumItems
160
+ && value.every(validHttpOrigin)
161
+ && new Set(value).size === value.length;
162
+ }
163
+ function validFingerprintIdentifier(value) {
164
+ return isJsonObject(value)
165
+ && hasExactKeys(value, ['type', 'value'])
166
+ && typeof value.type === 'string'
167
+ && /^[a-z-]{1,40}$/u.test(value.type)
168
+ && typeof value.value === 'string'
169
+ && /^[A-Z0-9-]{1,64}$/u.test(value.value);
170
+ }
171
+ function validPageFingerprintProfile(value) {
172
+ const normalizedHtml = value.normalizedHtml;
173
+ const visibleText = value.visibleText;
174
+ const domStructure = value.domStructure;
175
+ const domStructureBaseKeys = ['algorithm', 'value', 'nodeCount', 'parser', 'truncated'];
176
+ const formStructure = value.formStructure;
177
+ const resourceHosts = value.resourceHosts;
178
+ const identifiers = value.identifiers;
179
+ if (!isJsonObject(normalizedHtml)
180
+ || !hasExactKeys(normalizedHtml, ['algorithm', 'value', 'tokenCount', 'truncated'])
181
+ || normalizedHtml.algorithm !== 'sha256'
182
+ || typeof normalizedHtml.value !== 'string'
183
+ || !SHA256_RE.test(normalizedHtml.value)
184
+ || !validUint(normalizedHtml.tokenCount, 4_096)
185
+ || typeof normalizedHtml.truncated !== 'boolean'
186
+ || !(visibleText === null || isJsonObject(visibleText)
187
+ && hasExactKeys(visibleText, ['algorithm', 'value', 'tokenCount', 'featureCount', 'truncated'])
188
+ && visibleText.algorithm === 'simhash64-v1'
189
+ && typeof visibleText.value === 'string'
190
+ && SIMHASH64_RE.test(visibleText.value)
191
+ && validUint(visibleText.tokenCount, 8_192)
192
+ && validUint(visibleText.featureCount, 8_192)
193
+ && typeof visibleText.truncated === 'boolean')
194
+ || !isJsonObject(domStructure)
195
+ || !hasOnlyKeys(domStructure, [...domStructureBaseKeys, 'similarity'])
196
+ || !domStructureBaseKeys.every((key) => Object.hasOwn(domStructure, key))
197
+ || domStructure.algorithm !== 'sha256'
198
+ || typeof domStructure.value !== 'string'
199
+ || !SHA256_RE.test(domStructure.value)
200
+ || !validUint(domStructure.nodeCount, 4_096)
201
+ || domStructure.parser !== 'static-tag-sequence-v1'
202
+ || typeof domStructure.truncated !== 'boolean'
203
+ || !(domStructure.similarity === undefined || domStructure.similarity === null || isJsonObject(domStructure.similarity)
204
+ && hasExactKeys(domStructure.similarity, ['algorithm', 'value', 'tokenCount', 'featureCount', 'truncated'])
205
+ && domStructure.similarity.algorithm === 'simhash64-v1'
206
+ && typeof domStructure.similarity.value === 'string'
207
+ && SIMHASH64_RE.test(domStructure.similarity.value)
208
+ && validUint(domStructure.similarity.tokenCount, 4_096)
209
+ && validUint(domStructure.similarity.featureCount, 4_096)
210
+ && typeof domStructure.similarity.truncated === 'boolean')
211
+ || !(formStructure === null || isJsonObject(formStructure)
212
+ && hasExactKeys(formStructure, ['algorithm', 'value', 'formCount', 'controlCount', 'truncated'])
213
+ && formStructure.algorithm === 'sha256'
214
+ && typeof formStructure.value === 'string'
215
+ && SHA256_RE.test(formStructure.value)
216
+ && validUint(formStructure.formCount, 50)
217
+ && validUint(formStructure.controlCount, 500)
218
+ && typeof formStructure.truncated === 'boolean')
219
+ || !isJsonObject(resourceHosts)
220
+ || !hasExactKeys(resourceHosts, ['algorithm', 'value', 'values', 'truncated'])
221
+ || resourceHosts.algorithm !== 'set-sha256'
222
+ || !validNullableDigest(resourceHosts.value, SHA256_RE)
223
+ || !validExactStringArray(resourceHosts.values, 30, 253)
224
+ || typeof resourceHosts.truncated !== 'boolean'
225
+ || !isJsonObject(identifiers)
226
+ || !hasExactKeys(identifiers, ['algorithm', 'value', 'values', 'truncated'])
227
+ || identifiers.algorithm !== 'set-sha256'
228
+ || !validNullableDigest(identifiers.value, SHA256_RE)
229
+ || !Array.isArray(identifiers.values)
230
+ || identifiers.values.length > 30
231
+ || !identifiers.values.every(validFingerprintIdentifier)
232
+ || new Set(identifiers.values.map((item) => JSON.stringify(item))).size !== identifiers.values.length
233
+ || typeof identifiers.truncated !== 'boolean')
234
+ return false;
235
+ if (resourceHosts.values.length === 0 !== (resourceHosts.value === null)
236
+ || identifiers.values.length === 0 !== (identifiers.value === null))
237
+ return false;
238
+ return true;
239
+ }
240
+ function validCredentialCountRecord(value, keys, maximum) {
241
+ return validExactUintRecord(value, keys, maximum);
242
+ }
243
+ function validDistinguishedName(value) {
244
+ return isJsonObject(value)
245
+ && hasExactKeys(value, TLS_NAME_KEYS)
246
+ && TLS_NAME_KEYS.every((key) => validExactStringArray(value[key], MAX_LOOKUP_TLS_NAME_VALUES, 256));
247
+ }
248
+ function validTlsAltNames(value) {
249
+ return isJsonObject(value)
250
+ && hasExactKeys(value, ['dnsNames', 'ipAddresses', 'classes', 'truncated'])
251
+ && validExactStringArray(value.dnsNames, MAX_LOOKUP_TLS_ALT_NAMES, 253)
252
+ && validExactStringArray(value.ipAddresses, MAX_LOOKUP_TLS_ALT_NAMES, 64)
253
+ && value.dnsNames.length + value.ipAddresses.length <= MAX_LOOKUP_TLS_ALT_NAMES
254
+ && validExactUintRecord(value.classes, TLS_SAN_CLASS_KEYS, 100)
255
+ && typeof value.truncated === 'boolean';
256
+ }
257
+ function validTlsPublicKey(value) {
258
+ if (!isJsonObject(value) || !hasExactKeys(value, ['type', 'bits', 'curve', 'fingerprintSha256']))
259
+ return false;
260
+ return validNullableBoundedString(value.type, 32)
261
+ && (value.bits === null || validUint(value.bits, 32_768) && Number(value.bits) > 0)
262
+ && validNullableBoundedString(value.curve, 64)
263
+ && validNullableDigest(value.fingerprintSha256, SHA256_RE);
264
+ }
265
+ function validTlsSignature(value) {
266
+ return isJsonObject(value)
267
+ && hasExactKeys(value, ['algorithm', 'oid'])
268
+ && validNullableBoundedString(value.algorithm, 128)
269
+ && (value.oid === null || typeof value.oid === 'string' && OID_RE.test(value.oid));
270
+ }
271
+ function validTlsPurposes(value) {
272
+ return isJsonObject(value)
273
+ && hasExactKeys(value, ['values', 'truncated'])
274
+ && Array.isArray(value.values)
275
+ && value.values.length <= 16
276
+ && value.values.every((item) => isJsonObject(item)
277
+ && hasExactKeys(item, ['oid', 'name'])
278
+ && typeof item.oid === 'string'
279
+ && OID_RE.test(item.oid)
280
+ && validBoundedString(item.name, 128))
281
+ && typeof value.truncated === 'boolean';
282
+ }
283
+ function validAiaSummary(value) {
284
+ return isJsonObject(value)
285
+ && hasExactKeys(value, ['total', 'http', 'https', 'other'])
286
+ && ['total', 'http', 'https', 'other'].every((key) => validUint(value[key], 32))
287
+ && value.total === Number(value.http) + Number(value.https) + Number(value.other);
288
+ }
289
+ function validTlsAuthorityInformationAccess(value) {
290
+ return isJsonObject(value)
291
+ && hasExactKeys(value, ['ocsp', 'caIssuers', 'unknownMethods', 'truncated'])
292
+ && validAiaSummary(value.ocsp)
293
+ && validAiaSummary(value.caIssuers)
294
+ && validUint(value.unknownMethods, 32)
295
+ && Number(value.ocsp.total)
296
+ + Number(value.caIssuers.total)
297
+ + Number(value.unknownMethods) <= 32
298
+ && typeof value.truncated === 'boolean';
299
+ }
300
+ function validTlsExtensionProfile(value) {
301
+ if (!isJsonObject(value)
302
+ || !hasExactKeys(value, ['certificatePolicies', 'crlDistributionPoints', 'parsed', 'partial'])
303
+ || !isJsonObject(value.certificatePolicies)
304
+ || !hasExactKeys(value.certificatePolicies, ['oids', 'truncated'])
305
+ || !validExactStringArray(value.certificatePolicies.oids, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, 128)
306
+ || !value.certificatePolicies.oids.every((oid) => typeof oid === 'string' && OID_RE.test(oid))
307
+ || typeof value.certificatePolicies.truncated !== 'boolean'
308
+ || !isJsonObject(value.crlDistributionPoints)
309
+ || !hasExactKeys(value.crlDistributionPoints, ['total', 'http', 'https', 'ldap', 'other', 'truncated'])
310
+ || !['total', 'http', 'https', 'ldap', 'other'].every((key) => validUint(value.crlDistributionPoints[key], 32))
311
+ || value.crlDistributionPoints.total !== ['http', 'https', 'ldap', 'other']
312
+ .reduce((sum, key) => sum + Number(value.crlDistributionPoints[key]), 0)
313
+ || typeof value.crlDistributionPoints.truncated !== 'boolean'
314
+ || typeof value.parsed !== 'boolean'
315
+ || typeof value.partial !== 'boolean')
316
+ return false;
317
+ return true;
318
+ }
319
+ function validTlsCertificate(value, leaf) {
320
+ if (!isJsonObject(value))
321
+ return false;
322
+ const baseKeys = [
323
+ 'subject', 'issuer', 'serialNumber', 'validFrom', 'validTo', 'fingerprintSha1',
324
+ 'fingerprintSha256', 'isCertificateAuthority',
325
+ ];
326
+ const leafKeys = [
327
+ 'subjectAltNames', 'publicKey', 'signature', 'extendedKeyUsage',
328
+ 'authorityInformationAccess', 'extensionProfile',
329
+ ];
330
+ if (!hasOnlyKeys(value, leaf ? [...baseKeys, ...leafKeys] : baseKeys)
331
+ || !baseKeys.every((key) => Object.hasOwn(value, key))
332
+ || !validDistinguishedName(value.subject)
333
+ || !validDistinguishedName(value.issuer)
334
+ || !(value.serialNumber === null || typeof value.serialNumber === 'string'
335
+ && /^[a-f0-9]{1,128}$/u.test(value.serialNumber))
336
+ || !validNullableIsoTimestamp(value.validFrom)
337
+ || !validNullableIsoTimestamp(value.validTo)
338
+ || !validNullableDigest(value.fingerprintSha1, SHA1_RE)
339
+ || !validNullableDigest(value.fingerprintSha256, SHA256_RE)
340
+ || !(value.isCertificateAuthority === null || typeof value.isCertificateAuthority === 'boolean'))
341
+ return false;
342
+ if (!leaf)
343
+ return hasExactKeys(value, baseKeys);
344
+ return (value.subjectAltNames === undefined || validTlsAltNames(value.subjectAltNames))
345
+ && (value.publicKey === undefined || validTlsPublicKey(value.publicKey))
346
+ && (value.signature === undefined || validTlsSignature(value.signature))
347
+ && (value.extendedKeyUsage === undefined || validTlsPurposes(value.extendedKeyUsage))
348
+ && (value.authorityInformationAccess === undefined
349
+ || validTlsAuthorityInformationAccess(value.authorityInformationAccess))
350
+ && (value.extensionProfile === undefined || validTlsExtensionProfile(value.extensionProfile));
351
+ }
352
+ function validTlsCipher(value) {
353
+ return isJsonObject(value)
354
+ && hasExactKeys(value, ['name', 'standardName', 'version'])
355
+ && validNullableBoundedString(value.name, 128)
356
+ && validNullableBoundedString(value.standardName, 128)
357
+ && validNullableBoundedString(value.version, 32)
358
+ && [value.name, value.standardName, value.version].some((item) => item !== null);
359
+ }
360
+ function validTlsEphemeralKey(value) {
361
+ return isJsonObject(value)
362
+ && hasExactKeys(value, ['type', 'name', 'size'])
363
+ && validNullableBoundedString(value.type, 32)
364
+ && validNullableBoundedString(value.name, 64)
365
+ && (value.size === null || validUint(value.size, 32_768) && Number(value.size) > 0)
366
+ && [value.type, value.name, value.size].some((item) => item !== null);
367
+ }
368
+ function validTlsFinding(value) {
369
+ return isJsonObject(value)
370
+ && hasExactKeys(value, ['id', 'tone', 'label', 'detail'])
371
+ && typeof value.id === 'string'
372
+ && /^[a-z0-9_]{1,80}$/u.test(value.id)
373
+ && typeof value.tone === 'string'
374
+ && ['warning', 'neutral'].includes(value.tone)
375
+ && validBoundedString(value.label, 160)
376
+ && validBoundedString(value.detail, 500);
377
+ }
378
+ function validTlsChainCertificate(value) {
379
+ return validTlsCertificate(value, false);
380
+ }
381
+ function validSecurityPostureFinding(value) {
382
+ return isJsonObject(value)
383
+ && hasExactKeys(value, ['id', 'category', 'state', 'tone', 'label', 'detail', 'evidence'])
384
+ && typeof value.id === 'string'
385
+ && /^[a-z0-9_]{1,80}$/u.test(value.id)
386
+ && typeof value.category === 'string'
387
+ && SECURITY_POSTURE_CATEGORIES.has(value.category)
388
+ && typeof value.state === 'string'
389
+ && ['observed', 'potential_exposure', 'observed_absence', 'unavailable'].includes(value.state)
390
+ && typeof value.tone === 'string'
391
+ && ['configured', 'review', 'neutral'].includes(value.tone)
392
+ && validBoundedString(value.label, 160)
393
+ && validBoundedString(value.detail, 300)
394
+ && validStringArray(value.evidence, 4, 120);
395
+ }
396
+ function technologyProfileContractState(value) {
397
+ const versionState = childVersionState(value, 'profileVersion', SUPPORTED_TECHNOLOGY_PROFILE_VERSIONS);
398
+ if (versionState !== 'supported')
399
+ return versionState;
400
+ const profile = value;
401
+ const profileVersion = Number(profile.profileVersion);
402
+ if (!hasOnlyKeys(profile, [...OBSERVATION_FIELDS, 'profileVersion', 'findings', 'browserLibraryProfile'])
403
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['derived']))
404
+ || !Array.isArray(profile.findings)
405
+ || profile.findings.length > MAX_TECHNOLOGY_FINDINGS
406
+ || !(profile.browserLibraryProfile === null || isJsonObject(profile.browserLibraryProfile)))
407
+ return 'invalid';
408
+ const findingIds = new Set();
409
+ for (const candidate of profile.findings) {
410
+ if (!isJsonObject(candidate)
411
+ || !hasOnlyKeys(candidate, profileVersion >= 11
412
+ ? ['id', 'name', 'category', 'confidence', 'roles', 'evidence']
413
+ : ['id', 'name', 'category', 'confidence', 'evidence'])
414
+ || typeof candidate.id !== 'string'
415
+ || !TECHNOLOGY_ID_RE.test(candidate.id)
416
+ || !validBoundedString(candidate.name, 120)
417
+ || typeof candidate.category !== 'string'
418
+ || !TECHNOLOGY_CATEGORIES.has(candidate.category)
419
+ || typeof candidate.confidence !== 'string'
420
+ || !TECHNOLOGY_CONFIDENCE.has(candidate.confidence)
421
+ || !Array.isArray(candidate.evidence)
422
+ || candidate.evidence.length < 1
423
+ || candidate.evidence.length > MAX_EVIDENCE_PER_TECHNOLOGY)
424
+ return 'invalid';
425
+ if (findingIds.has(candidate.id))
426
+ return 'invalid';
427
+ findingIds.add(candidate.id);
428
+ if (profileVersion >= 11 && !validExactStringArray(candidate.roles, 4, 40, TECHNOLOGY_EVIDENCE_ROLES))
429
+ return 'invalid';
430
+ for (const evidence of candidate.evidence) {
431
+ if (!isJsonObject(evidence)
432
+ || !hasOnlyKeys(evidence, profileVersion >= 11 ? ['source', 'role', 'description'] : ['source', 'description'])
433
+ || typeof evidence.source !== 'string'
434
+ || !TECHNOLOGY_EVIDENCE_SOURCES.has(evidence.source)
435
+ || !validBoundedString(evidence.description, MAX_TECHNOLOGY_EVIDENCE_DESCRIPTION_LENGTH)
436
+ || profileVersion >= 11 && (typeof evidence.role !== 'string'
437
+ || !TECHNOLOGY_EVIDENCE_ROLES.has(evidence.role)))
438
+ return 'invalid';
439
+ }
440
+ if (profileVersion >= 11) {
441
+ const observedRoles = new Set(candidate.evidence.map((evidence) => String(evidence.role)));
442
+ const expectedRoles = TECHNOLOGY_EVIDENCE_ROLE_ORDER.filter((role) => observedRoles.has(role));
443
+ if (JSON.stringify(candidate.roles) !== JSON.stringify(expectedRoles))
444
+ return 'invalid';
445
+ }
446
+ }
447
+ return 'supported';
448
+ }
449
+ function browserLibraryProfileContractState(value) {
450
+ const versionState = childVersionState(value, 'profileVersion', [BROWSER_LIBRARY_PROFILE_VERSION]);
451
+ if (versionState !== 'supported')
452
+ return versionState;
453
+ const profile = value;
454
+ const catalog = profile.catalog;
455
+ const knownExploitedCatalog = profile.knownExploitedCatalog;
456
+ if (!hasOnlyKeys(profile, [...OBSERVATION_FIELDS, 'profileVersion', 'catalog', 'knownExploitedCatalog', 'findings'])
457
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['derived']))
458
+ || !isJsonObject(catalog)
459
+ || !hasOnlyKeys(catalog, ['name', 'version', 'sourceRevision'])
460
+ || !['name', 'version', 'sourceRevision'].every((field) => validBoundedString(catalog[field], 160))
461
+ || !isJsonObject(knownExploitedCatalog)
462
+ || !hasOnlyKeys(knownExploitedCatalog, ['name', 'version', 'releasedAt'])
463
+ || !['name', 'version', 'releasedAt'].every((field) => validBoundedString(knownExploitedCatalog[field], 160))
464
+ || normalizeExplicitIsoTimestamp(knownExploitedCatalog.releasedAt) !== knownExploitedCatalog.releasedAt
465
+ || !Array.isArray(profile.findings)
466
+ || profile.findings.length > MAX_LIBRARY_FINDINGS)
467
+ return 'invalid';
468
+ const methods = new Set(['script URL', 'script filename', 'inline signature', 'inline hash']);
469
+ const severities = new Set(['none', 'low', 'medium', 'high', 'critical']);
470
+ const findingIds = new Set();
471
+ for (const candidate of profile.findings) {
472
+ if (!isJsonObject(candidate)
473
+ || !hasOnlyKeys(candidate, [
474
+ 'id', 'name', 'apparentVersion', 'detectionMethods', 'advisoryCount',
475
+ 'highestSeverity', 'advisoryIdentifiers', 'knownExploitedCount',
476
+ 'knownExploitedIdentifiers', 'weaknessClasses',
477
+ ])
478
+ || typeof candidate.id !== 'string'
479
+ || !/^[a-z0-9._-]{1,80}$/iu.test(candidate.id)
480
+ || !validBoundedString(candidate.name, 120)
481
+ || !/^[0-9][0-9.a-z_-]{0,63}$/iu.test(String(candidate.apparentVersion))
482
+ || !validExactStringArray(candidate.detectionMethods, 4, 40, methods)
483
+ || !validUint(candidate.advisoryCount, 10_000)
484
+ || !(candidate.highestSeverity === null
485
+ || typeof candidate.highestSeverity === 'string' && severities.has(candidate.highestSeverity))
486
+ || !validExactStringArray(candidate.advisoryIdentifiers, 16, 80)
487
+ || !validUint(candidate.knownExploitedCount, 10_000)
488
+ || !validExactStringArray(candidate.knownExploitedIdentifiers, 16, 80)
489
+ || !validExactStringArray(candidate.weaknessClasses, 12, 80))
490
+ return 'invalid';
491
+ if (findingIds.has(candidate.id)
492
+ || !candidate.advisoryIdentifiers.every((identifier) => typeof identifier === 'string'
493
+ && /^(?:CVE-[0-9X-]+|GHSA-[A-Z0-9-]+)$/u.test(identifier))
494
+ || !candidate.knownExploitedIdentifiers.every((identifier) => typeof identifier === 'string'
495
+ && /^CVE-[0-9X-]+$/u.test(identifier))
496
+ || !candidate.weaknessClasses.every((identifier) => typeof identifier === 'string'
497
+ && /^CWE-[0-9]+$/u.test(identifier))
498
+ || Number(candidate.advisoryCount) < candidate.advisoryIdentifiers.length
499
+ || Number(candidate.knownExploitedCount) < candidate.knownExploitedIdentifiers.length)
500
+ return 'invalid';
501
+ findingIds.add(candidate.id);
502
+ }
503
+ return 'supported';
504
+ }
505
+ function pageFingerprintContractState(value) {
506
+ const versionState = childVersionState(value, 'fingerprintVersion', [PAGE_FINGERPRINT_VERSION]);
507
+ if (versionState !== 'supported')
508
+ return versionState;
509
+ const profile = value;
510
+ if (!hasExactKeys(profile, [
511
+ 'fingerprintVersion', 'exact', 'normalizedHtml', 'visibleText', 'domStructure',
512
+ 'formStructure', 'resourceHosts', 'identifiers', 'complete', 'truncated', 'limitations',
513
+ ])
514
+ || typeof profile.complete !== 'boolean'
515
+ || typeof profile.truncated !== 'boolean'
516
+ || !validExactStringArray(profile.limitations, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH)
517
+ || !isJsonObject(profile.exact)
518
+ || !hasExactKeys(profile.exact, ['algorithm', 'value', 'scope', 'bytes', 'source'])
519
+ || profile.exact.algorithm !== 'sha256'
520
+ || typeof profile.exact.value !== 'string'
521
+ || !SHA256_RE.test(profile.exact.value)
522
+ || !['complete-body', 'captured-prefix'].includes(String(profile.exact.scope))
523
+ || !validUint(profile.exact.bytes, 300_000)
524
+ || !['captured-response-bytes', 'decoded-markup'].includes(String(profile.exact.source))
525
+ || !validPageFingerprintProfile(profile)
526
+ || profile.complete !== (profile.truncated !== true))
527
+ return 'invalid';
528
+ return 'supported';
529
+ }
530
+ function validIdentityUrl(value) {
531
+ return isJsonObject(value)
532
+ && hasOnlyKeys(value, ['url', 'queryOmitted', 'pathTruncated'])
533
+ && validHttpProvenanceUrl(value.url)
534
+ && typeof value.queryOmitted === 'boolean'
535
+ && typeof value.pathTruncated === 'boolean';
536
+ }
537
+ function validNullableIdentityUrl(value) {
538
+ return value === null || validIdentityUrl(value);
539
+ }
540
+ function pageIdentityContractState(value) {
541
+ const versionState = childVersionState(value, 'identityVersion', [PAGE_IDENTITY_VERSION]);
542
+ if (versionState !== 'supported')
543
+ return versionState;
544
+ const profile = value;
545
+ if (!hasOnlyKeys(profile, [
546
+ ...OBSERVATION_FIELDS, 'identityVersion', 'documentLanguage', 'canonical',
547
+ 'metaRefresh', 'openGraph', 'generator', 'forms', 'resources',
548
+ 'embeddedOrigins', 'contactDomains', 'downloads', 'trackingIdentifiers',
549
+ 'fingerprints', 'publicationMetadata',
550
+ ])
551
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['html']))
552
+ || !(profile.documentLanguage === null || validBoundedString(profile.documentLanguage, 80))
553
+ || !(profile.generator === null || validBoundedString(profile.generator, 120))
554
+ || !validNullableIdentityUrl(profile.canonical)
555
+ || !validNullableIdentityUrl(profile.metaRefresh)
556
+ || !isJsonObject(profile.openGraph)
557
+ || !hasOnlyKeys(profile.openGraph, ['title', 'siteName', 'url'])
558
+ || !validOptionalNullableText(profile.openGraph.title, 200)
559
+ || !validOptionalNullableText(profile.openGraph.siteName, 200)
560
+ || !validNullableIdentityUrl(profile.openGraph.url)
561
+ || !isJsonObject(profile.forms)
562
+ || !hasOnlyKeys(profile.forms, ['count', 'postCount', 'insecureActionCount', 'externalActionOrigins', 'truncated'])
563
+ || !validUint(profile.forms.count, 50)
564
+ || !validUint(profile.forms.postCount, 50)
565
+ || !validUint(profile.forms.insecureActionCount, 50)
566
+ || Number(profile.forms.postCount) > Number(profile.forms.count)
567
+ || Number(profile.forms.insecureActionCount) > Number(profile.forms.count)
568
+ || !validHttpOriginArray(profile.forms.externalActionOrigins, 10)
569
+ || typeof profile.forms.truncated !== 'boolean'
570
+ || !isJsonObject(profile.resources)
571
+ || !hasOnlyKeys(profile.resources, ['count', 'byType', 'externalOrigins', 'truncated'])
572
+ || !validUint(profile.resources.count, 1_024)
573
+ || !isJsonObject(profile.resources.byType)
574
+ || !hasOnlyKeys(profile.resources.byType, ['image', 'script', 'stylesheet', 'link', 'frame', 'media', 'object'])
575
+ || !Object.values(profile.resources.byType).every((count) => validUint(count, 1_024))
576
+ || Object.values(profile.resources.byType).reduce((sum, count) => sum + Number(count), 0) !== profile.resources.count
577
+ || !validHttpOriginArray(profile.resources.externalOrigins, 30)
578
+ || typeof profile.resources.truncated !== 'boolean'
579
+ || !isJsonObject(profile.downloads)
580
+ || !hasOnlyKeys(profile.downloads, ['count', 'explicitCount', 'riskyCount', 'externalOrigins', 'riskyFileTypes', 'truncated'])
581
+ || !validUint(profile.downloads.count, 1_024)
582
+ || !validUint(profile.downloads.explicitCount, 1_024)
583
+ || !validUint(profile.downloads.riskyCount, 1_024)
584
+ || Number(profile.downloads.explicitCount) > Number(profile.downloads.count)
585
+ || Number(profile.downloads.riskyCount) > Number(profile.downloads.count)
586
+ || !validHttpOriginArray(profile.downloads.externalOrigins, 20)
587
+ || !validExactStringArray(profile.downloads.riskyFileTypes, 20, 80)
588
+ || typeof profile.downloads.truncated !== 'boolean'
589
+ || !validHttpOriginArray(profile.embeddedOrigins, 20)
590
+ || !validExactStringArray(profile.contactDomains, 20, 253)
591
+ || !profile.contactDomains.every((domain) => typeof domain === 'string'
592
+ && normalizedDomain(domain) === domain)
593
+ || !Array.isArray(profile.trackingIdentifiers)
594
+ || profile.trackingIdentifiers.length > 30
595
+ || !profile.trackingIdentifiers.every(validFingerprintIdentifier)
596
+ || new Set(profile.trackingIdentifiers.map((item) => JSON.stringify(item))).size !== profile.trackingIdentifiers.length
597
+ || !isJsonObject(profile.fingerprints)
598
+ || profile.publicationMetadata !== undefined
599
+ && !validPagePublicationMetadata(profile.publicationMetadata))
600
+ return 'invalid';
601
+ return 'supported';
602
+ }
603
+ function tlsProfileContractState(value) {
604
+ const versionState = childVersionState(value, 'profileVersion', [TLS_PROFILE_VERSION]);
605
+ if (versionState !== 'supported')
606
+ return versionState;
607
+ const profile = value;
608
+ if (!hasExactKeys(profile, [
609
+ ...OBSERVATION_FIELDS, 'profileVersion', 'connectedAddress', 'connectedFamily',
610
+ 'port', 'sniHost', 'protocol', 'alpnProtocol', 'cipher', 'ephemeralKey',
611
+ 'authorization', 'hostname', 'validity', 'certificate', 'chain',
612
+ 'chainTruncated', 'findings',
613
+ ])
614
+ || !validProfileObservation(profile, new Set(['success', 'partial', 'error', 'skipped']), new Set(['tls']))
615
+ || !(profile.connectedAddress === null || typeof profile.connectedAddress === 'string'
616
+ && /^[0-9a-f:.]{2,64}$/iu.test(profile.connectedAddress))
617
+ || !(profile.connectedFamily === null || typeof profile.connectedFamily === 'number'
618
+ && [4, 6].includes(profile.connectedFamily))
619
+ || (profile.connectedAddress === null) !== (profile.connectedFamily === null)
620
+ || profile.port !== 443
621
+ || !(profile.sniHost === null || typeof profile.sniHost === 'string'
622
+ && normalizedDomain(profile.sniHost) === profile.sniHost)
623
+ || !(profile.protocol === null || validBoundedString(profile.protocol, 32))
624
+ || !(profile.alpnProtocol === null || validBoundedString(profile.alpnProtocol, 32))
625
+ || !(profile.cipher === null || validTlsCipher(profile.cipher))
626
+ || !(profile.ephemeralKey === null || validTlsEphemeralKey(profile.ephemeralKey))
627
+ || !isJsonObject(profile.authorization)
628
+ || !hasOnlyKeys(profile.authorization, ['authorized', 'error'])
629
+ || !(profile.authorization.authorized === null || typeof profile.authorization.authorized === 'boolean')
630
+ || !validOptionalNullableText(profile.authorization.error, 240)
631
+ || !isJsonObject(profile.hostname)
632
+ || !hasOnlyKeys(profile.hostname, ['matches', 'error'])
633
+ || !(profile.hostname.matches === null || typeof profile.hostname.matches === 'boolean')
634
+ || !validOptionalNullableText(profile.hostname.error, 240)
635
+ || !isJsonObject(profile.validity)
636
+ || !hasOnlyKeys(profile.validity, ['status'])
637
+ || !['valid', 'expired', 'not_yet_valid', 'unknown'].includes(String(profile.validity.status))
638
+ || !(profile.certificate === null || validTlsCertificate(profile.certificate, true))
639
+ || !Array.isArray(profile.chain)
640
+ || profile.chain.length > MAX_LOOKUP_TLS_CHAIN_CERTIFICATES
641
+ || !profile.chain.every((certificate) => validTlsCertificate(certificate, false))
642
+ || typeof profile.chainTruncated !== 'boolean'
643
+ || !Array.isArray(profile.findings)
644
+ || profile.findings.length > MAX_LOOKUP_TLS_FINDINGS
645
+ || !profile.findings.every(validTlsFinding))
646
+ return 'invalid';
647
+ return 'supported';
648
+ }
649
+ function securityPostureContractState(value) {
650
+ const versionState = childVersionState(value, 'postureVersion', [WEBSITE_SECURITY_POSTURE_VERSION]);
651
+ if (versionState !== 'supported')
652
+ return versionState;
653
+ const profile = value;
654
+ if (!hasOnlyKeys(profile, [...OBSERVATION_FIELDS, 'postureVersion', 'summary', 'findings'])
655
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['derived']))
656
+ || !isJsonObject(profile.summary)
657
+ || !hasOnlyKeys(profile.summary, ['observed', 'potentialExposure', 'observedAbsence', 'unavailable'])
658
+ || !Object.values(profile.summary).every((count) => validUint(count, MAX_SECURITY_POSTURE_FINDINGS))
659
+ || !Array.isArray(profile.findings)
660
+ || profile.findings.length > MAX_SECURITY_POSTURE_FINDINGS
661
+ || !profile.findings.every(validSecurityPostureFinding)
662
+ || new Set(profile.findings.map((finding) => finding.id)).size !== profile.findings.length
663
+ || ['observed', 'potentialExposure', 'observedAbsence', 'unavailable']
664
+ .reduce((sum, key) => sum + Number(profile.summary[key]), 0) !== profile.findings.length
665
+ || Number(profile.summary.observed) !== profile.findings
666
+ .filter((finding) => finding.state === 'observed').length
667
+ || Number(profile.summary.potentialExposure) !== profile.findings
668
+ .filter((finding) => finding.state === 'potential_exposure').length
669
+ || Number(profile.summary.observedAbsence) !== profile.findings
670
+ .filter((finding) => finding.state === 'observed_absence').length
671
+ || Number(profile.summary.unavailable) !== profile.findings
672
+ .filter((finding) => finding.state === 'unavailable').length)
673
+ return 'invalid';
674
+ return 'supported';
675
+ }
676
+ function credentialSurfaceContractState(value) {
677
+ const versionState = childVersionState(value, 'credentialSurfaceVersion', [CREDENTIAL_SURFACE_PROFILE_VERSION]);
678
+ if (versionState !== 'supported')
679
+ return versionState;
680
+ const profile = value;
681
+ const forms = profile.forms;
682
+ const inputs = profile.inputs;
683
+ if (!hasExactKeys(profile, [...OBSERVATION_FIELDS, 'credentialSurfaceVersion', 'forms', 'inputs'])
684
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['html']))
685
+ || !isJsonObject(forms)
686
+ || !hasExactKeys(forms, ['count', 'methods', 'actions'])
687
+ || !validUint(forms.count, 50)
688
+ || !validCredentialCountRecord(forms.methods, CREDENTIAL_METHOD_KEYS, 50)
689
+ || !validCredentialCountRecord(forms.actions, CREDENTIAL_ACTION_KEYS, 50)
690
+ || CREDENTIAL_METHOD_KEYS.reduce((sum, key) => sum + Number(forms.methods[key]), 0) !== forms.count
691
+ || CREDENTIAL_ACTION_KEYS.reduce((sum, key) => sum + Number(forms.actions[key]), 0) !== forms.count
692
+ || !isJsonObject(inputs)
693
+ || !hasExactKeys(inputs, ['count', 'classifiedCount', 'categories'])
694
+ || !validUint(inputs.count, 500)
695
+ || !validUint(inputs.classifiedCount, 500)
696
+ || Number(inputs.classifiedCount) > Number(inputs.count)
697
+ || !validCredentialCountRecord(inputs.categories, CREDENTIAL_CATEGORY_KEYS, 500))
698
+ return 'invalid';
699
+ return 'supported';
700
+ }
701
+ function structuredDataContractState(value) {
702
+ const versionState = childVersionState(value, 'structuredDataVersion', [STRUCTURED_DATA_IDENTITY_VERSION]);
703
+ if (versionState !== 'supported')
704
+ return versionState;
705
+ const profile = value;
706
+ if (!hasOnlyKeys(profile, [...OBSERVATION_FIELDS, 'structuredDataVersion', 'entities'])
707
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['html']))
708
+ || !Array.isArray(profile.entities)
709
+ || profile.entities.length > MAX_STRUCTURED_DATA_ENTITIES)
710
+ return 'invalid';
711
+ return profile.entities.every((entity) => isJsonObject(entity)
712
+ && hasOnlyKeys(entity, ['types', 'name', 'declaredOrigin', 'sameAsHosts'])
713
+ && validExactStringArray(entity.types, 8, 160, STRUCTURED_DATA_TYPES)
714
+ && (entity.name === null || validBoundedString(entity.name, 160))
715
+ && (entity.declaredOrigin === null || validHttpOrigin(entity.declaredOrigin))
716
+ && validExactStringArray(entity.sameAsHosts, MAX_STRUCTURED_DATA_SAME_AS_HOSTS, 253)
717
+ && entity.sameAsHosts.every((host) => typeof host === 'string'
718
+ && normalizedDomain(host) === host))
719
+ ? 'supported'
720
+ : 'invalid';
721
+ }
722
+ function pageRoleContractState(value) {
723
+ const versionState = childVersionState(value, 'pageRoleProfileVersion', [PAGE_ROLE_PROFILE_VERSION]);
724
+ if (versionState !== 'supported')
725
+ return versionState;
726
+ const profile = value;
727
+ if (!hasOnlyKeys(profile, [...OBSERVATION_FIELDS, 'pageRoleProfileVersion', 'primaryRole', 'findings'])
728
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['derived']))
729
+ || typeof profile.primaryRole !== 'string'
730
+ || !PAGE_ROLE_VALUES.has(profile.primaryRole)
731
+ || !Array.isArray(profile.findings)
732
+ || profile.findings.length < 1
733
+ || profile.findings.length > MAX_PAGE_ROLE_FINDINGS)
734
+ return 'invalid';
735
+ if (!profile.findings.every((finding) => isJsonObject(finding)
736
+ && hasOnlyKeys(finding, ['role', 'label', 'confidence', 'evidence'])
737
+ && typeof finding.role === 'string'
738
+ && PAGE_ROLE_VALUES.has(finding.role)
739
+ && validBoundedString(finding.label, 120)
740
+ && ['high', 'medium', 'low'].includes(String(finding.confidence))
741
+ && validExactStringArray(finding.evidence, MAX_PAGE_ROLE_EVIDENCE, 300)))
742
+ return 'invalid';
743
+ if (profile.findings[0].role !== profile.primaryRole
744
+ || new Set(profile.findings.map((finding) => finding.role)).size !== profile.findings.length) {
745
+ return 'invalid';
746
+ }
747
+ return 'supported';
748
+ }
749
+ function clientBehaviorContractState(value) {
750
+ const versionState = childVersionState(value, 'clientBehaviorProfileVersion', [CLIENT_BEHAVIOR_PROFILE_VERSION]);
751
+ if (versionState !== 'supported')
752
+ return versionState;
753
+ const profile = value;
754
+ if (!hasOnlyKeys(profile, [...OBSERVATION_FIELDS, 'clientBehaviorProfileVersion', 'scriptSummary', 'indicators'])
755
+ || !validProfileObservation(profile, new Set(['success', 'partial']), new Set(['derived']))
756
+ || !isJsonObject(profile.scriptSummary)
757
+ || !hasOnlyKeys(profile.scriptSummary, ['elementsObserved', 'referencedScripts', 'inlineScripts', 'moduleScripts'])
758
+ || !Object.values(profile.scriptSummary).every((count) => validUint(count, 10_000))
759
+ || !Array.isArray(profile.indicators)
760
+ || profile.indicators.length > MAX_CLIENT_BEHAVIOR_INDICATORS)
761
+ return 'invalid';
762
+ if (!profile.indicators.every((indicator) => isJsonObject(indicator)
763
+ && hasOnlyKeys(indicator, ['id', 'label', 'evidenceClass', 'occurrences', 'explanation'])
764
+ && typeof indicator.id === 'string'
765
+ && CLIENT_BEHAVIOR_IDS.has(indicator.id)
766
+ && validBoundedString(indicator.label, 120)
767
+ && ['static_markup', 'inline_script'].includes(String(indicator.evidenceClass))
768
+ && validUint(indicator.occurrences, 999)
769
+ && Number(indicator.occurrences) > 0
770
+ && validBoundedString(indicator.explanation, 300)))
771
+ return 'invalid';
772
+ const summary = profile.scriptSummary;
773
+ if (Number(summary.referencedScripts) + Number(summary.inlineScripts) !== summary.elementsObserved
774
+ || Number(summary.moduleScripts) > Number(summary.elementsObserved)
775
+ || new Set(profile.indicators.map((indicator) => indicator.id)).size !== profile.indicators.length) {
776
+ return 'invalid';
777
+ }
778
+ return 'supported';
779
+ }
780
+ function profilePlaceholder(label, source, state) {
781
+ return {
782
+ status: state === 'unsupported' ? 'unsupported' : 'error',
783
+ source,
784
+ complete: false,
785
+ truncated: false,
786
+ compatibility: state === 'unsupported' ? 'unsupported_version' : 'malformed',
787
+ limitations: [state === 'unsupported'
788
+ ? `${label} uses a newer unsupported version; its evidence was withheld.`
789
+ : `${label} was malformed; its evidence was withheld.`],
790
+ };
791
+ }
792
+ function sanitizeLookupChildProfiles(value) {
793
+ let output = value;
794
+ let availability = value.availability;
795
+ const mutableAvailability = () => {
796
+ if (output === value) {
797
+ availability = { ...value.availability };
798
+ output = { ...value, availability };
799
+ }
800
+ return availability;
801
+ };
802
+ const replaceChild = (key, label, source, contract) => {
803
+ const child = availability[key];
804
+ if (child === undefined || child === null)
805
+ return;
806
+ const state = contract(child);
807
+ if (state !== 'supported')
808
+ mutableAvailability()[key] = profilePlaceholder(label, source, state);
809
+ };
810
+ replaceChild('tls', 'TLS profile', 'tls', tlsProfileContractState);
811
+ replaceChild('securityPosture', 'Security-posture profile', 'derived', securityPostureContractState);
812
+ replaceChild('credentialSurfaceProfile', 'Credential-surface profile', 'html', credentialSurfaceContractState);
813
+ replaceChild('structuredDataIdentity', 'Structured-data identity profile', 'html', structuredDataContractState);
814
+ replaceChild('pageRoleProfile', 'Page-role profile', 'derived', pageRoleContractState);
815
+ replaceChild('clientBehaviorProfile', 'Client-behaviour profile', 'derived', clientBehaviorContractState);
816
+ const page = availability.pageIdentity;
817
+ if (page !== undefined && page !== null) {
818
+ const pageState = pageIdentityContractState(page);
819
+ if (pageState !== 'supported') {
820
+ mutableAvailability().pageIdentity = profilePlaceholder('Page-identity profile', 'html', pageState);
821
+ }
822
+ else if (isJsonObject(page) && page.fingerprints !== undefined && page.fingerprints !== null) {
823
+ const fingerprintState = pageFingerprintContractState(page.fingerprints);
824
+ if (fingerprintState !== 'supported') {
825
+ const replacedPage = { ...page, fingerprints: profilePlaceholder('Page-fingerprint profile', 'derived', fingerprintState) };
826
+ mutableAvailability().pageIdentity = replacedPage;
827
+ }
828
+ }
829
+ }
830
+ const technology = availability.technologyProfile;
831
+ if (technology !== undefined && technology !== null) {
832
+ const technologyState = technologyProfileContractState(technology);
833
+ if (technologyState !== 'supported') {
834
+ mutableAvailability().technologyProfile = {
835
+ ...profilePlaceholder('Technology profile', 'derived', technologyState),
836
+ findings: [],
837
+ browserLibraryProfile: null,
838
+ };
839
+ }
840
+ else if (isJsonObject(technology)
841
+ && technology.browserLibraryProfile !== undefined
842
+ && technology.browserLibraryProfile !== null) {
843
+ const libraryState = browserLibraryProfileContractState(technology.browserLibraryProfile);
844
+ if (libraryState !== 'supported') {
845
+ const replacedTechnology = {
846
+ ...technology,
847
+ browserLibraryProfile: {
848
+ ...profilePlaceholder('Browser-library profile', 'derived', libraryState),
849
+ findings: [],
850
+ },
851
+ };
852
+ mutableAvailability().technologyProfile = replacedTechnology;
853
+ }
854
+ }
855
+ }
856
+ return output;
857
+ }
858
+ export { sanitizeLookupChildProfiles, validSecurityPostureFinding, validTlsChainCertificate, validTlsFinding, };