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.
- package/CHANGELOG.md +241 -0
- package/LICENSE +21 -0
- package/README.md +427 -0
- package/RELEASING.md +37 -0
- package/SECURITY.md +27 -0
- package/dist/certificate.d.ts +5 -0
- package/dist/certificate.js +92 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +674 -0
- package/dist/compromiseSignals.d.ts +10 -0
- package/dist/compromiseSignals.js +183 -0
- package/dist/cookie-analysis.d.ts +2 -0
- package/dist/cookie-analysis.js +41 -0
- package/dist/cookieAnalysis.d.ts +2 -0
- package/dist/cookieAnalysis.js +82 -0
- package/dist/ctDiscovery.d.ts +19 -0
- package/dist/ctDiscovery.js +357 -0
- package/dist/domain-security.d.ts +10 -0
- package/dist/domain-security.js +416 -0
- package/dist/header-analysis.d.ts +14 -0
- package/dist/header-analysis.js +165 -0
- package/dist/historyDiff.d.ts +4 -0
- package/dist/historyDiff.js +117 -0
- package/dist/html-extraction.d.ts +12 -0
- package/dist/html-extraction.js +279 -0
- package/dist/html-page-analysis.d.ts +38 -0
- package/dist/html-page-analysis.js +459 -0
- package/dist/htmlInsights.d.ts +23 -0
- package/dist/htmlInsights.js +460 -0
- package/dist/identityProvider.d.ts +14 -0
- package/dist/identityProvider.js +259 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +1008 -0
- package/dist/infrastructure.d.ts +9 -0
- package/dist/infrastructure.js +149 -0
- package/dist/libraryRisk.d.ts +3 -0
- package/dist/libraryRisk.js +164 -0
- package/dist/network-validation.d.ts +30 -0
- package/dist/network-validation.js +161 -0
- package/dist/network.d.ts +34 -0
- package/dist/network.js +139 -0
- package/dist/passive-intelligence.d.ts +21 -0
- package/dist/passive-intelligence.js +247 -0
- package/dist/path-discovery.d.ts +4 -0
- package/dist/path-discovery.js +50 -0
- package/dist/postureDigest.d.ts +142 -0
- package/dist/postureDigest.js +159 -0
- package/dist/postureDrift.d.ts +4 -0
- package/dist/postureDrift.js +118 -0
- package/dist/postureRemediation.d.ts +6 -0
- package/dist/postureRemediation.js +286 -0
- package/dist/redirectChain.d.ts +2 -0
- package/dist/redirectChain.js +39 -0
- package/dist/riskEvents.d.ts +3 -0
- package/dist/riskEvents.js +187 -0
- package/dist/scannerConfig.d.ts +49 -0
- package/dist/scannerConfig.js +79 -0
- package/dist/scoring.d.ts +32 -0
- package/dist/scoring.js +367 -0
- package/dist/security-txt.d.ts +4 -0
- package/dist/security-txt.js +123 -0
- package/dist/surfaceEnrichment.d.ts +44 -0
- package/dist/surfaceEnrichment.js +377 -0
- package/dist/technology-detection.d.ts +4 -0
- package/dist/technology-detection.js +93 -0
- package/dist/types.d.ts +730 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +66 -0
- package/dist/wafFingerprint.d.ts +5 -0
- package/dist/wafFingerprint.js +156 -0
- package/examples/risk-events.mjs +27 -0
- package/examples/scan-url.mjs +17 -0
- package/package.json +102 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { getSiteDomain, unique } from "./utils.js";
|
|
2
|
+
const parseResource = (value, baseUrl) => {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(value, baseUrl);
|
|
5
|
+
return {
|
|
6
|
+
hostname: parsed.hostname.toLowerCase(),
|
|
7
|
+
pathname: parsed.pathname.toLowerCase(),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {
|
|
12
|
+
hostname: null,
|
|
13
|
+
pathname: value.toLowerCase(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const hostMatches = (hostname, domain) => {
|
|
18
|
+
if (!hostname) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return hostname === domain || hostname.endsWith(`.${domain}`);
|
|
22
|
+
};
|
|
23
|
+
const addDetectedTechnology = (target, seen, name, category, evidence, version, confidence = "medium", detection = "inferred") => {
|
|
24
|
+
const key = `${name}:${category}`;
|
|
25
|
+
if (seen.has(key)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
seen.add(key);
|
|
29
|
+
target.push({
|
|
30
|
+
name,
|
|
31
|
+
category,
|
|
32
|
+
evidence,
|
|
33
|
+
version: version || null,
|
|
34
|
+
confidence,
|
|
35
|
+
detection,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
export const AI_VENDOR_MATCHERS = [
|
|
39
|
+
{ name: "Intercom Fin", pattern: /intercom.*fin|fin ai|intercom/i, evidence: "Detected from Intercom-related assets or markup", category: "support_automation", confidence: "medium" },
|
|
40
|
+
{ name: "Drift", pattern: /drift\.com|driftt/i, evidence: "Detected from Drift assets or widget markup", category: "support_automation", confidence: "high" },
|
|
41
|
+
{ name: "Zendesk AI", pattern: /zendesk|zopim/i, evidence: "Detected from Zendesk widget assets or markup", category: "support_automation", confidence: "medium" },
|
|
42
|
+
{ name: "HubSpot Chat", pattern: /hubspot|hs-scripts/i, evidence: "Detected from HubSpot assets or chat markup", category: "support_automation", confidence: "medium" },
|
|
43
|
+
{ name: "Salesforce Einstein", pattern: /einstein|salesforce ai/i, evidence: "Detected from Salesforce or Einstein signals", category: "ai_vendor", confidence: "medium" },
|
|
44
|
+
{ name: "Crisp", pattern: /\$crisp|crisp\.chat|client\.crisp|go\.crisp|crisp-im/i, evidence: "Detected from Crisp widget assets or markup", category: "support_automation", confidence: "high" },
|
|
45
|
+
{ name: "Freshchat", pattern: /freshchat|freshworks/i, evidence: "Detected from Freshchat assets or markup", category: "support_automation", confidence: "high" },
|
|
46
|
+
{ name: "OpenAI", pattern: /openai/i, evidence: "Detected from OpenAI-related assets or markup", category: "ai_vendor", confidence: "high" },
|
|
47
|
+
{ name: "Anthropic", pattern: /anthropic|claude/i, evidence: "Detected from Anthropic-related assets or markup", category: "ai_vendor", confidence: "high" },
|
|
48
|
+
{ name: "Google Gemini", pattern: /gemini|generativelanguage|vertex ai/i, evidence: "Detected from Google AI-related assets or markup", category: "ai_vendor", confidence: "medium" },
|
|
49
|
+
{ name: "Microsoft Copilot", pattern: /\bmicrosoft copilot\b|copilot for|copilot studio|copilot.microsoft/i, evidence: "Detected from Copilot-specific assets or markup", category: "assistant_ui", confidence: "medium" },
|
|
50
|
+
];
|
|
51
|
+
export const THIRD_PARTY_PROVIDER_MATCHERS = [
|
|
52
|
+
{ pattern: /(google-analytics|googletagmanager|doubleclick|omtrdc|adobedtm|adobedc|analytics|plausible|matomo|segment|mixpanel|amplitude|heapanalytics|pendo|clarity\.ms|newrelic|nr-data|datadog)/, name: "Analytics / Telemetry", category: "analytics", risk: "medium", evidence: "Detected from third-party analytics, telemetry, or tag-management assets" },
|
|
53
|
+
{ pattern: /(onetrust|cookiebot|usercentrics)/, name: "Consent Management", category: "consent", risk: "low", evidence: "Detected from consent-management assets" },
|
|
54
|
+
{ pattern: /(intercom|drift|zendesk|zopim|hubspot|freshchat|crisp|sprinklr)/, name: "Support / Chat", category: "support", risk: "medium", evidence: "Detected from public support or chat tooling" },
|
|
55
|
+
{ pattern: /(openai|anthropic|gemini|vertex|copilot|wizdom\.ai)/, name: "AI / Assistant Vendor", category: "ai", risk: "high", evidence: "Detected from AI-related scripts, assets, or public assistant tooling" },
|
|
56
|
+
{ pattern: /(contentsquare|decibelinsight|hotjar|fullstory|medallia|logrocket|clarity\.ms)/, name: "Session Replay / Experience Analytics", category: "session_replay", risk: "high", evidence: "Detected from session-replay or detailed experience-analytics assets" },
|
|
57
|
+
{ pattern: /(braintree|paypal|cardinalcommerce|arcot|3dsecure|tsys|payment|payments)/, name: "Payments / Verification", category: "payments", risk: "medium", evidence: "Detected from payments or challenge-flow assets" },
|
|
58
|
+
{ pattern: /(facebook|twitter|linkedin|tiktok|pinterest|reddit|youtube|snapchat|instagram)/, name: "Social / Advertising", category: "social", risk: "medium", evidence: "Detected from social, embedded media, or advertising assets" },
|
|
59
|
+
{ pattern: /(ads|adservice|amazon-adsystem|smartadserver|pubmatic|gumgum|teads|casalemedia|openx|lijit|bidswitch)/, name: "Advertising", category: "ads", risk: "high", evidence: "Detected from advertising or programmatic asset domains" },
|
|
60
|
+
{ pattern: /(cloudfront|fastly|akamai|cloudflare|jsdelivr|cdnjs)/, name: "CDN / Delivery", category: "cdn", risk: "low", evidence: "Detected from CDN or static-delivery domains" },
|
|
61
|
+
{ pattern: /(imperva|incapsula|sucuri|sentry)/, name: "Security / Monitoring", category: "security", risk: "low", evidence: "Detected from security, edge-protection, or monitoring assets" },
|
|
62
|
+
];
|
|
63
|
+
export const detectHtmlTechnologies = (html, finalUrl, metaGenerator, externalScriptUrls, externalStylesheetUrls) => {
|
|
64
|
+
const technologies = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const htmlLower = html.toLowerCase();
|
|
67
|
+
const resources = [...externalScriptUrls, ...externalStylesheetUrls].map((url) => parseResource(url, finalUrl));
|
|
68
|
+
const allPaths = resources.map((resource) => resource.pathname);
|
|
69
|
+
const hasDomain = (domain) => resources.some((resource) => hostMatches(resource.hostname, domain));
|
|
70
|
+
const hasPath = (needle) => allPaths.some((path) => path.includes(needle));
|
|
71
|
+
const generator = metaGenerator?.toLowerCase() || "";
|
|
72
|
+
if (generator.includes("wordpress") || htmlLower.includes("/wp-content/") || htmlLower.includes("/wp-includes/")) {
|
|
73
|
+
addDetectedTechnology(technologies, seen, "WordPress", "frontend", "Detected from meta generator or wp-content assets");
|
|
74
|
+
}
|
|
75
|
+
if (generator.includes("drupal") || htmlLower.includes("drupalsettings") || htmlLower.includes("/sites/default/files/")) {
|
|
76
|
+
addDetectedTechnology(technologies, seen, "Drupal", "frontend", "Detected from Drupal page markers");
|
|
77
|
+
}
|
|
78
|
+
if (generator.includes("joomla")) {
|
|
79
|
+
addDetectedTechnology(technologies, seen, "Joomla", "frontend", "Detected from meta generator");
|
|
80
|
+
}
|
|
81
|
+
if (generator.includes("ghost")) {
|
|
82
|
+
addDetectedTechnology(technologies, seen, "Ghost", "frontend", "Detected from meta generator");
|
|
83
|
+
}
|
|
84
|
+
if (generator.includes("webflow") || hasDomain("webflow.com")) {
|
|
85
|
+
addDetectedTechnology(technologies, seen, "Webflow", "hosting", "Detected from Webflow assets or generator");
|
|
86
|
+
}
|
|
87
|
+
if (generator.includes("wix") || hasDomain("wixstatic.com")) {
|
|
88
|
+
addDetectedTechnology(technologies, seen, "Wix", "hosting", "Detected from Wix assets or generator");
|
|
89
|
+
}
|
|
90
|
+
if (hasDomain("static1.squarespace.com") || generator.includes("squarespace")) {
|
|
91
|
+
addDetectedTechnology(technologies, seen, "Squarespace", "hosting", "Detected from Squarespace assets or generator");
|
|
92
|
+
}
|
|
93
|
+
if (htmlLower.includes("/_next/") || htmlLower.includes("__next_data__")) {
|
|
94
|
+
addDetectedTechnology(technologies, seen, "Next.js", "frontend", "Detected from Next.js page assets");
|
|
95
|
+
}
|
|
96
|
+
if (htmlLower.includes("/_nuxt/") || htmlLower.includes("__nuxt")) {
|
|
97
|
+
addDetectedTechnology(technologies, seen, "Nuxt", "frontend", "Detected from Nuxt page assets");
|
|
98
|
+
}
|
|
99
|
+
if (hasDomain("cdn.shopify.com") || htmlLower.includes("shopify.theme")) {
|
|
100
|
+
addDetectedTechnology(technologies, seen, "Shopify", "hosting", "Detected from Shopify assets");
|
|
101
|
+
}
|
|
102
|
+
if (hasDomain("code.jquery.com") || htmlLower.includes("jquery")) {
|
|
103
|
+
addDetectedTechnology(technologies, seen, "jQuery", "frontend", "Detected from jQuery asset references");
|
|
104
|
+
}
|
|
105
|
+
if (hasDomain("googletagmanager.com")) {
|
|
106
|
+
addDetectedTechnology(technologies, seen, "Google Tag Manager", "network", "Detected from third-party script domains");
|
|
107
|
+
}
|
|
108
|
+
if (hasDomain("google-analytics.com") || hasPath("gtag/js")) {
|
|
109
|
+
addDetectedTechnology(technologies, seen, "Google Analytics", "network", "Detected from analytics asset references");
|
|
110
|
+
}
|
|
111
|
+
if (hasDomain("plausible.io")) {
|
|
112
|
+
addDetectedTechnology(technologies, seen, "Plausible Analytics", "network", "Detected from analytics asset references");
|
|
113
|
+
}
|
|
114
|
+
if (hasDomain("matomo.cloud") || hasDomain("matomo.org") || hasPath("matomo.js")) {
|
|
115
|
+
addDetectedTechnology(technologies, seen, "Matomo", "network", "Detected from analytics asset references");
|
|
116
|
+
}
|
|
117
|
+
if (hasDomain("segment.io") || hasDomain("segment.com") || hasDomain("segmentcdn.com")) {
|
|
118
|
+
addDetectedTechnology(technologies, seen, "Segment", "network", "Detected from customer-data platform assets");
|
|
119
|
+
}
|
|
120
|
+
if (hasDomain("mixpanel.com")) {
|
|
121
|
+
addDetectedTechnology(technologies, seen, "Mixpanel", "network", "Detected from product analytics assets");
|
|
122
|
+
}
|
|
123
|
+
if (hasDomain("amplitude.com")) {
|
|
124
|
+
addDetectedTechnology(technologies, seen, "Amplitude", "network", "Detected from product analytics assets");
|
|
125
|
+
}
|
|
126
|
+
if (hasDomain("heapanalytics.com")) {
|
|
127
|
+
addDetectedTechnology(technologies, seen, "Heap", "network", "Detected from product analytics assets");
|
|
128
|
+
}
|
|
129
|
+
if (hasDomain("clarity.ms")) {
|
|
130
|
+
addDetectedTechnology(technologies, seen, "Microsoft Clarity", "network", "Detected from session analytics assets");
|
|
131
|
+
}
|
|
132
|
+
if (hasDomain("logrocket.com")) {
|
|
133
|
+
addDetectedTechnology(technologies, seen, "LogRocket", "network", "Detected from session replay assets");
|
|
134
|
+
}
|
|
135
|
+
if (hasDomain("pendo.io")) {
|
|
136
|
+
addDetectedTechnology(technologies, seen, "Pendo", "network", "Detected from product analytics assets");
|
|
137
|
+
}
|
|
138
|
+
if (hasDomain("newrelic.com") || hasDomain("nr-data.net")) {
|
|
139
|
+
addDetectedTechnology(technologies, seen, "New Relic Browser", "network", "Detected from client telemetry assets");
|
|
140
|
+
}
|
|
141
|
+
if (hasDomain("datadoghq-browser-agent.com") || hasDomain("browser-intake-datadoghq.com")) {
|
|
142
|
+
addDetectedTechnology(technologies, seen, "Datadog RUM", "network", "Detected from client telemetry assets");
|
|
143
|
+
}
|
|
144
|
+
if (hasDomain("app.usercentrics.eu")) {
|
|
145
|
+
addDetectedTechnology(technologies, seen, "Usercentrics", "security", "Detected from consent-management script");
|
|
146
|
+
}
|
|
147
|
+
if (hasDomain("consent.cookiebot.com")) {
|
|
148
|
+
addDetectedTechnology(technologies, seen, "Cookiebot", "security", "Detected from consent-management script");
|
|
149
|
+
}
|
|
150
|
+
if (hasDomain("js.hs-scripts.com")) {
|
|
151
|
+
addDetectedTechnology(technologies, seen, "HubSpot", "network", "Detected from HubSpot script references");
|
|
152
|
+
}
|
|
153
|
+
if (hasDomain("adobedtm.com") || hasDomain("adobedc.net")) {
|
|
154
|
+
addDetectedTechnology(technologies, seen, "Adobe Experience Cloud", "network", "Detected from Adobe tag or delivery assets");
|
|
155
|
+
}
|
|
156
|
+
if (hasDomain("contentsquare.com") || hasDomain("decibelinsight.net")) {
|
|
157
|
+
addDetectedTechnology(technologies, seen, "Contentsquare / Decibel", "network", "Detected from session analytics assets");
|
|
158
|
+
}
|
|
159
|
+
if (hasDomain("imperva.com") || hasDomain("incapsula.com")) {
|
|
160
|
+
addDetectedTechnology(technologies, seen, "Imperva", "security", "Detected from Imperva / Incapsula assets");
|
|
161
|
+
}
|
|
162
|
+
if (hasDomain("onetrust.com") || hasDomain("cookielaw.org")) {
|
|
163
|
+
addDetectedTechnology(technologies, seen, "OneTrust", "security", "Detected from OneTrust consent assets");
|
|
164
|
+
}
|
|
165
|
+
if (hasDomain("braintreegateway.com")) {
|
|
166
|
+
addDetectedTechnology(technologies, seen, "Braintree", "security", "Detected from payments-related assets");
|
|
167
|
+
}
|
|
168
|
+
if (hasDomain("sentry.io")) {
|
|
169
|
+
addDetectedTechnology(technologies, seen, "Sentry", "security", "Detected from client monitoring assets");
|
|
170
|
+
}
|
|
171
|
+
if (hasDomain("cloudfront.net")) {
|
|
172
|
+
addDetectedTechnology(technologies, seen, "Amazon CloudFront", "network", "Detected from asset hosting domain");
|
|
173
|
+
}
|
|
174
|
+
if (finalUrl.hostname.endsWith(".pages.dev")) {
|
|
175
|
+
addDetectedTechnology(technologies, seen, "Cloudflare Pages", "hosting", "Derived from final hostname", null, "low", "inferred");
|
|
176
|
+
}
|
|
177
|
+
return technologies;
|
|
178
|
+
};
|
|
179
|
+
export const analyzeAiSurface = (html, externalScriptUrls, firstPartyPaths) => {
|
|
180
|
+
const htmlLower = html.toLowerCase();
|
|
181
|
+
const vendors = [];
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
const addVendor = (name, evidence, category, confidence) => {
|
|
184
|
+
const key = `${name}:${category}`;
|
|
185
|
+
if (seen.has(key)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
seen.add(key);
|
|
189
|
+
vendors.push({ name, evidence, category, confidence });
|
|
190
|
+
};
|
|
191
|
+
const combinedSignals = `${htmlLower} ${externalScriptUrls.join(" ").toLowerCase()}`;
|
|
192
|
+
for (const matcher of AI_VENDOR_MATCHERS) {
|
|
193
|
+
if (matcher.pattern.test(combinedSignals)) {
|
|
194
|
+
addVendor(matcher.name, matcher.evidence, matcher.category, matcher.confidence);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const assistantPhrases = [
|
|
198
|
+
"chat with ai",
|
|
199
|
+
"chat with our ai",
|
|
200
|
+
"ask ai",
|
|
201
|
+
"ai assistant",
|
|
202
|
+
"virtual assistant",
|
|
203
|
+
"talk to our assistant",
|
|
204
|
+
"assistant for",
|
|
205
|
+
];
|
|
206
|
+
const assistantVisible = assistantPhrases.some((phrase) => htmlLower.includes(phrase));
|
|
207
|
+
const aiPageSignals = firstPartyPaths.filter((path) => /\/(ai|assistant|copilot|chat|ask-ai|automation)(\/|$)/i.test(path));
|
|
208
|
+
const disclosures = [];
|
|
209
|
+
const privacySignals = [];
|
|
210
|
+
const governanceSignals = [];
|
|
211
|
+
if (/do not share sensitive|may be inaccurate|ai-generated|generative ai|assistant may/i.test(htmlLower)) {
|
|
212
|
+
disclosures.push("The page appears to include AI usage or safety disclosure language.");
|
|
213
|
+
}
|
|
214
|
+
if (/privacy policy/i.test(htmlLower) && /ai/i.test(htmlLower)) {
|
|
215
|
+
disclosures.push("AI-related language appears alongside privacy-policy content.");
|
|
216
|
+
}
|
|
217
|
+
if (/do not share personal|do not enter personal|do not submit sensitive|avoid sharing confidential/i.test(htmlLower)) {
|
|
218
|
+
privacySignals.push("The page appears to warn users not to enter sensitive or personal data.");
|
|
219
|
+
}
|
|
220
|
+
if (/data may be used to improve|used to train|retained for|stored to improve/i.test(htmlLower)) {
|
|
221
|
+
privacySignals.push("The page appears to disclose AI-related retention or model-improvement language.");
|
|
222
|
+
}
|
|
223
|
+
if (/human review|reviewed by humans|monitored for quality/i.test(htmlLower)) {
|
|
224
|
+
governanceSignals.push("The page appears to disclose human review or quality-monitoring language.");
|
|
225
|
+
}
|
|
226
|
+
if (/terms of use|acceptable use|responsible ai|ai principles/i.test(htmlLower) && /ai/i.test(htmlLower)) {
|
|
227
|
+
governanceSignals.push("The page appears to reference AI governance or acceptable-use language.");
|
|
228
|
+
}
|
|
229
|
+
const issues = [];
|
|
230
|
+
const strengths = [];
|
|
231
|
+
const automationOnly = vendors.length > 0 && vendors.every((vendor) => vendor.category === "support_automation");
|
|
232
|
+
const highConfidenceAiSignals = assistantVisible ||
|
|
233
|
+
vendors.some((vendor) => vendor.category === "ai_vendor" && vendor.confidence === "high") ||
|
|
234
|
+
aiPageSignals.length > 0;
|
|
235
|
+
if (assistantVisible || vendors.length || aiPageSignals.length) {
|
|
236
|
+
strengths.push(automationOnly && !assistantVisible && !aiPageSignals.length
|
|
237
|
+
? "Public-facing support automation signals were detected passively."
|
|
238
|
+
: "Public-facing AI or automation signals were detected passively.");
|
|
239
|
+
}
|
|
240
|
+
if (highConfidenceAiSignals && !disclosures.length) {
|
|
241
|
+
issues.push("AI-related signals were detected, but no obvious AI disclosure language was found on the fetched page.");
|
|
242
|
+
}
|
|
243
|
+
else if (automationOnly && !disclosures.length) {
|
|
244
|
+
issues.push("Support automation signals were detected, but no obvious disclosure language was found on the fetched page.");
|
|
245
|
+
}
|
|
246
|
+
if (highConfidenceAiSignals && !privacySignals.length) {
|
|
247
|
+
issues.push("AI-related signals were detected, but no obvious data-handling or privacy guidance was found on the fetched page.");
|
|
248
|
+
}
|
|
249
|
+
if (privacySignals.length) {
|
|
250
|
+
strengths.push("AI-related privacy guidance appears to be visible on the fetched page.");
|
|
251
|
+
}
|
|
252
|
+
if (governanceSignals.length) {
|
|
253
|
+
strengths.push("AI governance or human-review language appears to be visible on the fetched page.");
|
|
254
|
+
}
|
|
255
|
+
if (!assistantVisible && !vendors.length && !aiPageSignals.length) {
|
|
256
|
+
strengths.push("No obvious public-facing AI assistant or automation surface was detected on the fetched page.");
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
detected: Boolean(assistantVisible || vendors.length || aiPageSignals.length),
|
|
260
|
+
assistantVisible,
|
|
261
|
+
aiPageSignals,
|
|
262
|
+
vendors,
|
|
263
|
+
discoveredPaths: aiPageSignals,
|
|
264
|
+
disclosures,
|
|
265
|
+
privacySignals,
|
|
266
|
+
governanceSignals,
|
|
267
|
+
issues,
|
|
268
|
+
strengths,
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
const classifyThirdPartyProvider = (domain) => {
|
|
272
|
+
const lower = domain.toLowerCase();
|
|
273
|
+
const match = THIRD_PARTY_PROVIDER_MATCHERS.find((provider) => provider.pattern.test(lower));
|
|
274
|
+
if (match) {
|
|
275
|
+
return {
|
|
276
|
+
name: match.name,
|
|
277
|
+
category: match.category,
|
|
278
|
+
risk: match.risk,
|
|
279
|
+
evidence: match.evidence,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
name: domain,
|
|
284
|
+
category: "other",
|
|
285
|
+
risk: "medium",
|
|
286
|
+
evidence: "Detected from third-party assets loaded by the page",
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
export const analyzeThirdPartyTrust = (finalUrl, htmlSecurity, aiSurface) => {
|
|
290
|
+
const siteDomain = getSiteDomain(finalUrl.hostname);
|
|
291
|
+
const thirdPartyDomains = unique([
|
|
292
|
+
...(htmlSecurity.externalScriptDomains || []),
|
|
293
|
+
...(htmlSecurity.externalStylesheetDomains || []),
|
|
294
|
+
]).filter((domain) => domain && getSiteDomain(domain) !== siteDomain);
|
|
295
|
+
const providers = thirdPartyDomains.map((domain) => ({
|
|
296
|
+
domain,
|
|
297
|
+
...classifyThirdPartyProvider(domain),
|
|
298
|
+
}));
|
|
299
|
+
const highRiskProviders = providers.filter((provider) => provider.risk === "high").length;
|
|
300
|
+
const issues = [];
|
|
301
|
+
const strengths = [];
|
|
302
|
+
if (highRiskProviders >= 3) {
|
|
303
|
+
issues.push("The page relies on several high-trust or high-observability third parties, which expands data exposure and review scope.");
|
|
304
|
+
}
|
|
305
|
+
else if (highRiskProviders > 0) {
|
|
306
|
+
issues.push("The page includes high-trust third-party providers that deserve explicit review and ownership.");
|
|
307
|
+
}
|
|
308
|
+
if ((htmlSecurity.missingSriScriptUrls || []).length > 0) {
|
|
309
|
+
issues.push("Some third-party scripts are loaded without Subresource Integrity.");
|
|
310
|
+
}
|
|
311
|
+
if (providers.some((provider) => provider.category === "session_replay")) {
|
|
312
|
+
issues.push("Session replay or experience analytics tooling appears to be present.");
|
|
313
|
+
}
|
|
314
|
+
if (providers.some((provider) => provider.category === "ai") && !aiSurface.disclosures.length) {
|
|
315
|
+
issues.push("AI-related third-party tooling appears present without obvious on-page disclosure language.");
|
|
316
|
+
}
|
|
317
|
+
if (providers.some((provider) => provider.category === "consent")) {
|
|
318
|
+
strengths.push("A consent-management provider appears to be present.");
|
|
319
|
+
}
|
|
320
|
+
if (providers.length > 0 && highRiskProviders === 0) {
|
|
321
|
+
strengths.push("Third-party footprint appears present but mostly concentrated in lower-risk delivery, monitoring, or consent tooling.");
|
|
322
|
+
}
|
|
323
|
+
if (!providers.length) {
|
|
324
|
+
strengths.push("No obvious third-party script or stylesheet domains were detected on the fetched page.");
|
|
325
|
+
}
|
|
326
|
+
const summary = !providers.length
|
|
327
|
+
? "Minimal visible third-party footprint on the fetched page."
|
|
328
|
+
: highRiskProviders > 0
|
|
329
|
+
? "The page depends on several third-party providers that increase trust and data-flow complexity."
|
|
330
|
+
: "The page uses third-party providers, but the visible footprint is weighted more toward delivery and operational tooling.";
|
|
331
|
+
return {
|
|
332
|
+
totalProviders: providers.length,
|
|
333
|
+
highRiskProviders,
|
|
334
|
+
providers,
|
|
335
|
+
issues,
|
|
336
|
+
strengths,
|
|
337
|
+
summary,
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
export const buildExecutiveSummary = (result) => {
|
|
341
|
+
const headerWeaknessCount = result.headers.filter((header) => header.status === "missing" || header.status === "warning").length;
|
|
342
|
+
const highRiskThirdParties = result.thirdPartyTrust.highRiskProviders;
|
|
343
|
+
const domainTrustIssueCount = result.domainSecurity.issues.length + result.publicSignals.issues.length;
|
|
344
|
+
const aiIssueCount = result.aiSurface.issues.length;
|
|
345
|
+
const trainingSurfaceDetected = result.htmlSecurity.issues.includes("Page content suggests an intentionally vulnerable training or challenge surface.");
|
|
346
|
+
const browserRiskWeight = headerWeaknessCount * 2;
|
|
347
|
+
const domainRiskWeight = domainTrustIssueCount;
|
|
348
|
+
const thirdPartyRiskWeight = highRiskThirdParties * 3 + result.thirdPartyTrust.issues.length;
|
|
349
|
+
const aiRiskWeight = aiIssueCount * 3 + result.aiSurface.disclosures.length;
|
|
350
|
+
const posture = result.score >= 80 ? "strong" : result.score >= 60 ? "mixed" : "weak";
|
|
351
|
+
let mainRisk = "Browser-layer hardening gaps are the main visible risk.";
|
|
352
|
+
if (result.assessmentLimitation.limited && result.assessmentLimitation.kind === "service_unavailable") {
|
|
353
|
+
mainRisk = "Availability or reachability issues prevented a normal posture read.";
|
|
354
|
+
}
|
|
355
|
+
else if (result.assessmentLimitation.limited && result.assessmentLimitation.kind) {
|
|
356
|
+
mainRisk = "Transport trust or access controls prevented a normal posture read.";
|
|
357
|
+
}
|
|
358
|
+
else if (thirdPartyRiskWeight > browserRiskWeight && thirdPartyRiskWeight >= domainRiskWeight) {
|
|
359
|
+
mainRisk = "Third-party trust and data-flow sprawl are the main visible risk.";
|
|
360
|
+
}
|
|
361
|
+
else if (aiRiskWeight > browserRiskWeight && aiRiskWeight >= domainRiskWeight && result.aiSurface.detected) {
|
|
362
|
+
mainRisk = "Public AI or automation signals are visible without much supporting disclosure or privacy guidance.";
|
|
363
|
+
}
|
|
364
|
+
else if (domainRiskWeight > browserRiskWeight) {
|
|
365
|
+
mainRisk = "Public trust and domain hygiene signals need attention alongside the web posture.";
|
|
366
|
+
}
|
|
367
|
+
const takeawayCandidates = [
|
|
368
|
+
trainingSurfaceDetected
|
|
369
|
+
? {
|
|
370
|
+
weight: 90,
|
|
371
|
+
text: "This target appears to be an intentionally vulnerable lab or training surface, so read the grade as posture-only context rather than a business-risk verdict.",
|
|
372
|
+
}
|
|
373
|
+
: null,
|
|
374
|
+
result.assessmentLimitation.limited && result.assessmentLimitation.detail
|
|
375
|
+
? { weight: 100, text: result.assessmentLimitation.detail }
|
|
376
|
+
: null,
|
|
377
|
+
headerWeaknessCount > 0
|
|
378
|
+
? {
|
|
379
|
+
weight: browserRiskWeight || 1,
|
|
380
|
+
text: `${headerWeaknessCount} browser-facing protection${headerWeaknessCount === 1 ? " is" : "s are"} missing or weak on the scanned response.`,
|
|
381
|
+
}
|
|
382
|
+
: {
|
|
383
|
+
weight: 1,
|
|
384
|
+
text: "Core browser-facing protections look consistently present on the scanned response.",
|
|
385
|
+
},
|
|
386
|
+
domainTrustIssueCount > 0
|
|
387
|
+
? {
|
|
388
|
+
weight: domainRiskWeight || 1,
|
|
389
|
+
text: `${domainTrustIssueCount} domain, disclosure, or public-trust signal${domainTrustIssueCount === 1 ? " needs" : "s need"} attention.`,
|
|
390
|
+
}
|
|
391
|
+
: null,
|
|
392
|
+
result.thirdPartyTrust.totalProviders > 0
|
|
393
|
+
? {
|
|
394
|
+
weight: thirdPartyRiskWeight || 1,
|
|
395
|
+
text: `${result.thirdPartyTrust.totalProviders} third-party provider${result.thirdPartyTrust.totalProviders === 1 ? " was" : "s were"} detected, including ${highRiskThirdParties} higher-risk integration${highRiskThirdParties === 1 ? "" : "s"}.`,
|
|
396
|
+
}
|
|
397
|
+
: {
|
|
398
|
+
weight: 1,
|
|
399
|
+
text: "No obvious third-party script or stylesheet providers were detected on the fetched page.",
|
|
400
|
+
},
|
|
401
|
+
result.aiSurface.detected
|
|
402
|
+
? {
|
|
403
|
+
weight: aiRiskWeight || 1,
|
|
404
|
+
text: `${result.aiSurface.vendors.length || result.aiSurface.discoveredPaths.length} public AI or automation signal${(result.aiSurface.vendors.length || result.aiSurface.discoveredPaths.length) === 1 ? " was" : "s were"} detected.`,
|
|
405
|
+
}
|
|
406
|
+
: {
|
|
407
|
+
weight: 1,
|
|
408
|
+
text: "No obvious public-facing AI or automation surface was detected.",
|
|
409
|
+
},
|
|
410
|
+
]
|
|
411
|
+
.filter((item) => Boolean(item))
|
|
412
|
+
.sort((left, right) => right.weight - left.weight);
|
|
413
|
+
const takeaways = takeawayCandidates
|
|
414
|
+
.map((item) => item.text)
|
|
415
|
+
.filter((text, index, items) => items.indexOf(text) === index)
|
|
416
|
+
.slice(0, 3);
|
|
417
|
+
const overview = result.assessmentLimitation.limited
|
|
418
|
+
? result.assessmentLimitation.kind === "service_unavailable"
|
|
419
|
+
? "The scanner could not obtain a stable response from the target, so this assessment is only a limited availability read."
|
|
420
|
+
: "The scanner could not establish a normal trusted read of the target, so this assessment is only a partial posture view."
|
|
421
|
+
: trainingSurfaceDetected
|
|
422
|
+
? "The target appears to be an intentionally vulnerable lab or training surface, so this assessment should be read as passive posture context rather than a normal business-risk verdict."
|
|
423
|
+
: posture === "strong"
|
|
424
|
+
? "External posture looks broadly solid, with only a few areas that still deserve tuning."
|
|
425
|
+
: posture === "mixed"
|
|
426
|
+
? "External posture looks operationally mature in places, but the report still shows several areas that need tightening."
|
|
427
|
+
: "External posture shows multiple weaknesses that make the site look less well hardened than a mature public-facing platform should.";
|
|
428
|
+
return {
|
|
429
|
+
overview,
|
|
430
|
+
mainRisk,
|
|
431
|
+
posture,
|
|
432
|
+
takeaways: takeaways.filter((item) => Boolean(item)),
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
export const mergeTechnologies = (...groups) => {
|
|
436
|
+
const merged = [];
|
|
437
|
+
const byKey = new Map();
|
|
438
|
+
const confidenceRank = { high: 3, medium: 2, low: 1 };
|
|
439
|
+
for (const group of groups) {
|
|
440
|
+
for (const technology of group || []) {
|
|
441
|
+
const key = `${technology.name}:${technology.category}`;
|
|
442
|
+
const existing = byKey.get(key);
|
|
443
|
+
if (!existing) {
|
|
444
|
+
byKey.set(key, technology);
|
|
445
|
+
merged.push(technology);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const existingScore = confidenceRank[existing.confidence] + (existing.detection === "observed" ? 10 : 0);
|
|
449
|
+
const nextScore = confidenceRank[technology.confidence] + (technology.detection === "observed" ? 10 : 0);
|
|
450
|
+
if (nextScore > existingScore) {
|
|
451
|
+
const index = merged.indexOf(existing);
|
|
452
|
+
if (index >= 0) {
|
|
453
|
+
merged[index] = technology;
|
|
454
|
+
}
|
|
455
|
+
byKey.set(key, technology);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return merged;
|
|
460
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { URL } from "node:url";
|
|
2
|
+
import type { CtDiscoveryInfo, HtmlSecurityInfo, IdentityProviderInfo, RedirectHop } from "./types.js";
|
|
3
|
+
interface JsonResponse<T = unknown> {
|
|
4
|
+
statusCode: number;
|
|
5
|
+
json: T | null;
|
|
6
|
+
}
|
|
7
|
+
type RequestJsonFn = (targetUrl: URL, extraHeaders?: Record<string, string>) => Promise<JsonResponse>;
|
|
8
|
+
export declare const IDENTITY_PROVIDER_PATTERNS: {
|
|
9
|
+
provider: string;
|
|
10
|
+
pattern: RegExp;
|
|
11
|
+
}[];
|
|
12
|
+
export declare const detectIdentityProviderName: (candidates: string[]) => string;
|
|
13
|
+
export declare const analyzeIdentityProvider: (finalUrl: URL, redirects: RedirectHop[], htmlSecurity: HtmlSecurityInfo, html: string | null, requestJson: RequestJsonFn, ctDiscovery?: CtDiscoveryInfo) => Promise<IdentityProviderInfo>;
|
|
14
|
+
export {};
|