@slicedearth/whoisleuth-cli 1.32.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 (189) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
  3. package/NOTICE +76 -0
  4. package/README.md +84 -0
  5. package/TRADEMARKS.md +41 -0
  6. package/bin/whoisleuth.mjs +35 -0
  7. package/cli/archive-inspect.mjs +198 -0
  8. package/cli/arguments.mjs +1022 -0
  9. package/cli/artifact-verify.mjs +210 -0
  10. package/cli/bulk-checkpoint.mjs +218 -0
  11. package/cli/bulk-command-runner.mjs +109 -0
  12. package/cli/bulk-output.mjs +112 -0
  13. package/cli/bulk.mjs +133 -0
  14. package/cli/compare.mjs +238 -0
  15. package/cli/completion.mjs +175 -0
  16. package/cli/csv.mjs +1 -0
  17. package/cli/discover.mjs +50 -0
  18. package/cli/discovery-command-runner.mjs +36 -0
  19. package/cli/discovery-observation-snapshot.mjs +315 -0
  20. package/cli/discovery-scan-command-runner.mjs +141 -0
  21. package/cli/discovery-scan.mjs +325 -0
  22. package/cli/discovery-snapshot.mjs +133 -0
  23. package/cli/discovery-workflow.mjs +73 -0
  24. package/cli/doctor.mjs +137 -0
  25. package/cli/errors.mjs +21 -0
  26. package/cli/evidence-command-arguments.mjs +145 -0
  27. package/cli/evidence-command-runner.mjs +106 -0
  28. package/cli/evidence-signing.mjs +215 -0
  29. package/cli/exit-codes.mjs +9 -0
  30. package/cli/export-evidence.mjs +28 -0
  31. package/cli/formatters/evidence-report.mjs +3 -0
  32. package/cli/formatters/html.mjs +69 -0
  33. package/cli/formatters/json.mjs +135 -0
  34. package/cli/formatters/markdown.mjs +3 -0
  35. package/cli/formatters/terminal.mjs +658 -0
  36. package/cli/http.mjs +27 -0
  37. package/cli/lookup-command-runner.mjs +87 -0
  38. package/cli/lookup-diff.mjs +171 -0
  39. package/cli/mail-review.mjs +204 -0
  40. package/cli/manual.mjs +34 -0
  41. package/cli/network-command-runner.mjs +88 -0
  42. package/cli/output-file.mjs +97 -0
  43. package/cli/page-compare.mjs +123 -0
  44. package/cli/posture.mjs +20 -0
  45. package/cli/progress-events.mjs +53 -0
  46. package/cli/progress.mjs +56 -0
  47. package/cli/registry-access.mjs +3 -0
  48. package/cli/registry-support.mjs +120 -0
  49. package/cli/risk-calibration.mjs +294 -0
  50. package/cli/runner-types.mjs +1 -0
  51. package/cli/runner.mjs +726 -0
  52. package/cli/saved-lookup.mjs +87 -0
  53. package/cli/source-reliability.mjs +727 -0
  54. package/cli/strict-exit.mjs +55 -0
  55. package/cli/terminal-presentation.mjs +147 -0
  56. package/docs/cli.md +827 -0
  57. package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
  58. package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
  59. package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
  60. package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
  61. package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
  62. package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
  63. package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
  64. package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
  65. package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
  66. package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
  67. package/frontend/src/lib/analysis/bulk-sort.js +49 -0
  68. package/frontend/src/lib/analysis/campaign-model.js +265 -0
  69. package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
  70. package/frontend/src/lib/analysis/case-migration-model.js +248 -0
  71. package/frontend/src/lib/analysis/case-model.js +5 -0
  72. package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
  73. package/frontend/src/lib/analysis/case-record-core.js +232 -0
  74. package/frontend/src/lib/analysis/case-record-model.js +7 -0
  75. package/frontend/src/lib/analysis/case-record-operations.js +224 -0
  76. package/frontend/src/lib/analysis/case-response-model.js +640 -0
  77. package/frontend/src/lib/analysis/case-response-packet.js +573 -0
  78. package/frontend/src/lib/analysis/case-storage-model.js +93 -0
  79. package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
  80. package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
  81. package/frontend/src/lib/analysis/http-summary.js +165 -0
  82. package/frontend/src/lib/analysis/investigation-guide.js +500 -0
  83. package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
  84. package/frontend/src/lib/analysis/page-baseline.js +272 -0
  85. package/frontend/src/lib/analysis/page-similarity.js +179 -0
  86. package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
  87. package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
  88. package/frontend/src/lib/analysis/scoring.js +135 -0
  89. package/frontend/src/lib/analysis/shortlist-model.js +209 -0
  90. package/frontend/src/lib/analysis/utils.js +265 -0
  91. package/frontend/src/lib/analysis/watchlist-history.js +643 -0
  92. package/frontend/src/lib/analysis/watchlist-store.js +127 -0
  93. package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
  94. package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
  95. package/frontend/src/lib/analysis/workspace-archive.js +472 -0
  96. package/lib/abort.mjs +26 -0
  97. package/lib/abusech-auth.mjs +19 -0
  98. package/lib/availability.mjs +716 -0
  99. package/lib/bounded-contract-normalizers.mjs +61 -0
  100. package/lib/browser-library-profile.mjs +310 -0
  101. package/lib/bulk-comparison-evidence.mjs +83 -0
  102. package/lib/classify.mjs +93 -0
  103. package/lib/client-behavior-profile.mjs +83 -0
  104. package/lib/credential-surface-profile.mjs +66 -0
  105. package/lib/csv.mjs +23 -0
  106. package/lib/ct-query.mjs +27 -0
  107. package/lib/ct-search.mjs +393 -0
  108. package/lib/distributed-operation-budget.mjs +461 -0
  109. package/lib/dns-delegation-health.mjs +344 -0
  110. package/lib/dns-intelligence.mjs +739 -0
  111. package/lib/dns-mx.mjs +24 -0
  112. package/lib/dns-resolver-selection.mjs +33 -0
  113. package/lib/domain-posture-analysis.mjs +254 -0
  114. package/lib/domain-posture-parsers.mjs +358 -0
  115. package/lib/domain-posture.mjs +745 -0
  116. package/lib/error-detail.mjs +10 -0
  117. package/lib/evidence-export.mjs +431 -0
  118. package/lib/evidence-report-markdown.mjs +71 -0
  119. package/lib/evidence-report.mjs +391 -0
  120. package/lib/external-intelligence-risk.mjs +108 -0
  121. package/lib/favicon.mjs +146 -0
  122. package/lib/feature-policy.mjs +70 -0
  123. package/lib/generated/retire-browser-catalog.mjs +8724 -0
  124. package/lib/generated/unicode-confusables-17.mjs +93 -0
  125. package/lib/hostname.mjs +38 -0
  126. package/lib/html-signals.mjs +680 -0
  127. package/lib/http-intelligence.mjs +232 -0
  128. package/lib/idn-confusable-policy.mjs +86 -0
  129. package/lib/idn-confusables.mjs +465 -0
  130. package/lib/lookup-cache.mjs +130 -0
  131. package/lib/lookup-progress.mjs +291 -0
  132. package/lib/lookup-source-progress.mjs +109 -0
  133. package/lib/lookup-threat-provider-inventory.mjs +13 -0
  134. package/lib/lookup.mjs +592 -0
  135. package/lib/observation.mjs +114 -0
  136. package/lib/observed-network-context.mjs +355 -0
  137. package/lib/operation-budget.mjs +489 -0
  138. package/lib/page-fingerprints.mjs +481 -0
  139. package/lib/page-role-profile.mjs +117 -0
  140. package/lib/perceptual-hash.mjs +451 -0
  141. package/lib/rdap-attempts.mjs +34 -0
  142. package/lib/rdap-bootstrap.mjs +173 -0
  143. package/lib/rdap-capabilities.mjs +292 -0
  144. package/lib/rdap-client.mjs +125 -0
  145. package/lib/rdap-normalization.mjs +691 -0
  146. package/lib/rdap-registrar.mjs +196 -0
  147. package/lib/rdap-transport.mjs +39 -0
  148. package/lib/rdap-types.mjs +1 -0
  149. package/lib/rdap-validation.mjs +94 -0
  150. package/lib/rdap.mjs +16 -0
  151. package/lib/registry-access.mjs +32 -0
  152. package/lib/registry-capabilities.mjs +117 -0
  153. package/lib/registry-capability-catalogue.mjs +1206 -0
  154. package/lib/registry-comparison.mjs +318 -0
  155. package/lib/registry-dates.mjs +190 -0
  156. package/lib/registry-insights.mjs +325 -0
  157. package/lib/response-policy.mjs +256 -0
  158. package/lib/risk-scoring.mjs +164 -0
  159. package/lib/safe-fetch.mjs +446 -0
  160. package/lib/security-txt.mjs +359 -0
  161. package/lib/service-binding-dns.mjs +608 -0
  162. package/lib/sslbl-certificates.generated.mjs +143 -0
  163. package/lib/sslbl-intelligence.mjs +241 -0
  164. package/lib/static-html-analysis.mjs +340 -0
  165. package/lib/structured-data-identity.mjs +323 -0
  166. package/lib/threat-intelligence-contract.mjs +5 -0
  167. package/lib/threat-intelligence-definition-registry.mjs +34 -0
  168. package/lib/threat-intelligence-fixtures.mjs +82 -0
  169. package/lib/threat-intelligence-manifests.mjs +512 -0
  170. package/lib/threat-intelligence-runtime.mjs +662 -0
  171. package/lib/threat-intelligence-targets.mjs +168 -0
  172. package/lib/threat-intelligence-types.mjs +3 -0
  173. package/lib/threatfox-intelligence.mjs +307 -0
  174. package/lib/tls-intelligence.mjs +814 -0
  175. package/lib/typosquat-generator.mjs +884 -0
  176. package/lib/urlhaus-intelligence.mjs +361 -0
  177. package/lib/urlscan-intelligence.mjs +349 -0
  178. package/lib/website-security-posture.mjs +325 -0
  179. package/lib/website-technology.mjs +437 -0
  180. package/lib/whois-authority.mjs +98 -0
  181. package/lib/whois-chain.mjs +261 -0
  182. package/lib/whois-contacts.mjs +163 -0
  183. package/lib/whois-contracts.mjs +3 -0
  184. package/lib/whois-normalization.mjs +27 -0
  185. package/lib/whois-transport.mjs +100 -0
  186. package/lib/whois-values.mjs +92 -0
  187. package/lib/whois.mjs +1187 -0
  188. package/package.json +55 -0
  189. package/third-party-notices.txt +5571 -0
