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
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export declare const unique: <T>(values: Array<T | null | undefined | false>) => T[];
2
+ export declare const withTimeout: <T>(promise: Promise<T>, timeoutMs: number, message: string) => Promise<T>;
3
+ export declare const headerValue: (headers: Record<string, string | string[] | undefined>, name: string) => string | null;
4
+ export declare const safeResolve: <T>(operation: () => Promise<T>) => Promise<T | null>;
5
+ export declare const safeResolveWithTimeout: <T>(operation: () => Promise<T>, timeoutMs: number, message?: string) => Promise<T | null>;
6
+ export declare const mapWithConcurrency: <T, R>(items: T[], limit: number, mapper: (item: T, index: number) => Promise<R>) => Promise<R[]>;
7
+ export declare const getSiteDomain: (hostname: string) => string;
package/dist/utils.js ADDED
@@ -0,0 +1,66 @@
1
+ export const unique = (values) => [...new Set(values.filter((value) => Boolean(value)))];
2
+ export const withTimeout = async (promise, timeoutMs, message) => {
3
+ let timeoutId = null;
4
+ try {
5
+ return await Promise.race([
6
+ promise,
7
+ new Promise((_, reject) => {
8
+ timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
9
+ }),
10
+ ]);
11
+ }
12
+ finally {
13
+ if (timeoutId) {
14
+ clearTimeout(timeoutId);
15
+ }
16
+ }
17
+ };
18
+ export const headerValue = (headers, name) => {
19
+ const value = headers[name];
20
+ if (Array.isArray(value)) {
21
+ return value.join(", ");
22
+ }
23
+ return value ?? null;
24
+ };
25
+ export const safeResolve = async (operation) => {
26
+ try {
27
+ return await operation();
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ };
33
+ export const safeResolveWithTimeout = async (operation, timeoutMs, message = "DNS lookup timed out.") => {
34
+ try {
35
+ return await withTimeout(operation(), timeoutMs, message);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ };
41
+ export const mapWithConcurrency = async (items, limit, mapper) => {
42
+ const results = new Array(items.length);
43
+ let nextIndex = 0;
44
+ const worker = async () => {
45
+ while (nextIndex < items.length) {
46
+ const currentIndex = nextIndex;
47
+ nextIndex += 1;
48
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
49
+ }
50
+ };
51
+ await Promise.all(Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, () => worker()));
52
+ return results;
53
+ };
54
+ export const getSiteDomain = (hostname) => {
55
+ const lower = hostname.toLowerCase();
56
+ const parts = lower.split(".").filter(Boolean);
57
+ if (parts.length <= 2) {
58
+ return lower;
59
+ }
60
+ const compoundSuffixes = new Set(["co.uk", "org.uk", "ac.uk", "gov.uk", "com.au", "co.nz"]);
61
+ const suffix = parts.slice(-2).join(".");
62
+ if (compoundSuffixes.has(suffix) && parts.length >= 3) {
63
+ return parts.slice(-3).join(".");
64
+ }
65
+ return parts.slice(-2).join(".");
66
+ };
@@ -0,0 +1,5 @@
1
+ import { URL } from "node:url";
2
+ import type { RedirectHop, WafFingerprintInfo } from "./types.js";
3
+ type ResponseHeaders = Record<string, string | string[] | undefined>;
4
+ export declare const analyzeWafFingerprint: (finalUrl: URL, headers: ResponseHeaders, html: string | null, redirects: RedirectHop[]) => WafFingerprintInfo;
5
+ export {};
@@ -0,0 +1,156 @@
1
+ import { URL } from "node:url";
2
+ import { unique } from "./utils.js";
3
+ const headerValue = (headers, name) => {
4
+ const value = headers[name];
5
+ if (Array.isArray(value)) {
6
+ return value.join(", ");
7
+ }
8
+ return value ?? null;
9
+ };
10
+ const WAF_DETECTORS = [
11
+ {
12
+ name: "Cloudflare",
13
+ confidence: "high",
14
+ detection: "observed",
15
+ test: (headers, body) => Boolean(headerValue(headers, "cf-ray") || headerValue(headers, "cf-cache-status") || /cloudflare/i.test(headerValue(headers, "server") || "") || /attention required|cloudflare/i.test(body)),
16
+ evidence: (headers) => headerValue(headers, "cf-ray") ? "Observed cf-ray / Cloudflare edge headers." : "Observed Cloudflare-branded edge response markers.",
17
+ },
18
+ {
19
+ name: "Akamai",
20
+ confidence: "high",
21
+ detection: "observed",
22
+ test: (headers, body) => Boolean(headerValue(headers, "x-akamai-transformed") || headerValue(headers, "akamai-cache-status") || /akamai/i.test(headerValue(headers, "server") || "") || /reference #\d+\.[a-z0-9.]+\.akamai/i.test(body)),
23
+ evidence: () => "Observed Akamai edge headers or block-page signatures.",
24
+ },
25
+ {
26
+ name: "Imperva",
27
+ confidence: "high",
28
+ detection: "observed",
29
+ test: (headers, body) => Boolean(headerValue(headers, "x-iinfo") || /imperva|incapsula/i.test(headerValue(headers, "server") || "") || /incapsula incident id|imperva/i.test(body)),
30
+ evidence: () => "Observed Imperva/Incapsula response markers.",
31
+ },
32
+ {
33
+ name: "Sucuri",
34
+ confidence: "high",
35
+ detection: "observed",
36
+ test: (headers, body) => Boolean(headerValue(headers, "x-sucuri-id") || headerValue(headers, "x-sucuri-cache") || /sucuri/i.test(headerValue(headers, "server") || "") || /sucuri website firewall/i.test(body)),
37
+ evidence: () => "Observed Sucuri edge headers or branded error-page markers.",
38
+ },
39
+ {
40
+ name: "Fastly",
41
+ confidence: "medium",
42
+ detection: "observed",
43
+ test: (headers) => Boolean((headerValue(headers, "x-cache") || "").toLowerCase().includes("fastly") || (headerValue(headers, "x-served-by") || "").toLowerCase().includes("cache-")),
44
+ evidence: () => "Observed Fastly cache headers.",
45
+ },
46
+ {
47
+ name: "AWS CloudFront / WAF",
48
+ confidence: "medium",
49
+ detection: "observed",
50
+ test: (headers) => Boolean(headerValue(headers, "x-amz-cf-id") || /cloudfront/i.test(headerValue(headers, "server") || "")),
51
+ evidence: () => "Observed CloudFront edge headers.",
52
+ },
53
+ {
54
+ name: "Azure Front Door",
55
+ confidence: "high",
56
+ detection: "observed",
57
+ test: (headers) => Boolean(headerValue(headers, "x-azure-ref")),
58
+ evidence: () => "Observed x-azure-ref edge headers.",
59
+ },
60
+ {
61
+ name: "F5 BIG-IP ASM",
62
+ confidence: "medium",
63
+ detection: "observed",
64
+ test: (headers, body) => Boolean(headerValue(headers, "x-wa-info") || headerValue(headers, "x-cnection") || /the requested url was rejected/i.test(body)),
65
+ evidence: () => "Observed F5-style response headers or rejection-body markers.",
66
+ },
67
+ {
68
+ name: "Barracuda",
69
+ confidence: "high",
70
+ detection: "observed",
71
+ test: (headers) => Object.keys(headers).some((key) => key.toLowerCase().startsWith("x-barracuda-")),
72
+ evidence: () => "Observed Barracuda-branded response headers.",
73
+ },
74
+ {
75
+ name: "Nginx Plus / ModSecurity",
76
+ confidence: "medium",
77
+ detection: "observed",
78
+ test: (headers) => Boolean((headerValue(headers, "server") || "").toLowerCase().includes("mod_security") || headerValue(headers, "x-response-code")),
79
+ evidence: () => "Observed mod_security or gateway response markers.",
80
+ },
81
+ {
82
+ name: "Palo Alto Prisma WAAS",
83
+ confidence: "high",
84
+ detection: "observed",
85
+ test: (headers) => Boolean(headerValue(headers, "x-pan-request-id")),
86
+ evidence: () => "Observed x-pan-request-id header.",
87
+ },
88
+ {
89
+ name: "Google Cloud Armor",
90
+ confidence: "medium",
91
+ detection: "observed",
92
+ test: (headers) => Object.keys(headers).some((key) => key.toLowerCase().startsWith("x-goog-")) &&
93
+ (headerValue(headers, "via") || "").toLowerCase().includes("google"),
94
+ evidence: () => "Observed x-goog-* headers with Google edge routing markers.",
95
+ },
96
+ {
97
+ name: "Vercel Edge Network",
98
+ confidence: "high",
99
+ detection: "observed",
100
+ test: (headers) => Boolean(headerValue(headers, "x-vercel-id")),
101
+ evidence: () => "Observed x-vercel-id header.",
102
+ },
103
+ ];
104
+ export const analyzeWafFingerprint = (finalUrl, headers, html, redirects) => {
105
+ const body = (html || "").toLowerCase();
106
+ const providers = WAF_DETECTORS
107
+ .filter((detector) => detector.test(headers, body))
108
+ .map((detector) => ({
109
+ name: detector.name,
110
+ confidence: detector.confidence,
111
+ detection: detector.detection,
112
+ evidence: detector.evidence(headers),
113
+ }));
114
+ const via = headerValue(headers, "via");
115
+ const server = headerValue(headers, "server");
116
+ const xCdn = headerValue(headers, "x-cdn");
117
+ const edgeSignals = unique([
118
+ server && /(edge|proxy|gateway|cache|gtm|belfrage|varnish)/i.test(server) ? `Server: ${server}` : null,
119
+ via ? `Via: ${via}` : null,
120
+ xCdn ? `X-CDN: ${xCdn}` : null,
121
+ redirects.some((hop) => {
122
+ try {
123
+ return hop.location ? new URL(hop.location, finalUrl).origin !== finalUrl.origin : false;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ })
129
+ ? "Redirect chain includes a separate edge or identity origin."
130
+ : null,
131
+ ]);
132
+ const strengths = [];
133
+ const issues = [];
134
+ if (providers.length) {
135
+ strengths.push(`Edge protection or delivery signals point to ${providers.map((provider) => provider.name).join(", ")}.`);
136
+ }
137
+ else {
138
+ strengths.push("No branded WAF or edge provider was conclusively identified from passive response evidence.");
139
+ }
140
+ if (edgeSignals.length) {
141
+ strengths.push("Response headers exposed edge-network handling details that help classify the delivery path.");
142
+ }
143
+ if (providers.some((provider) => provider.name.includes("CloudFront"))) {
144
+ issues.push("AWS edge delivery was observed, but passive evidence alone does not confirm whether AWS WAF policies are enforced.");
145
+ }
146
+ return {
147
+ detected: Boolean(providers.length),
148
+ providers,
149
+ edgeSignals,
150
+ issues,
151
+ strengths,
152
+ summary: providers.length
153
+ ? `Passive response evidence suggests ${providers.map((provider) => provider.name).join(", ")} in front of the target.`
154
+ : "No branded WAF or edge-protection provider was conclusively identified from passive response evidence.",
155
+ };
156
+ };
@@ -0,0 +1,27 @@
1
+ import {
2
+ buildHistoryDiffFromSnapshots,
3
+ snapshotFromAnalysis,
4
+ } from "securl/history-diff";
5
+ import {
6
+ buildPostureRiskEventsFromSnapshots,
7
+ } from "securl/risk-events";
8
+
9
+ const [currentPath, previousPath] = process.argv.slice(2);
10
+
11
+ if (!currentPath || !previousPath) {
12
+ console.error("Usage: node risk-events.mjs current-report.json previous-report.json");
13
+ process.exit(1);
14
+ }
15
+
16
+ const current = JSON.parse(await import("node:fs/promises").then((fs) => fs.readFile(currentPath, "utf8")));
17
+ const previous = JSON.parse(await import("node:fs/promises").then((fs) => fs.readFile(previousPath, "utf8")));
18
+
19
+ const currentSnapshot = snapshotFromAnalysis(current.analysis ?? current);
20
+ const previousSnapshot = snapshotFromAnalysis(previous.analysis ?? previous);
21
+ const diff = buildHistoryDiffFromSnapshots(currentSnapshot, previousSnapshot);
22
+ const riskEvents = buildPostureRiskEventsFromSnapshots(currentSnapshot, previousSnapshot, diff);
23
+
24
+ console.log(JSON.stringify({
25
+ diff,
26
+ riskEvents,
27
+ }, null, 2));
@@ -0,0 +1,17 @@
1
+ import { analyzeUrl } from "securl";
2
+
3
+ const target = process.argv[2] ?? "https://example.com";
4
+ const result = await analyzeUrl(target, {
5
+ scanMode: "quiet",
6
+ });
7
+
8
+ console.log(JSON.stringify({
9
+ url: result.finalUrl,
10
+ score: result.score,
11
+ grade: result.grade,
12
+ mainRisk: result.executiveSummary?.mainRisk ?? null,
13
+ findings: result.issues.map((issue) => ({
14
+ severity: issue.severity,
15
+ title: issue.title,
16
+ })),
17
+ }, null, 2));
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "securl",
3
+ "version": "1.4.1",
4
+ "type": "module",
5
+ "description": "Passive external security posture analysis engine for SecURL.",
6
+ "author": {
7
+ "name": "Keith Batterham",
8
+ "url": "https://github.com/ktbatterham"
9
+ },
10
+ "homepage": "https://securl.online",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/this-is-securl/securl.git",
14
+ "directory": "packages/core"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/this-is-securl/securl/issues"
18
+ },
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "types": "./dist/index.d.ts",
23
+ "main": "./dist/index.js",
24
+ "bin": {
25
+ "securl": "dist/cli.js",
26
+ "epi": "dist/cli.js",
27
+ "external-posture-insight": "dist/cli.js"
28
+ },
29
+ "sideEffects": false,
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./cli": {
39
+ "default": "./dist/cli.js"
40
+ },
41
+ "./types": {
42
+ "types": "./dist/types.d.ts",
43
+ "default": "./dist/types.js"
44
+ },
45
+ "./history-diff": {
46
+ "types": "./dist/historyDiff.d.ts",
47
+ "default": "./dist/historyDiff.js"
48
+ },
49
+ "./risk-events": {
50
+ "types": "./dist/riskEvents.d.ts",
51
+ "default": "./dist/riskEvents.js"
52
+ },
53
+ "./posture-digest": {
54
+ "types": "./dist/postureDigest.d.ts",
55
+ "default": "./dist/postureDigest.js"
56
+ },
57
+ "./posture-drift": {
58
+ "types": "./dist/postureDrift.d.ts",
59
+ "default": "./dist/postureDrift.js"
60
+ },
61
+ "./remediation-plan": {
62
+ "types": "./dist/postureRemediation.d.ts",
63
+ "default": "./dist/postureRemediation.js"
64
+ }
65
+ },
66
+ "files": [
67
+ "dist",
68
+ "examples",
69
+ "CHANGELOG.md",
70
+ "README.md",
71
+ "RELEASING.md",
72
+ "SECURITY.md",
73
+ "LICENSE"
74
+ ],
75
+ "scripts": {
76
+ "build": "tsc -p tsconfig.json",
77
+ "test": "npm run build && node --test test/*.test.mjs"
78
+ },
79
+ "keywords": [
80
+ "security",
81
+ "security-headers",
82
+ "headers",
83
+ "tls",
84
+ "dns",
85
+ "dmarc",
86
+ "owasp",
87
+ "posture",
88
+ "scanner",
89
+ "attack-surface",
90
+ "external-attack-surface",
91
+ "defensive-security",
92
+ "cli",
93
+ "ci",
94
+ "monitoring",
95
+ "supply-chain",
96
+ "vendor-risk"
97
+ ],
98
+ "license": "MIT",
99
+ "dependencies": {
100
+ "node-html-parser": "^7.1.0"
101
+ }
102
+ }