securl 1.4.1

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 (74) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/LICENSE +21 -0
  3. package/README.md +427 -0
  4. package/RELEASING.md +37 -0
  5. package/SECURITY.md +27 -0
  6. package/dist/certificate.d.ts +5 -0
  7. package/dist/certificate.js +92 -0
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +674 -0
  10. package/dist/compromiseSignals.d.ts +10 -0
  11. package/dist/compromiseSignals.js +183 -0
  12. package/dist/cookie-analysis.d.ts +2 -0
  13. package/dist/cookie-analysis.js +41 -0
  14. package/dist/cookieAnalysis.d.ts +2 -0
  15. package/dist/cookieAnalysis.js +82 -0
  16. package/dist/ctDiscovery.d.ts +19 -0
  17. package/dist/ctDiscovery.js +357 -0
  18. package/dist/domain-security.d.ts +10 -0
  19. package/dist/domain-security.js +416 -0
  20. package/dist/header-analysis.d.ts +14 -0
  21. package/dist/header-analysis.js +165 -0
  22. package/dist/historyDiff.d.ts +4 -0
  23. package/dist/historyDiff.js +117 -0
  24. package/dist/html-extraction.d.ts +12 -0
  25. package/dist/html-extraction.js +279 -0
  26. package/dist/html-page-analysis.d.ts +38 -0
  27. package/dist/html-page-analysis.js +459 -0
  28. package/dist/htmlInsights.d.ts +23 -0
  29. package/dist/htmlInsights.js +460 -0
  30. package/dist/identityProvider.d.ts +14 -0
  31. package/dist/identityProvider.js +259 -0
  32. package/dist/index.d.ts +17 -0
  33. package/dist/index.js +1008 -0
  34. package/dist/infrastructure.d.ts +9 -0
  35. package/dist/infrastructure.js +149 -0
  36. package/dist/libraryRisk.d.ts +3 -0
  37. package/dist/libraryRisk.js +164 -0
  38. package/dist/network-validation.d.ts +30 -0
  39. package/dist/network-validation.js +161 -0
  40. package/dist/network.d.ts +34 -0
  41. package/dist/network.js +139 -0
  42. package/dist/passive-intelligence.d.ts +21 -0
  43. package/dist/passive-intelligence.js +247 -0
  44. package/dist/path-discovery.d.ts +4 -0
  45. package/dist/path-discovery.js +50 -0
  46. package/dist/postureDigest.d.ts +142 -0
  47. package/dist/postureDigest.js +159 -0
  48. package/dist/postureDrift.d.ts +4 -0
  49. package/dist/postureDrift.js +118 -0
  50. package/dist/postureRemediation.d.ts +6 -0
  51. package/dist/postureRemediation.js +286 -0
  52. package/dist/redirectChain.d.ts +2 -0
  53. package/dist/redirectChain.js +39 -0
  54. package/dist/riskEvents.d.ts +3 -0
  55. package/dist/riskEvents.js +187 -0
  56. package/dist/scannerConfig.d.ts +49 -0
  57. package/dist/scannerConfig.js +79 -0
  58. package/dist/scoring.d.ts +32 -0
  59. package/dist/scoring.js +367 -0
  60. package/dist/security-txt.d.ts +4 -0
  61. package/dist/security-txt.js +123 -0
  62. package/dist/surfaceEnrichment.d.ts +44 -0
  63. package/dist/surfaceEnrichment.js +377 -0
  64. package/dist/technology-detection.d.ts +4 -0
  65. package/dist/technology-detection.js +93 -0
  66. package/dist/types.d.ts +730 -0
  67. package/dist/types.js +1 -0
  68. package/dist/utils.d.ts +7 -0
  69. package/dist/utils.js +66 -0
  70. package/dist/wafFingerprint.d.ts +5 -0
  71. package/dist/wafFingerprint.js +156 -0
  72. package/examples/risk-events.mjs +27 -0
  73. package/examples/scan-url.mjs +17 -0
  74. package/package.json +102 -0