@@ -0,0 +1,325 @@
1
+ // Versioned, bounded passive security-posture findings derived only from the
2
+ // HTTP, static page, TLS, and DNS evidence already collected by a deep Lookup.
3
+ // Findings use fixed explanatory text and bounded counts; upstream header
4
+ // values, certificate errors, URLs, and DNS record contents are not copied.
5
+ import { createObservation } from "./observation.mjs";
6
+ const WEBSITE_SECURITY_POSTURE_VERSION = 2;
7
+ const MAX_SECURITY_POSTURE_FINDINGS = 32;
8
+ const MAX_RETAINED_ORIGINS = 30;
9
+ function record(value) {
10
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
11
+ }
12
+ function statusAvailable(value, source) {
13
+ const item = record(value);
14
+ return item.source === source && typeof item.status === 'string' && ['success', 'partial'].includes(item.status);
15
+ }
16
+ function present(value) {
17
+ return typeof value === 'string' && value.length <= 2048 && value.trim().length > 0;
18
+ }
19
+ function boundedCount(value, maximum) {
20
+ const number = Number(value);
21
+ return Number.isSafeInteger(number) && number >= 0 ? Math.min(number, maximum) : 0;
22
+ }
23
+ function finding(id, category, state, tone, label, detail, evidence) {
24
+ return { id, category, state, tone, label, detail, evidence };
25
+ }
26
+ function httpFindings(httpValue) {
27
+ const http = record(httpValue);
28
+ if (!statusAvailable(http, 'http') || !http.response || typeof http.response !== 'object') {
29
+ return [finding('http_response_unavailable', 'transport', 'unavailable', 'neutral', 'HTTP posture unavailable', 'No selected HTTP response was available to evaluate transport or response-header posture.', ['HTTP collection'])];
30
+ }
31
+ const response = record(http.response);
32
+ const headers = record(response.securityHeaders);
33
+ const transport = typeof http.transportSecurity === 'string' && http.transportSecurity.length <= 16
34
+ ? http.transportSecurity.toLowerCase()
35
+ : '';
36
+ const findings = [];
37
+ if (transport === 'https') {
38
+ findings.push(finding('https_transport', 'transport', 'observed', 'configured', 'HTTPS transport observed', 'The selected homepage response was reached over HTTPS.', ['HTTP response']));
39
+ }
40
+ else if (transport === 'http') {
41
+ findings.push(finding('cleartext_transport', 'transport', 'potential_exposure', 'review', 'Cleartext HTTP transport', 'The selected homepage response used cleartext HTTP, so traffic did not receive HTTPS transport protection.', ['HTTP response']));
42
+ }
43
+ else {
44
+ findings.push(finding('http_transport_unavailable', 'transport', 'unavailable', 'neutral', 'HTTP transport unavailable', 'The selected response did not provide a usable HTTP or HTTPS transport observation.', ['HTTP response']));
45
+ }
46
+ if (http.httpsDowngrade === true) {
47
+ findings.push(finding('https_downgrade', 'transport', 'potential_exposure', 'review', 'HTTPS downgrade observed', 'The retained redirect chain moved from HTTPS to cleartext HTTP.', ['HTTP redirect chain']));
48
+ }
49
+ if (!Object.prototype.hasOwnProperty.call(response, 'securityHeaders')
50
+ || !response.securityHeaders
51
+ || typeof response.securityHeaders !== 'object'
52
+ || Array.isArray(response.securityHeaders)) {
53
+ findings.push(finding('http_headers_unavailable', 'response headers', 'unavailable', 'neutral', 'Response-header posture unavailable', 'The selected HTTP observation did not include the normalized security-header set.', ['Selected HTTP response headers']));
54
+ return findings;
55
+ }
56
+ const headerChecks = [
57
+ ['content_security_policy', headers.contentSecurityPolicy, 'Content Security Policy', 'Content-Security-Policy'],
58
+ ['content_type_protection', headers.xContentTypeOptions, 'Content-type protection', 'X-Content-Type-Options'],
59
+ ['referrer_policy', headers.referrerPolicy, 'Referrer policy', 'Referrer-Policy'],
60
+ ['frame_options', headers.xFrameOptions, 'Frame options', 'X-Frame-Options'],
61
+ ];
62
+ if (transport === 'https') {
63
+ headerChecks.unshift(['strict_transport_security', headers.strictTransportSecurity, 'Strict Transport Security', 'Strict-Transport-Security']);
64
+ }
65
+ for (const [id, value, label, header] of headerChecks) {
66
+ findings.push(present(value)
67
+ ? finding(`${id}_observed`, 'response headers', 'observed', 'configured', `${label} observed`, `The selected response included the ${header} header. Supported policy-quality checks are reported separately.`, ['Selected HTTP response headers'])
68
+ : finding(`${id}_absent`, 'response headers', 'observed_absence', 'review', `${label} not observed`, `The selected response did not include the ${header} header. This is a response-scoped observation, not a site-wide vulnerability finding.`, ['Selected HTTP response headers']));
69
+ }
70
+ return findings;
71
+ }
72
+ const RESPONSE_POLICY_FINDINGS = Object.freeze({
73
+ csp_default_source_missing: {
74
+ label: 'CSP default source not declared',
75
+ detail: () => 'The selected Content Security Policy did not declare default-src. A policy can enumerate fetch directives individually, so this is a bounded review signal rather than proof of exposure.',
76
+ },
77
+ csp_base_uri_missing: {
78
+ label: 'CSP base URI control not declared',
79
+ detail: () => 'The selected Content Security Policy did not declare base-uri, which has no default-src fallback.',
80
+ },
81
+ csp_object_source_unbounded: {
82
+ label: 'CSP object source not bounded',
83
+ detail: () => 'The selected Content Security Policy declared neither object-src nor a default-src fallback.',
84
+ },
85
+ csp_permissive_script_source: {
86
+ label: 'Permissive CSP script source observed',
87
+ detail: () => 'The effective selected script source included a wildcard or broad scheme source. This fixed observation does not establish that an unsafe script is reachable.',
88
+ },
89
+ csp_unsafe_eval: {
90
+ label: 'CSP permits unsafe evaluation',
91
+ detail: () => "The effective selected script source included 'unsafe-eval'.",
92
+ },
93
+ csp_unsafe_inline: {
94
+ label: 'CSP permits unqualified inline script',
95
+ detail: () => "The effective selected script source included 'unsafe-inline' without an observed nonce or hash source in that directive.",
96
+ },
97
+ hsts_disabled: {
98
+ label: 'HSTS disabled by selected response',
99
+ detail: () => 'The selected Strict-Transport-Security policy declared max-age=0.',
100
+ },
101
+ hsts_short_max_age: {
102
+ label: 'Short HSTS duration observed',
103
+ detail: () => 'The selected Strict-Transport-Security max-age was greater than zero but shorter than 180 days.',
104
+ },
105
+ referrer_policy_permissive: {
106
+ label: 'Permissive referrer policy observed',
107
+ detail: () => 'The effective selected Referrer-Policy was unsafe-url or no-referrer-when-downgrade.',
108
+ },
109
+ cookies_missing_secure: {
110
+ label: 'Response cookies without Secure',
111
+ detail: (count) => `${count} selected response cookie${count === 1 ? '' : 's'} did not declare Secure. Cookie purpose and application behavior were not inspected.`,
112
+ },
113
+ cookies_missing_http_only: {
114
+ label: 'Response cookies without HttpOnly',
115
+ detail: (count) => `${count} selected response cookie${count === 1 ? '' : 's'} did not declare HttpOnly. Some client-readable cookies are intentional, so this is a review signal only.`,
116
+ },
117
+ cookies_missing_same_site: {
118
+ label: 'Response cookies without SameSite',
119
+ detail: (count) => `${count} selected response cookie${count === 1 ? '' : 's'} did not declare SameSite.`,
120
+ },
121
+ cookies_same_site_none_without_secure: {
122
+ label: 'SameSite=None cookies without Secure',
123
+ detail: (count) => `${count} selected response cookie${count === 1 ? '' : 's'} declared SameSite=None without Secure.`,
124
+ },
125
+ });
126
+ function responsePolicyFindings(value) {
127
+ const policy = record(value);
128
+ if (policy.responsePolicyVersion !== 1)
129
+ return [];
130
+ const findings = [];
131
+ const components = record(policy.components);
132
+ const componentLabels = [
133
+ ['contentSecurityPolicy', 'Content Security Policy quality unavailable'],
134
+ ['strictTransportSecurity', 'Strict Transport Security quality unavailable'],
135
+ ['referrerPolicy', 'Referrer policy quality unavailable'],
136
+ ['responseCookies', 'Response-cookie attribute review incomplete'],
137
+ ];
138
+ for (const [key, label] of componentLabels) {
139
+ if (!['partial', 'malformed'].includes(String(components[key] || '')))
140
+ continue;
141
+ findings.push(finding(`response_policy_${key}_unavailable`, 'response headers', 'unavailable', 'neutral', label, 'The selected value was present but could not be interpreted completely within the configured parser and byte limits.', ['Transient selected response-policy analysis']));
142
+ }
143
+ for (const rawSignal of (Array.isArray(policy.signals) ? policy.signals : []).slice(0, 16)) {
144
+ const signal = record(rawSignal);
145
+ const id = typeof signal.id === 'string' ? signal.id : '';
146
+ const definition = RESPONSE_POLICY_FINDINGS[id];
147
+ if (!definition || findings.some((item) => item.id === id))
148
+ continue;
149
+ const count = boundedCount(signal.count, 32);
150
+ findings.push(finding(id, 'response headers', 'potential_exposure', 'review', definition.label, definition.detail(count), ['Transient selected response-policy analysis']));
151
+ }
152
+ return findings;
153
+ }
154
+ function pageFindings(pageValue, httpValue) {
155
+ const page = record(pageValue);
156
+ if (!statusAvailable(page, 'html')) {
157
+ return [finding('static_page_evidence_unavailable', 'forms and resources', 'unavailable', 'neutral', 'Static page posture unavailable', 'No bounded static page-identity observation was available to evaluate form and resource destinations.', ['Static page analysis'])];
158
+ }
159
+ const forms = record(page.forms);
160
+ const resources = record(page.resources);
161
+ const formsAvailable = page.forms !== null && typeof page.forms === 'object' && !Array.isArray(page.forms)
162
+ && Object.prototype.hasOwnProperty.call(forms, 'insecureActionCount');
163
+ const resourcesAvailable = page.resources !== null && typeof page.resources === 'object' && !Array.isArray(page.resources)
164
+ && Array.isArray(resources.externalOrigins);
165
+ const insecureActions = boundedCount(forms.insecureActionCount, 50);
166
+ const externalActions = Math.min(Array.isArray(forms.externalActionOrigins) ? forms.externalActionOrigins.length : 0, 10);
167
+ const http = record(httpValue);
168
+ const finalUsesHttps = http.transportSecurity === 'https';
169
+ const externalOrigins = (Array.isArray(resources.externalOrigins) ? resources.externalOrigins : [])
170
+ .slice(0, MAX_RETAINED_ORIGINS)
171
+ .filter((value) => typeof value === 'string' && value.length <= 2048 && !/[\u0000-\u001f\u007f]/.test(value));
172
+ const insecureResourceOrigins = finalUsesHttps
173
+ ? externalOrigins.filter((value) => value.toLowerCase().startsWith('http://')).length
174
+ : 0;
175
+ const complete = page.complete === true && resources.truncated !== true && forms.truncated !== true;
176
+ const findings = [];
177
+ if (!formsAvailable) {
178
+ findings.push(finding('form_metadata_unavailable', 'forms and resources', 'unavailable', 'neutral', 'Form posture unavailable', 'The static page observation did not include the bounded form summary.', ['Static form metadata']));
179
+ }
180
+ else if (insecureActions > 0) {
181
+ findings.push(finding('cleartext_form_actions', 'forms and resources', 'potential_exposure', 'review', 'Cleartext form destinations observed', `${insecureActions} retained form action${insecureActions === 1 ? '' : 's'} used cleartext HTTP from an HTTPS page.`, ['Static form metadata']));
182
+ }
183
+ else if (complete) {
184
+ findings.push(finding('cleartext_form_actions_absent', 'forms and resources', 'observed_absence', 'configured', 'No cleartext form destination observed', 'No retained form action used cleartext HTTP in the complete bounded static-page observation.', ['Static form metadata']));
185
+ }
186
+ if (formsAvailable && externalActions > 0) {
187
+ findings.push(finding('external_form_destinations', 'forms and resources', 'observed', 'neutral', 'External form destinations observed', `${externalActions} external form-action origin${externalActions === 1 ? ' was' : 's were'} retained for review. External submission can be legitimate and is not itself a vulnerability.`, ['Static form metadata']));
188
+ }
189
+ if (finalUsesHttps && !resourcesAvailable) {
190
+ findings.push(finding('resource_metadata_unavailable', 'forms and resources', 'unavailable', 'neutral', 'Resource posture unavailable', 'The static page observation did not include the bounded resource-origin summary.', ['Static resource metadata']));
191
+ }
192
+ else if (finalUsesHttps && insecureResourceOrigins > 0) {
193
+ findings.push(finding('mixed_content_origins', 'forms and resources', 'potential_exposure', 'review', 'Cleartext resource origins observed', `${insecureResourceOrigins} retained resource origin${insecureResourceOrigins === 1 ? '' : 's'} used HTTP from the selected HTTPS page and may create mixed-content exposure.`, ['Static resource metadata']));
194
+ }
195
+ else if (finalUsesHttps && complete) {
196
+ findings.push(finding('mixed_content_origins_absent', 'forms and resources', 'observed_absence', 'configured', 'No cleartext resource origin observed', 'No retained resource origin used HTTP in the complete bounded static-page observation.', ['Static resource metadata']));
197
+ }
198
+ return findings;
199
+ }
200
+ function tlsFindings(tlsValue) {
201
+ const tls = record(tlsValue);
202
+ if (!statusAvailable(tls, 'tls')) {
203
+ return [finding('tls_evidence_unavailable', 'certificate', 'unavailable', 'neutral', 'TLS posture unavailable', 'No successful or partial TLS handshake observation was available for certificate review.', ['TLS collection'])];
204
+ }
205
+ const authorization = record(tls.authorization);
206
+ const hostname = record(tls.hostname);
207
+ const validity = record(tls.validity);
208
+ const protocol = typeof tls.protocol === 'string' && tls.protocol.length <= 32 ? tls.protocol.trim().toUpperCase() : '';
209
+ const findings = [];
210
+ if (protocol) {
211
+ const legacy = ['TLSV1', 'TLSV1.0', 'TLSV1.1'].includes(protocol);
212
+ findings.push(legacy
213
+ ? finding('legacy_tls_negotiated', 'certificate', 'potential_exposure', 'review', 'Legacy TLS negotiated', 'The single retained connection negotiated TLS 1.1 or earlier. This does not enumerate every protocol the endpoint supports.', ['TLS handshake'])
214
+ : finding('modern_tls_negotiated', 'certificate', 'observed', 'configured', 'Modern TLS negotiated', 'The single retained connection negotiated TLS 1.2 or later. This does not enumerate every protocol the endpoint supports.', ['TLS handshake']));
215
+ }
216
+ findings.push(authorization.authorized === true
217
+ ? finding('certificate_authorized', 'certificate', 'observed', 'configured', 'Certificate chain authorized', 'The runtime trust store authorized the observed certificate chain for the retained connection.', ['TLS handshake'])
218
+ : authorization.authorized === false
219
+ ? finding('certificate_not_authorized', 'certificate', 'potential_exposure', 'review', 'Certificate chain not authorized', 'The runtime trust store did not authorize the observed certificate chain. Review the separately attributed TLS evidence for context.', ['TLS handshake'])
220
+ : finding('certificate_authorization_unavailable', 'certificate', 'unavailable', 'neutral', 'Certificate authorization unavailable', 'The retained TLS observation did not include a conclusive chain-authorization result.', ['TLS handshake']));
221
+ findings.push(hostname.matches === true
222
+ ? finding('certificate_hostname_match', 'certificate', 'observed', 'configured', 'Certificate hostname matched', 'The observed leaf certificate matched the hostname used for the retained TLS connection.', ['TLS handshake'])
223
+ : hostname.matches === false
224
+ ? finding('certificate_hostname_mismatch', 'certificate', 'potential_exposure', 'review', 'Certificate hostname mismatch', 'The observed leaf certificate did not match the hostname used for the retained TLS connection.', ['TLS handshake'])
225
+ : finding('certificate_hostname_unavailable', 'certificate', 'unavailable', 'neutral', 'Certificate hostname check unavailable', 'The retained TLS observation did not include a conclusive hostname-match result.', ['TLS handshake']));
226
+ const validityStatus = typeof validity.status === 'string' && validity.status.length <= 32
227
+ ? validity.status.toLowerCase()
228
+ : '';
229
+ findings.push(validityStatus === 'valid'
230
+ ? finding('certificate_valid', 'certificate', 'observed', 'configured', 'Certificate valid at observation time', 'The observed leaf certificate was within its published validity period.', ['TLS certificate'])
231
+ : ['expired', 'not_yet_valid'].includes(validityStatus)
232
+ ? finding('certificate_validity_problem', 'certificate', 'potential_exposure', 'review', 'Certificate validity requires review', 'The observed leaf certificate was outside its published validity period.', ['TLS certificate'])
233
+ : finding('certificate_validity_unavailable', 'certificate', 'unavailable', 'neutral', 'Certificate validity unavailable', 'The retained TLS observation did not provide a complete certificate validity period.', ['TLS certificate']));
234
+ return findings;
235
+ }
236
+ function dnssecState(value) {
237
+ if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/.test(value))
238
+ return 'unknown';
239
+ const normalized = value.trim().toLowerCase().replace(/[\s_-]+/g, '');
240
+ if (['signed', 'signeddelegation', 'secure', 'yes', 'true', 'active'].includes(normalized))
241
+ return 'signed';
242
+ if (['unsigned', 'insecure', 'no', 'false', 'inactive'].includes(normalized))
243
+ return 'unsigned';
244
+ return 'unknown';
245
+ }
246
+ function dnsFindings(dnsValue, dnssecValue) {
247
+ const dns = record(dnsValue);
248
+ const records = record(dns.records);
249
+ const diagnostics = record(dns.diagnostics);
250
+ const caaDiagnostic = record(diagnostics.caa);
251
+ const caaPolicy = record(dns.caaPolicy);
252
+ const hasEffectivePolicy = caaPolicy.policyVersion === 1;
253
+ const caaAvailable = hasEffectivePolicy
254
+ ? caaPolicy.complete === true
255
+ && caaPolicy.truncated !== true
256
+ && typeof caaPolicy.status === 'string'
257
+ && ['success', 'not_found'].includes(caaPolicy.status)
258
+ : statusAvailable(dns, 'dns')
259
+ && typeof caaDiagnostic.status === 'string'
260
+ && ['success', 'not_found'].includes(caaDiagnostic.status);
261
+ const caaRecords = hasEffectivePolicy ? caaPolicy.records : records.caa;
262
+ const caaCount = Math.min(Array.isArray(caaRecords) ? caaRecords.length : 0, 16);
263
+ const caaSource = hasEffectivePolicy ? 'DNS effective CAA policy' : 'DNS CAA query';
264
+ const findings = [];
265
+ const dnssec = dnssecState(dnssecValue);
266
+ findings.push(dnssec === 'signed'
267
+ ? finding('dnssec_signed', 'domain controls', 'observed', 'configured', 'DNSSEC signing reported', 'The registry or WHOIS source reported a signed DNSSEC delegation.', ['Registry or WHOIS DNSSEC publication'])
268
+ : dnssec === 'unsigned'
269
+ ? finding('dnssec_unsigned', 'domain controls', 'observed_absence', 'review', 'DNSSEC signing not reported', 'The registry or WHOIS source reported an unsigned DNSSEC delegation.', ['Registry or WHOIS DNSSEC publication'])
270
+ : finding('dnssec_unavailable', 'domain controls', 'unavailable', 'neutral', 'DNSSEC state unavailable', 'The retained registry and WHOIS evidence did not provide a recognized DNSSEC state.', ['Registry or WHOIS DNSSEC publication']));
271
+ findings.push(!caaAvailable
272
+ ? finding('caa_unavailable', 'domain controls', 'unavailable', 'neutral', 'CAA posture unavailable', 'The CAA query did not produce a conclusive response for this observation.', [caaSource])
273
+ : caaCount > 0
274
+ ? finding('caa_observed', 'domain controls', 'observed', 'configured', 'CAA records observed', `${caaCount} bounded CAA record${caaCount === 1 ? ' was' : 's were'} observed. Record policy quality was not scored.`, [caaSource])
275
+ : finding('caa_absent', 'domain controls', 'observed_absence', 'review', 'CAA records not observed', hasEffectivePolicy
276
+ ? 'The completed effective-policy walk returned no applicable CAA record in this point-in-time observation.'
277
+ : 'The resolver returned no CAA record in this point-in-time query.', [caaSource]));
278
+ return findings;
279
+ }
280
+ function analyzeWebsiteSecurityPosture(input = {}) {
281
+ const findings = [
282
+ ...httpFindings(input.http),
283
+ ...responsePolicyFindings(input.responsePolicy),
284
+ ...pageFindings(input.pageIdentity, input.http),
285
+ ...tlsFindings(input.tls),
286
+ ...dnsFindings(input.dns, input.dnssec),
287
+ ].slice(0, MAX_SECURITY_POSTURE_FINDINGS);
288
+ const sourceValues = [input.http, input.pageIdentity, input.tls, input.dns];
289
+ if (input.responsePolicy !== undefined)
290
+ sourceValues.push(input.responsePolicy);
291
+ const partial = sourceValues.some((value) => {
292
+ const item = record(value);
293
+ return item.status !== 'success' || item.complete !== true;
294
+ }) || findings.some((item) => item.state === 'unavailable');
295
+ const counts = {
296
+ observed: findings.filter((item) => item.state === 'observed').length,
297
+ potentialExposure: findings.filter((item) => item.state === 'potential_exposure').length,
298
+ observedAbsence: findings.filter((item) => item.state === 'observed_absence').length,
299
+ unavailable: findings.filter((item) => item.state === 'unavailable').length,
300
+ };
301
+ const limitations = [
302
+ 'This is a point-in-time passive interpretation of one bounded deep lookup, not an active vulnerability assessment.',
303
+ 'Observed absence applies only to the selected response or retained static evidence and does not establish site-wide absence.',
304
+ 'Response-policy findings retain fixed identifiers and counts only; complete policies, cookie data, nonces, hashes, and reporting endpoints are discarded.',
305
+ 'The TLS result describes one negotiated connection and does not enumerate every supported protocol or cipher.',
306
+ ];
307
+ if (partial)
308
+ limitations.push('One or more contributing source observations were partial or unavailable.');
309
+ return {
310
+ postureVersion: WEBSITE_SECURITY_POSTURE_VERSION,
311
+ ...createObservation({
312
+ status: partial ? 'partial' : 'success',
313
+ observedAt: input.observedAt,
314
+ scanMode: 'deep',
315
+ source: 'derived',
316
+ complete: !partial,
317
+ truncated: false,
318
+ limitations,
319
+ diagnostics: { findings: findings.length, ...counts },
320
+ }),
321
+ summary: counts,
322
+ findings,
323
+ };
324
+ }
325
+ export { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, analyzeWebsiteSecurityPosture, };