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,259 @@
1
+ import { URL } from "node:url";
2
+ import { DISCOVERY_PATH_LIMIT, OIDC_DISCOVERY_TIMEOUT_MS, SUMMARY_EVIDENCE_LIMIT } from "./scannerConfig.js";
3
+ import { unique, withTimeout } from "./utils.js";
4
+ const AUTH_HOST_LIMIT = 5;
5
+ export const IDENTITY_PROVIDER_PATTERNS = [
6
+ { provider: "Microsoft Entra ID", pattern: /(^|\.)login\.microsoftonline\.com$/i },
7
+ { provider: "Okta", pattern: /(^|\.)okta(?:-emea)?\.com$/i },
8
+ { provider: "Auth0", pattern: /(^|\.)auth0\.com$/i },
9
+ { provider: "Ping Identity", pattern: /(^|\.)ping(?:one|identity)\.com$/i },
10
+ { provider: "OneLogin", pattern: /(^|\.)onelogin\.com$/i },
11
+ { provider: "Amazon Cognito", pattern: /amazoncognito\.com$/i },
12
+ { provider: "Google Identity", pattern: /(^|\.)accounts\.google\.com$/i },
13
+ { provider: "Keycloak", pattern: /keycloak/i },
14
+ ];
15
+ export const detectIdentityProviderName = (candidates) => {
16
+ for (const candidate of candidates) {
17
+ for (const entry of IDENTITY_PROVIDER_PATTERNS) {
18
+ if (entry.pattern.test(candidate)) {
19
+ return entry.provider;
20
+ }
21
+ }
22
+ }
23
+ return null;
24
+ };
25
+ const inferProtocol = ({ openIdConfigurationUrl, authorizationEndpoint, tokenEndpoint, redirectOrigins, redirectUriSignals, loginPaths, }) => {
26
+ if (openIdConfigurationUrl || authorizationEndpoint || tokenEndpoint) {
27
+ return "oidc";
28
+ }
29
+ if (redirectOrigins.some((origin) => /saml|adfs/i.test(origin))) {
30
+ return "saml";
31
+ }
32
+ if (redirectOrigins.length ||
33
+ redirectUriSignals.length ||
34
+ loginPaths.some((path) => /oauth|authorize|sso|auth/i.test(path))) {
35
+ return "oauth";
36
+ }
37
+ return null;
38
+ };
39
+ const collectRedirectUriSignals = (html, finalUrl) => {
40
+ const signals = [];
41
+ const matches = [...html.matchAll(/(?:redirect_uri|post_logout_redirect_uri)=([^"'`\s<>()&]+)/gi)];
42
+ for (const match of matches) {
43
+ try {
44
+ const decoded = decodeURIComponent(match[1]);
45
+ const redirectUrl = new URL(decoded, finalUrl);
46
+ if (redirectUrl.protocol === "http:" ||
47
+ redirectUrl.hostname === "localhost" ||
48
+ redirectUrl.hostname.endsWith(".localhost") ||
49
+ redirectUrl.origin !== finalUrl.origin) {
50
+ signals.push(redirectUrl.toString());
51
+ }
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ }
57
+ return unique(signals).slice(0, SUMMARY_EVIDENCE_LIMIT);
58
+ };
59
+ const deriveOpenIdCandidates = (finalUrl, redirects, htmlSecurity, authHostCandidates) => {
60
+ const candidates = [new URL("/.well-known/openid-configuration", finalUrl.origin).toString()];
61
+ if (/login\.microsoftonline\.com$/i.test(finalUrl.hostname)) {
62
+ candidates.push(new URL("/common/v2.0/.well-known/openid-configuration", finalUrl.origin).toString());
63
+ }
64
+ for (const host of authHostCandidates) {
65
+ if (host !== finalUrl.hostname) {
66
+ candidates.push(new URL("/.well-known/openid-configuration", `https://${host}`).toString());
67
+ }
68
+ }
69
+ const loginPaths = [
70
+ ...redirects
71
+ .map((hop) => hop.location)
72
+ .filter((location) => Boolean(location)),
73
+ ...htmlSecurity.firstPartyPaths.filter((path) => /login|signin|oauth|authorize|sso|auth/i.test(path)),
74
+ ];
75
+ for (const value of loginPaths) {
76
+ try {
77
+ const resolved = new URL(value, finalUrl);
78
+ const pathname = resolved.pathname;
79
+ if (/\/oauth2\/[^/]+\/v1\/authorize/i.test(pathname)) {
80
+ const issuerPath = pathname.replace(/\/v1\/authorize.*$/i, "");
81
+ candidates.push(new URL(`${issuerPath}/.well-known/openid-configuration`, resolved.origin).toString());
82
+ }
83
+ else if (/\/authorize/i.test(pathname)) {
84
+ const issuerPath = pathname.replace(/\/authorize.*$/i, "");
85
+ candidates.push(new URL(`${issuerPath}/.well-known/openid-configuration`, resolved.origin).toString());
86
+ }
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ }
92
+ return unique(candidates);
93
+ };
94
+ const deriveAuthHostCandidates = (finalUrl, redirects, htmlSecurity, ctDiscovery) => unique([
95
+ finalUrl.hostname,
96
+ ...redirects
97
+ .map((hop) => hop.location)
98
+ .filter((location) => Boolean(location))
99
+ .map((location) => {
100
+ try {
101
+ return new URL(location, finalUrl).hostname;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }),
107
+ ...htmlSecurity.externalScriptDomains.filter((hostname) => /auth|login|okta|auth0|onelogin|microsoftonline|ping|cognito/i.test(hostname) ||
108
+ /(^|\.)accounts\.google\.com$/i.test(hostname)),
109
+ ...htmlSecurity.firstPartyPaths
110
+ .filter((path) => /login|signin|oauth|authorize|sso|auth/i.test(path))
111
+ .map((path) => {
112
+ try {
113
+ return new URL(path, finalUrl).hostname;
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }),
119
+ ...(ctDiscovery?.prioritizedHosts || [])
120
+ .filter((entry) => entry.category === "auth" || entry.priority === "high")
121
+ .map((entry) => entry.host),
122
+ ]).slice(0, AUTH_HOST_LIMIT);
123
+ const extractTenantSignals = (provider, issuer, metadata) => {
124
+ if (provider !== "Microsoft Entra ID") {
125
+ return {
126
+ tenantBrand: null,
127
+ tenantRegion: null,
128
+ tenantSignals: [],
129
+ };
130
+ }
131
+ const signals = unique([
132
+ issuer && /\/[0-9a-f-]{36}\//i.test(issuer) ? "Issuer exposes a tenant-specific GUID." : null,
133
+ metadata?.tenant_region_scope ? `Tenant region scope: ${metadata.tenant_region_scope}` : null,
134
+ metadata?.cloud_instance_name ? `Cloud instance: ${metadata.cloud_instance_name}` : null,
135
+ metadata?.tenant_region_sub_scope ? `Tenant region sub-scope: ${metadata.tenant_region_sub_scope}` : null,
136
+ ]);
137
+ return {
138
+ tenantBrand: metadata?.cloud_instance_name || "Microsoft Entra ID",
139
+ tenantRegion: metadata?.tenant_region_scope || metadata?.tenant_region_sub_scope || null,
140
+ tenantSignals: signals,
141
+ };
142
+ };
143
+ export const analyzeIdentityProvider = async (finalUrl, redirects, htmlSecurity, html, requestJson, ctDiscovery) => {
144
+ const redirectOrigins = unique(redirects
145
+ .map((hop) => hop.location)
146
+ .filter((location) => Boolean(location))
147
+ .map((location) => {
148
+ try {
149
+ const resolved = new URL(location, finalUrl);
150
+ const looksAuthRelated = resolved.origin !== finalUrl.origin ||
151
+ /login|signin|oauth|authorize|sso|auth|adfs|saml/i.test(resolved.pathname) ||
152
+ detectIdentityProviderName([resolved.hostname]) !== null;
153
+ return looksAuthRelated ? resolved.origin : null;
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ }));
159
+ const redirectHosts = redirectOrigins.map((origin) => new URL(origin).hostname);
160
+ const dedicatedRedirectOrigins = redirectOrigins.filter((origin) => origin !== finalUrl.origin);
161
+ const loginPaths = unique(htmlSecurity.firstPartyPaths.filter((path) => /login|signin|oauth|authorize|sso|auth/i.test(path))).slice(0, DISCOVERY_PATH_LIMIT);
162
+ const authHostCandidates = deriveAuthHostCandidates(finalUrl, redirects, htmlSecurity, ctDiscovery);
163
+ const provider = detectIdentityProviderName([
164
+ finalUrl.hostname,
165
+ ...redirectHosts,
166
+ ...authHostCandidates,
167
+ ...htmlSecurity.externalScriptDomains,
168
+ ...htmlSecurity.externalStylesheetDomains,
169
+ ...htmlSecurity.aiSurface.discoveredPaths,
170
+ ]);
171
+ const redirectUriSignals = html ? collectRedirectUriSignals(html, finalUrl) : [];
172
+ let openIdConfigurationUrl = null;
173
+ let issuer = null;
174
+ let authorizationEndpoint = null;
175
+ let tokenEndpoint = null;
176
+ let endSessionEndpoint = null;
177
+ let metadataSnapshot = null;
178
+ const strengths = [];
179
+ const issues = [];
180
+ const wellKnownEndpoints = [];
181
+ for (const candidate of deriveOpenIdCandidates(finalUrl, redirects, htmlSecurity, authHostCandidates)) {
182
+ try {
183
+ const response = await withTimeout(requestJson(new URL(candidate)), OIDC_DISCOVERY_TIMEOUT_MS, "OIDC discovery timed out.");
184
+ if (response.statusCode >= 200 && response.statusCode < 300 && response.json) {
185
+ const metadata = response.json;
186
+ metadataSnapshot = metadata;
187
+ openIdConfigurationUrl = candidate;
188
+ wellKnownEndpoints.push(candidate);
189
+ issuer = metadata.issuer || null;
190
+ authorizationEndpoint = metadata.authorization_endpoint || null;
191
+ tokenEndpoint = metadata.token_endpoint || null;
192
+ endSessionEndpoint = metadata.end_session_endpoint || metadata.revocation_endpoint || null;
193
+ break;
194
+ }
195
+ }
196
+ catch {
197
+ continue;
198
+ }
199
+ }
200
+ const protocol = inferProtocol({
201
+ openIdConfigurationUrl,
202
+ authorizationEndpoint,
203
+ tokenEndpoint,
204
+ redirectOrigins: dedicatedRedirectOrigins,
205
+ redirectUriSignals,
206
+ loginPaths,
207
+ });
208
+ const { tenantBrand, tenantRegion, tenantSignals } = extractTenantSignals(provider, issuer, metadataSnapshot);
209
+ if (provider) {
210
+ strengths.push(`Identity provider signals point to ${provider}.`);
211
+ }
212
+ if (openIdConfigurationUrl) {
213
+ strengths.push("An OpenID Connect configuration endpoint is publicly exposed.");
214
+ }
215
+ if (protocol) {
216
+ strengths.push(`Passive evidence suggests a ${protocol.toUpperCase()}-style identity flow.`);
217
+ }
218
+ if (dedicatedRedirectOrigins.length) {
219
+ strengths.push("Authentication redirects point to a dedicated identity origin.");
220
+ }
221
+ if (loginPaths.length) {
222
+ strengths.push(`Passive discovery surfaced ${loginPaths.length} login-like path${loginPaths.length === 1 ? "" : "s"} on the scanned origin.`);
223
+ }
224
+ if (authHostCandidates.some((hostname) => hostname !== finalUrl.hostname)) {
225
+ strengths.push("Separate auth-like hosts were passively observed alongside the main application origin.");
226
+ }
227
+ if (tenantSignals.length) {
228
+ strengths.push("Passive tenant-level Entra metadata was visible.");
229
+ }
230
+ if (redirectUriSignals.length) {
231
+ issues.push("Public markup exposed OAuth redirect_uri-style parameters worth review.");
232
+ }
233
+ if (protocol && !provider && !openIdConfigurationUrl) {
234
+ issues.push("Identity-related flow signals were observed, but no provider or public metadata endpoint could be confirmed.");
235
+ }
236
+ if (!provider && !openIdConfigurationUrl && !loginPaths.length && !redirectOrigins.length) {
237
+ strengths.push("No obvious public IdP or OAuth surface was detected from passive signals.");
238
+ }
239
+ return {
240
+ detected: Boolean(provider || openIdConfigurationUrl || dedicatedRedirectOrigins.length || loginPaths.length || redirectUriSignals.length),
241
+ provider,
242
+ protocol,
243
+ redirectOrigins,
244
+ authHostCandidates,
245
+ loginPaths,
246
+ openIdConfigurationUrl,
247
+ wellKnownEndpoints,
248
+ issuer,
249
+ authorizationEndpoint,
250
+ tokenEndpoint,
251
+ endSessionEndpoint,
252
+ redirectUriSignals,
253
+ tenantBrand,
254
+ tenantRegion,
255
+ tenantSignals,
256
+ issues,
257
+ strengths,
258
+ };
259
+ };
@@ -0,0 +1,17 @@
1
+ import { URL } from "node:url";
2
+ import type { AnalysisResult, AnalyzeTargetOptions, HtmlSecurityInfo } from "./types.js";
3
+ export { buildPostureRiskEventsFromDiff, buildPostureRiskEventsFromSnapshots } from "./riskEvents.js";
4
+ export { buildPostureDigest } from "./postureDigest.js";
5
+ export { buildPostureDriftReport, buildPostureDriftReportFromDiff, buildPostureDriftReportFromSnapshots, } from "./postureDrift.js";
6
+ export { attachIssueEvidence, buildIssueEvidence, buildPostureRemediationPlan, } from "./postureRemediation.js";
7
+ export type { PostureRiskEvent, PostureRiskEventSeverity } from "./types.js";
8
+ declare function formatErrorMessage(error: any): string;
9
+ export declare function analyzeHtmlDocument(input: string | URL, html: string): HtmlSecurityInfo;
10
+ export declare function analyzeUrl(input: string, options?: AnalyzeTargetOptions): Promise<AnalysisResult>;
11
+ export declare const analyzeTarget: typeof analyzeUrl;
12
+ export { formatErrorMessage };
13
+ export { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
14
+ export { analyzeInfrastructure } from "./infrastructure.js";
15
+ export { buildHistoryDiff, buildHistoryDiffFromSnapshots, snapshotFromAnalysis } from "./historyDiff.js";
16
+ export { assertPublicRequestTarget, isLocalHostname, isPrivateAddress, } from "./network-validation.js";
17
+ export type * from "./types.js";