@@ -0,0 +1,459 @@
1
+ import { URL } from "node:url";
2
+ import { parse } from "node-html-parser";
3
+ import { analyzeAiSurface, detectHtmlTechnologies } from "./htmlInsights.js";
4
+ import { collectClientExposureSignals, collectPassiveLeakSignals, collectSameSiteHosts, extractHtmlTitle, getHtmlTitle, isAccessDeniedHtml, normalizeHtmlSignature, } from "./html-extraction.js";
5
+ import { collectLibraryFingerprints } from "./libraryRisk.js";
6
+ import { normalizeDiscoveredPath, rankDiscoveredPaths } from "./path-discovery.js";
7
+ import { requestText } from "./network.js";
8
+ import { headerValue, unique } from "./utils.js";
9
+ const emptySriCoverage = () => ({
10
+ externalScripts: 0,
11
+ externalStylesheets: 0,
12
+ scriptsWithSri: 0,
13
+ stylesheetsWithSri: 0,
14
+ coveragePercent: 100,
15
+ issues: [],
16
+ strengths: [],
17
+ });
18
+ const hasValidSriPrefix = (integrity) => Boolean(integrity && /\bsha(?:256|384|512)-[A-Za-z0-9+/=]+/.test(integrity));
19
+ const isAbsoluteExternalResource = (value, finalUrl) => {
20
+ if (!value || !/^(?:https?:)?\/\//i.test(value)) {
21
+ return false;
22
+ }
23
+ try {
24
+ return new URL(value, finalUrl).hostname !== finalUrl.hostname;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ };
30
+ const calculateSriCoverage = (scriptElements, stylesheetElements, finalUrl) => {
31
+ const externalScripts = scriptElements.filter((script) => isAbsoluteExternalResource(script.getAttribute("src"), finalUrl));
32
+ const externalStylesheets = stylesheetElements.filter((link) => isAbsoluteExternalResource(link.getAttribute("href"), finalUrl));
33
+ const scriptsWithSri = externalScripts.filter((script) => hasValidSriPrefix(script.getAttribute("integrity"))).length;
34
+ const stylesheetsWithSri = externalStylesheets.filter((link) => hasValidSriPrefix(link.getAttribute("integrity"))).length;
35
+ const total = externalScripts.length + externalStylesheets.length;
36
+ const protectedTotal = scriptsWithSri + stylesheetsWithSri;
37
+ const coveragePercent = total ? Math.round((protectedTotal / total) * 100) : 100;
38
+ const issues = [];
39
+ const strengths = [];
40
+ if (total > 0 && coveragePercent === 0) {
41
+ issues.push("External scripts or stylesheets are loaded without Subresource Integrity coverage.");
42
+ }
43
+ else if (total > 0 && coveragePercent < 100) {
44
+ issues.push(`Subresource Integrity coverage is partial (${coveragePercent}%).`);
45
+ }
46
+ else if (total > 0) {
47
+ strengths.push("All externally hosted scripts and stylesheets include Subresource Integrity hashes.");
48
+ }
49
+ return {
50
+ externalScripts: externalScripts.length,
51
+ externalStylesheets: externalStylesheets.length,
52
+ scriptsWithSri,
53
+ stylesheetsWithSri,
54
+ coveragePercent,
55
+ issues,
56
+ strengths,
57
+ };
58
+ };
59
+ const extractVersion = (value, pattern) => value.match(pattern)?.slice(1).find(Boolean) || null;
60
+ const extractVersionNear = (value, marker) => {
61
+ const index = value.toLowerCase().indexOf(marker.toLowerCase());
62
+ if (index === -1) {
63
+ return null;
64
+ }
65
+ return value.slice(index, index + marker.length + 80).match(/\d+\.\d+\.\d+|\d+\.\d+/)?.[0] || null;
66
+ };
67
+ const detectFrameworkVersionLeaks = (html, metaGenerator, externalScriptUrls, externalStylesheetUrls) => {
68
+ const signals = `${html}\n${metaGenerator || ""}\n${externalScriptUrls.join("\n")}\n${externalStylesheetUrls.join("\n")}`;
69
+ const lower = signals.toLowerCase();
70
+ const generator = (metaGenerator || "").toLowerCase();
71
+ const leaks = [];
72
+ const seen = new Set();
73
+ const addLeak = (framework, versionHint, evidence, risk = "medium") => {
74
+ const key = `${framework}:${versionHint || evidence}`;
75
+ if (seen.has(key))
76
+ return;
77
+ seen.add(key);
78
+ leaks.push({ framework, versionHint, evidence, risk });
79
+ };
80
+ if (/__REACT_VERSION__|react@|react-dom@/i.test(signals)) {
81
+ addLeak("React", extractVersion(signals, /react(?:-dom)?@(\d+\.\d+\.\d+)/i), "React version marker is visible in page source or asset references.");
82
+ }
83
+ if (/ng\.version\.full|@angular\/core/i.test(signals)) {
84
+ addLeak("Angular", extractVersionNear(signals, "@angular/core"), "Angular version marker is visible in page source.");
85
+ }
86
+ if (/Vue\.version|__VUE_VERSION__|\/vue@\d/i.test(signals)) {
87
+ addLeak("Vue", extractVersionNear(signals, "Vue.version") || extractVersionNear(signals, "__VUE_VERSION__") || extractVersionNear(signals, "/vue@"), "Vue version marker is visible in page source or asset references.");
88
+ }
89
+ if (lower.includes("__next_data__") || lower.includes("/_next/static")) {
90
+ addLeak("Next.js", null, "Next.js runtime markers are visible in the fetched HTML.");
91
+ }
92
+ if (/jQuery v/i.test(signals) || /jquery[-.@/](\d+\.\d+\.\d+)/i.test(signals)) {
93
+ addLeak("jQuery", extractVersion(signals, /jQuery v(\d+\.\d+\.\d+)|jquery[-.@/](\d+\.\d+\.\d+)/i), "jQuery version marker or asset path is visible.");
94
+ }
95
+ if (/bootstrap@|Bootstrap v/i.test(signals)) {
96
+ addLeak("Bootstrap", extractVersion(signals, /(?:bootstrap@|Bootstrap v)(\d+\.\d+\.\d+)/i), "Bootstrap version marker is visible.");
97
+ }
98
+ if (lower.includes("wp-content/") || lower.includes("wp-includes/") || generator.includes("wordpress")) {
99
+ addLeak("WordPress", extractVersion(signals, /WordPress\s+(\d+\.\d+(?:\.\d+)?)/i), "WordPress public page markers are visible.", "low");
100
+ }
101
+ if (lower.includes("sites/default/files/") || lower.includes("drupal.js") || generator.includes("drupal")) {
102
+ addLeak("Drupal", extractVersion(signals, /Drupal\s+(\d+\.\d+(?:\.\d+)?)/i), "Drupal public page markers are visible.", "low");
103
+ }
104
+ if (lower.includes("/media/joomla_version.xml") || generator.includes("joomla")) {
105
+ addLeak("Joomla", extractVersion(signals, /Joomla!?\s+(\d+\.\d+(?:\.\d+)?)/i), "Joomla public page markers are visible.", "low");
106
+ }
107
+ return leaks;
108
+ };
109
+ const summarizeEvidence = (values, limit = 6) => unique(values.filter((value) => Boolean(value && value.trim())).map((value) => value.trim())).slice(0, limit);
110
+ const detectSuspiciousScriptSignals = (scriptElements, externalScriptUrls, finalUrl) => {
111
+ const signals = [];
112
+ const inlineBodies = scriptElements
113
+ .filter((script) => !script.getAttribute("src"))
114
+ .map((script) => script.text.slice(0, 20_000));
115
+ const inlineCorpus = inlineBodies.join("\n");
116
+ const obfuscationMarkers = summarizeEvidence([
117
+ /\beval\s*\(/.test(inlineCorpus) ? "eval(...)" : null,
118
+ /\batob\s*\(/.test(inlineCorpus) ? "atob(...)" : null,
119
+ /String\.fromCharCode\s*\(/.test(inlineCorpus) ? "String.fromCharCode(...)" : null,
120
+ /\bunescape\s*\(/.test(inlineCorpus) ? "unescape(...)" : null,
121
+ /[A-Za-z0-9+/]{180,}={0,2}/.test(inlineCorpus) ? "long base64-like string" : null,
122
+ ]);
123
+ if (obfuscationMarkers.length) {
124
+ signals.push({
125
+ category: "obfuscation",
126
+ severity: "warning",
127
+ title: "Obfuscated inline script markers visible",
128
+ detail: "The fetched page contains inline JavaScript patterns often used by packers, tag loaders, or injected scripts. Review the source before treating this as compromise proof.",
129
+ evidence: obfuscationMarkers,
130
+ });
131
+ }
132
+ const dynamicLoaderMarkers = summarizeEvidence([
133
+ /document\.write\s*\([^)]*<script/i.test(inlineCorpus) ? "document.write(<script...)" : null,
134
+ /createElement\s*\(\s*['"]script['"]\s*\)/i.test(inlineCorpus) ? "createElement('script')" : null,
135
+ /\.appendChild\s*\([^)]*script/i.test(inlineCorpus) ? "appendChild(script)" : null,
136
+ ]);
137
+ if (dynamicLoaderMarkers.length) {
138
+ signals.push({
139
+ category: "dynamic_loader",
140
+ severity: "info",
141
+ title: "Dynamic script loader markers visible",
142
+ detail: "The page dynamically creates or writes script elements. That is common for tag managers, but it increases review value when combined with weak CSP or unfamiliar third-party hosts.",
143
+ evidence: dynamicLoaderMarkers,
144
+ });
145
+ }
146
+ const suspiciousHosts = summarizeEvidence(externalScriptUrls
147
+ .map((url) => {
148
+ const parsed = new URL(url);
149
+ if (parsed.hostname === finalUrl.hostname)
150
+ return null;
151
+ return /(^|\.)xn--|\.duckdns\.org$|\.ddns\.net$|\.no-ip\./i.test(parsed.hostname)
152
+ ? parsed.hostname
153
+ : null;
154
+ }));
155
+ if (suspiciousHosts.length) {
156
+ signals.push({
157
+ category: "suspicious_host",
158
+ severity: "warning",
159
+ title: "Suspicious script host pattern visible",
160
+ detail: "One or more script hosts use punycode or dynamic-DNS style naming. Review ownership and expected use before trusting loaded code.",
161
+ evidence: suspiciousHosts,
162
+ });
163
+ }
164
+ return signals;
165
+ };
166
+ export async function fetchHtmlDocument(finalUrl) {
167
+ const response = await requestText(finalUrl);
168
+ const contentType = headerValue(response.headers, "content-type") || "";
169
+ if (!contentType.toLowerCase().includes("text/html")) {
170
+ return null;
171
+ }
172
+ const html = response.body;
173
+ return {
174
+ html,
175
+ pageTitle: getHtmlTitle(html),
176
+ signature: normalizeHtmlSignature(html),
177
+ };
178
+ }
179
+ export function analyzeHtmlSecurity(finalUrl, document) {
180
+ try {
181
+ if (!document) {
182
+ return {
183
+ fetched: false,
184
+ pageUrl: finalUrl.toString(),
185
+ pageTitle: null,
186
+ metaGenerator: null,
187
+ forms: [],
188
+ sameSiteHosts: [],
189
+ externalScriptDomains: [],
190
+ externalStylesheetDomains: [],
191
+ insecureResourceUrls: [],
192
+ inlineScriptCount: 0,
193
+ inlineStyleCount: 0,
194
+ missingSriScriptUrls: [],
195
+ sriCoverage: emptySriCoverage(),
196
+ firstPartyPaths: [],
197
+ passiveLeakSignals: [],
198
+ clientExposureSignals: [],
199
+ libraryFingerprints: [],
200
+ libraryRiskSignals: [],
201
+ frameworkVersionLeaks: [],
202
+ suspiciousScriptSignals: [],
203
+ detectedTechnologies: [],
204
+ aiSurface: {
205
+ detected: false,
206
+ assistantVisible: false,
207
+ aiPageSignals: [],
208
+ vendors: [],
209
+ discoveredPaths: [],
210
+ disclosures: [],
211
+ privacySignals: [],
212
+ governanceSignals: [],
213
+ issues: ["Primary response was not HTML, so AI surface inspection was skipped."],
214
+ strengths: [],
215
+ },
216
+ issues: ["Primary response was not HTML, so page content inspection was skipped."],
217
+ strengths: [],
218
+ };
219
+ }
220
+ const html = document.html;
221
+ const issues = [];
222
+ const strengths = [];
223
+ const root = parse(html);
224
+ const pageTitle = document.pageTitle || root.querySelector("title")?.text?.trim() || null;
225
+ const metaGenerator = root.querySelector('meta[name="generator"]')?.getAttribute("content") || null;
226
+ const forms = root.querySelectorAll("form").map((form) => {
227
+ const action = form.getAttribute("action") || null;
228
+ const method = (form.getAttribute("method") || "GET").toUpperCase();
229
+ const resolvedAction = action ? new URL(action, finalUrl).toString() : finalUrl.toString();
230
+ const actionHost = new URL(resolvedAction).hostname;
231
+ return {
232
+ action,
233
+ resolvedAction,
234
+ actionHost,
235
+ method,
236
+ insecureSubmission: resolvedAction.startsWith("http://"),
237
+ hasPasswordField: form.querySelectorAll('input[type="password"]').length > 0,
238
+ offOriginSubmission: actionHost !== finalUrl.hostname,
239
+ };
240
+ });
241
+ const scriptElements = root.querySelectorAll("script");
242
+ const externalScriptUrls = scriptElements
243
+ .map((script) => script.getAttribute("src"))
244
+ .filter(Boolean)
245
+ .map((src) => new URL(src, finalUrl).toString());
246
+ const stylesheetElements = root.querySelectorAll('link[rel~="stylesheet"]');
247
+ const externalStylesheetUrls = stylesheetElements
248
+ .map((link) => link.getAttribute("href"))
249
+ .filter(Boolean)
250
+ .map((href) => new URL(href, finalUrl).toString());
251
+ const anchorElements = root.querySelectorAll("a[href]");
252
+ const sameSiteHosts = collectSameSiteHosts(finalUrl, [
253
+ ...anchorElements.map((anchor) => anchor.getAttribute("href")),
254
+ ...scriptElements.map((script) => script.getAttribute("src")),
255
+ ...stylesheetElements.map((link) => link.getAttribute("href")),
256
+ ...forms.map((form) => form.action),
257
+ ]);
258
+ const firstPartyPaths = rankDiscoveredPaths([
259
+ ...anchorElements.map((anchor) => normalizeDiscoveredPath(anchor.getAttribute("href"), finalUrl)),
260
+ ...forms.map((form) => normalizeDiscoveredPath(form.action, finalUrl)),
261
+ ]);
262
+ const trainingLabMarkers = unique([
263
+ /(xss game|firing range|vulnweb|testfire|altoro mutual)/i.test(pageTitle || "")
264
+ ? `Title: ${pageTitle}`
265
+ : null,
266
+ ...firstPartyPaths
267
+ .filter((path) => /(xss|clickjacking|csrf|sql|sqli|mixedcontent|leakedcookie|dom)(?:\/|$|-|_)/i.test(path))
268
+ .slice(0, 6),
269
+ ]);
270
+ const insecureResourceUrls = unique([...externalScriptUrls, ...externalStylesheetUrls].filter((url) => url.startsWith("http://")));
271
+ const externalScriptDomains = unique(externalScriptUrls.map((url) => new URL(url).hostname).filter((hostname) => hostname !== finalUrl.hostname));
272
+ const externalStylesheetDomains = unique(externalStylesheetUrls.map((url) => new URL(url).hostname).filter((hostname) => hostname !== finalUrl.hostname));
273
+ const inlineScriptCount = scriptElements.filter((script) => !script.getAttribute("src")).length;
274
+ const inlineStyleCount = root.querySelectorAll("style").length;
275
+ const missingSriScriptUrls = scriptElements
276
+ .map((script) => {
277
+ const src = script.getAttribute("src");
278
+ if (!src) {
279
+ return null;
280
+ }
281
+ const resolved = new URL(src, finalUrl);
282
+ if (resolved.hostname === finalUrl.hostname || script.getAttribute("integrity")) {
283
+ return null;
284
+ }
285
+ return resolved.toString();
286
+ })
287
+ .filter(Boolean);
288
+ const sriCoverage = calculateSriCoverage(scriptElements, stylesheetElements, finalUrl);
289
+ const suspiciousScriptSignals = detectSuspiciousScriptSignals(scriptElements, externalScriptUrls, finalUrl);
290
+ const passiveLeakSignals = collectPassiveLeakSignals(html, finalUrl, metaGenerator || null, externalScriptUrls, externalStylesheetUrls);
291
+ const clientExposureSignals = collectClientExposureSignals(html, finalUrl);
292
+ if (forms.some((form) => form.hasPasswordField)) {
293
+ strengths.push("Login-like form elements are present for passive inspection.");
294
+ }
295
+ if (sameSiteHosts.length) {
296
+ strengths.push(`Page content referenced ${sameSiteHosts.length} same-site host${sameSiteHosts.length === 1 ? "" : "s"} for passive discovery.`);
297
+ }
298
+ if (forms.some((form) => form.insecureSubmission)) {
299
+ issues.push("At least one form appears to submit over HTTP.");
300
+ }
301
+ if (forms.some((form) => form.hasPasswordField && form.offOriginSubmission)) {
302
+ issues.push("A password form appears to submit to a different origin.");
303
+ }
304
+ if (insecureResourceUrls.length) {
305
+ issues.push("The page references insecure HTTP resources.");
306
+ }
307
+ if (inlineScriptCount > 0) {
308
+ issues.push(`Inline scripts detected (${inlineScriptCount}).`);
309
+ }
310
+ if (inlineStyleCount > 0) {
311
+ issues.push(`Inline style blocks detected (${inlineStyleCount}).`);
312
+ }
313
+ if (missingSriScriptUrls.length) {
314
+ issues.push("Some third-party scripts are missing Subresource Integrity attributes.");
315
+ }
316
+ issues.push(...sriCoverage.issues);
317
+ strengths.push(...sriCoverage.strengths);
318
+ for (const signal of passiveLeakSignals) {
319
+ if (signal.severity === "warning") {
320
+ issues.push(signal.title);
321
+ }
322
+ }
323
+ for (const signal of clientExposureSignals) {
324
+ if (signal.severity === "warning") {
325
+ issues.push(signal.title);
326
+ }
327
+ }
328
+ for (const signal of suspiciousScriptSignals) {
329
+ if (signal.severity === "warning") {
330
+ issues.push(signal.title);
331
+ }
332
+ }
333
+ if (trainingLabMarkers.length) {
334
+ issues.push("Page content suggests an intentionally vulnerable training or challenge surface.");
335
+ }
336
+ if (firstPartyPaths.length) {
337
+ strengths.push(`Discovered ${firstPartyPaths.length} same-origin navigation paths for low-noise follow-up scans.`);
338
+ }
339
+ if (passiveLeakSignals.length) {
340
+ strengths.push(`Passive pre-check identified ${passiveLeakSignals.length} leak or fingerprinting signal${passiveLeakSignals.length === 1 ? "" : "s"} worth review.`);
341
+ }
342
+ if (clientExposureSignals.length) {
343
+ strengths.push(`Client-side markup exposed ${clientExposureSignals.length} API or configuration signal${clientExposureSignals.length === 1 ? "" : "s"} for review.`);
344
+ }
345
+ if (!issues.length) {
346
+ strengths.push("No obvious passive HTML transport/content risks detected on the fetched page.");
347
+ }
348
+ return {
349
+ fetched: true,
350
+ pageUrl: finalUrl.toString(),
351
+ pageTitle,
352
+ metaGenerator: metaGenerator || null,
353
+ forms,
354
+ sameSiteHosts,
355
+ externalScriptDomains,
356
+ externalStylesheetDomains,
357
+ insecureResourceUrls,
358
+ inlineScriptCount,
359
+ inlineStyleCount,
360
+ missingSriScriptUrls,
361
+ sriCoverage,
362
+ firstPartyPaths,
363
+ passiveLeakSignals,
364
+ clientExposureSignals,
365
+ libraryFingerprints: collectLibraryFingerprints(externalScriptUrls),
366
+ libraryRiskSignals: [],
367
+ frameworkVersionLeaks: detectFrameworkVersionLeaks(html, metaGenerator || null, externalScriptUrls, externalStylesheetUrls),
368
+ suspiciousScriptSignals,
369
+ detectedTechnologies: detectHtmlTechnologies(html, finalUrl, metaGenerator || null, externalScriptUrls, externalStylesheetUrls),
370
+ aiSurface: analyzeAiSurface(html, externalScriptUrls, firstPartyPaths),
371
+ issues,
372
+ strengths,
373
+ };
374
+ }
375
+ catch (error) {
376
+ return {
377
+ fetched: false,
378
+ pageUrl: finalUrl.toString(),
379
+ pageTitle: null,
380
+ metaGenerator: null,
381
+ forms: [],
382
+ sameSiteHosts: [],
383
+ externalScriptDomains: [],
384
+ externalStylesheetDomains: [],
385
+ insecureResourceUrls: [],
386
+ inlineScriptCount: 0,
387
+ inlineStyleCount: 0,
388
+ missingSriScriptUrls: [],
389
+ sriCoverage: emptySriCoverage(),
390
+ firstPartyPaths: [],
391
+ passiveLeakSignals: [],
392
+ clientExposureSignals: [],
393
+ libraryFingerprints: [],
394
+ libraryRiskSignals: [],
395
+ frameworkVersionLeaks: [],
396
+ suspiciousScriptSignals: [],
397
+ detectedTechnologies: [],
398
+ aiSurface: {
399
+ detected: false,
400
+ assistantVisible: false,
401
+ aiPageSignals: [],
402
+ vendors: [],
403
+ discoveredPaths: [],
404
+ disclosures: [],
405
+ privacySignals: [],
406
+ governanceSignals: [],
407
+ issues: [error instanceof Error ? error.message : "AI surface inspection failed."],
408
+ strengths: [],
409
+ },
410
+ issues: [error instanceof Error ? error.message : "HTML inspection failed."],
411
+ strengths: [],
412
+ };
413
+ }
414
+ }
415
+ export function analyzeHtmlDocument(input, html) {
416
+ const finalUrl = typeof input === "string" ? new URL(input) : input;
417
+ const pageTitle = extractHtmlTitle(html);
418
+ return analyzeHtmlSecurity(finalUrl, { html, pageTitle });
419
+ }
420
+ export function detectAssessmentLimitation(statusCode, headers, html) {
421
+ if (statusCode === 401) {
422
+ return {
423
+ limited: true,
424
+ kind: "auth_required",
425
+ title: "Assessment limited by an authenticated response",
426
+ detail: "The target required authentication before serving the normal page, so this result reflects a restricted response rather than the full application surface.",
427
+ };
428
+ }
429
+ if (statusCode === 429) {
430
+ return {
431
+ limited: true,
432
+ kind: "rate_limited",
433
+ title: "Assessment limited by rate limiting",
434
+ detail: "The target rate-limited the scanner, so this result reflects a throttled response rather than a normal page render.",
435
+ };
436
+ }
437
+ if (statusCode >= 500) {
438
+ return {
439
+ limited: true,
440
+ kind: "service_unavailable",
441
+ title: "Assessment limited by service availability",
442
+ detail: `The target returned HTTP ${statusCode}, so this result reflects an unavailable or error response rather than the normal site posture.`,
443
+ };
444
+ }
445
+ if (statusCode === 403 && html && isAccessDeniedHtml(headers, html)) {
446
+ return {
447
+ limited: true,
448
+ kind: "blocked_edge_response",
449
+ title: "Assessment limited by a blocked edge response",
450
+ detail: "The target returned a generic blocked or protection-layer response, so missing headers on this page may not reflect the normal site posture.",
451
+ };
452
+ }
453
+ return {
454
+ limited: false,
455
+ kind: null,
456
+ title: null,
457
+ detail: null,
458
+ };
459
+ }
@@ -0,0 +1,23 @@
1
+ import type { AiSurfaceInfo, AnalysisResult, ExecutiveSummaryInfo, IssueConfidence, TechnologyResult, ThirdPartyProvider, ThirdPartyTrustInfo } from "./types.js";
2
+ interface AiVendorMatcher {
3
+ name: string;
4
+ pattern: RegExp;
5
+ evidence: string;
6
+ category: AiSurfaceInfo["vendors"][number]["category"];
7
+ confidence: IssueConfidence;
8
+ }
9
+ export declare const AI_VENDOR_MATCHERS: AiVendorMatcher[];
10
+ interface ThirdPartyProviderMatcher {
11
+ pattern: RegExp;
12
+ name: string;
13
+ category: ThirdPartyProvider["category"];
14
+ risk: ThirdPartyProvider["risk"];
15
+ evidence: string;
16
+ }
17
+ export declare const THIRD_PARTY_PROVIDER_MATCHERS: ThirdPartyProviderMatcher[];
18
+ export declare const detectHtmlTechnologies: (html: string, finalUrl: URL, metaGenerator: string | null, externalScriptUrls: string[], externalStylesheetUrls: string[]) => TechnologyResult[];
19
+ export declare const analyzeAiSurface: (html: string, externalScriptUrls: string[], firstPartyPaths: string[]) => AiSurfaceInfo;
20
+ export declare const analyzeThirdPartyTrust: (finalUrl: URL, htmlSecurity: Pick<AnalysisResult["htmlSecurity"], "externalScriptDomains" | "externalStylesheetDomains" | "missingSriScriptUrls">, aiSurface: AiSurfaceInfo) => ThirdPartyTrustInfo;
21
+ export declare const buildExecutiveSummary: (result: Pick<AnalysisResult, "score" | "headers" | "thirdPartyTrust" | "aiSurface" | "domainSecurity" | "publicSignals" | "assessmentLimitation" | "htmlSecurity">) => ExecutiveSummaryInfo;
22
+ export declare const mergeTechnologies: (...groups: Array<TechnologyResult[] | null | undefined>) => TechnologyResult[];
23
+ export {};