kramscan 0.2.0 → 0.3.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/LICENSE +1 -1
- package/README.md +81 -54
- package/dist/cli.js +8 -1
- package/dist/commands/config.js +2 -2
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +239 -0
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +112 -0
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/report.js +89 -11
- package/dist/commands/scan.js +11 -0
- package/dist/commands/scans.js +4 -0
- package/dist/core/config-schema.js +1 -1
- package/dist/core/config.js +3 -3
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/scan-index.d.ts +1 -0
- package/dist/core/scanner.js +7 -1
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +6 -0
- package/dist/core/vulnerability-detector.js +21 -0
- package/dist/index.js +14 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.js +11 -1
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -0
- package/dist/reports/PdfGenerator.js +26 -1
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.js +7 -1
- package/package.json +8 -3
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CORSAnalyzerPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class CORSAnalyzerPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "CORS Analyzer";
|
|
7
|
+
type = "header";
|
|
8
|
+
description = "Detects overly permissive Cross-Origin Resource Sharing configurations";
|
|
9
|
+
reportedHosts = new Set();
|
|
10
|
+
async analyzeHeaders(context, headers) {
|
|
11
|
+
const host = new URL(context.url).host;
|
|
12
|
+
// Only report once per host
|
|
13
|
+
if (this.reportedHosts.has(host)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const vulnerabilities = [];
|
|
17
|
+
const acao = headers["access-control-allow-origin"];
|
|
18
|
+
const acac = headers["access-control-allow-credentials"];
|
|
19
|
+
const acam = headers["access-control-allow-methods"];
|
|
20
|
+
const acah = headers["access-control-allow-headers"];
|
|
21
|
+
// Check for wildcard origin
|
|
22
|
+
if (acao === "*") {
|
|
23
|
+
vulnerabilities.push(this.createVulnerability("CORS: Wildcard Origin Allowed", `The server at ${host} allows requests from any origin (Access-Control-Allow-Origin: *). ` +
|
|
24
|
+
`This can expose sensitive data to malicious third-party websites.`, context.url, "medium", `Access-Control-Allow-Origin: *`, "Restrict Access-Control-Allow-Origin to trusted domains only. " +
|
|
25
|
+
"Use a whitelist of allowed origins instead of the wildcard *.", "CWE-942"));
|
|
26
|
+
}
|
|
27
|
+
// Check for wildcard with credentials (very dangerous)
|
|
28
|
+
if (acao === "*" && acac?.toLowerCase() === "true") {
|
|
29
|
+
vulnerabilities.push(this.createVulnerability("CORS: Wildcard Origin with Credentials", `The server at ${host} allows any origin AND sends credentials. ` +
|
|
30
|
+
`This is a critical misconfiguration that can lead to complete session hijacking.`, context.url, "critical", `Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true`, "Never combine wildcard origin with credentials. " +
|
|
31
|
+
"Validate the Origin header against a strict whitelist and reflect only trusted origins.", "CWE-942"));
|
|
32
|
+
}
|
|
33
|
+
// Check if origin is reflected without validation (test with a probe)
|
|
34
|
+
if (acao && acao !== "*" && acao !== "null") {
|
|
35
|
+
// If the ACAO reflects back what looks like an origin, it might be reflecting without validation
|
|
36
|
+
const isLikelyReflecting = acao.includes("://") && !acao.includes(host);
|
|
37
|
+
if (isLikelyReflecting && acac?.toLowerCase() === "true") {
|
|
38
|
+
vulnerabilities.push(this.createVulnerability("CORS: Origin Reflection with Credentials", `The server at ${host} appears to reflect the Origin header value while also allowing credentials. ` +
|
|
39
|
+
`An attacker can make authenticated requests from any origin.`, context.url, "high", `Access-Control-Allow-Origin: ${acao}, Access-Control-Allow-Credentials: true`, "Validate the Origin header against a strict whitelist of trusted domains. " +
|
|
40
|
+
"Never blindly reflect the Origin header.", "CWE-942"));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Check for null origin allowed (can be exploited via sandboxed iframes)
|
|
44
|
+
if (acao === "null") {
|
|
45
|
+
vulnerabilities.push(this.createVulnerability("CORS: Null Origin Allowed", `The server at ${host} accepts the 'null' origin. ` +
|
|
46
|
+
`Attackers can exploit this using sandboxed iframes or data URIs.`, context.url, "medium", `Access-Control-Allow-Origin: null`, "Do not allow the 'null' origin. Sandboxed iframes and redirects can send Origin: null.", "CWE-942"));
|
|
47
|
+
}
|
|
48
|
+
// Check for dangerous methods
|
|
49
|
+
if (acam) {
|
|
50
|
+
const dangerousMethods = ["PUT", "DELETE", "PATCH"];
|
|
51
|
+
const allowedMethods = acam.toUpperCase().split(",").map(m => m.trim());
|
|
52
|
+
const exposed = dangerousMethods.filter(m => allowedMethods.includes(m));
|
|
53
|
+
if (exposed.length > 0 && acao === "*") {
|
|
54
|
+
vulnerabilities.push(this.createVulnerability("CORS: Dangerous Methods with Wildcard Origin", `The server at ${host} allows ${exposed.join(", ")} methods from any origin. ` +
|
|
55
|
+
`This could allow unauthorized data modification from third-party sites.`, context.url, "high", `Access-Control-Allow-Methods: ${acam}; Access-Control-Allow-Origin: *`, "Restrict allowed methods to those actually needed, and never combine with wildcard origin.", "CWE-942"));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (vulnerabilities.length > 0) {
|
|
59
|
+
this.reportedHosts.add(host);
|
|
60
|
+
}
|
|
61
|
+
return vulnerabilities;
|
|
62
|
+
}
|
|
63
|
+
reset() {
|
|
64
|
+
this.reportedHosts.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.CORSAnalyzerPlugin = CORSAnalyzerPlugin;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
import { Vulnerability } from "../../core/vulnerability-detector";
|
|
3
|
+
export declare class CookieSecurityPlugin extends BaseVulnerabilityPlugin {
|
|
4
|
+
readonly name = "Cookie Security Auditor";
|
|
5
|
+
readonly type: "header";
|
|
6
|
+
readonly description = "Audits cookies for missing security flags (HttpOnly, Secure, SameSite)";
|
|
7
|
+
private readonly reportedCookies;
|
|
8
|
+
analyzeHeaders(context: PluginContext, headers: Record<string, string>): Promise<Vulnerability[]>;
|
|
9
|
+
reset(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CookieSecurityPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class CookieSecurityPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "Cookie Security Auditor";
|
|
7
|
+
type = "header";
|
|
8
|
+
description = "Audits cookies for missing security flags (HttpOnly, Secure, SameSite)";
|
|
9
|
+
reportedCookies = new Set();
|
|
10
|
+
async analyzeHeaders(context, headers) {
|
|
11
|
+
const vulnerabilities = [];
|
|
12
|
+
const host = new URL(context.url).host;
|
|
13
|
+
// Collect all set-cookie headers
|
|
14
|
+
const setCookieHeader = headers["set-cookie"];
|
|
15
|
+
if (!setCookieHeader)
|
|
16
|
+
return [];
|
|
17
|
+
// set-cookie headers might be combined with newlines in some scenarios
|
|
18
|
+
const cookies = setCookieHeader.split(/,(?=[^;]*=)/g).map(c => c.trim());
|
|
19
|
+
for (const cookie of cookies) {
|
|
20
|
+
const cookieName = cookie.split("=")[0]?.trim();
|
|
21
|
+
if (!cookieName)
|
|
22
|
+
continue;
|
|
23
|
+
const cookieKey = `${host}:${cookieName}`;
|
|
24
|
+
if (this.reportedCookies.has(cookieKey))
|
|
25
|
+
continue;
|
|
26
|
+
const cookieLower = cookie.toLowerCase();
|
|
27
|
+
const issues = [];
|
|
28
|
+
// Check HttpOnly flag
|
|
29
|
+
if (!cookieLower.includes("httponly")) {
|
|
30
|
+
issues.push("Missing HttpOnly flag (vulnerable to XSS cookie theft)");
|
|
31
|
+
}
|
|
32
|
+
// Check Secure flag
|
|
33
|
+
if (!cookieLower.includes("secure")) {
|
|
34
|
+
issues.push("Missing Secure flag (cookie sent over HTTP)");
|
|
35
|
+
}
|
|
36
|
+
// Check SameSite attribute
|
|
37
|
+
if (!cookieLower.includes("samesite")) {
|
|
38
|
+
issues.push("Missing SameSite attribute (vulnerable to CSRF)");
|
|
39
|
+
}
|
|
40
|
+
else if (cookieLower.includes("samesite=none")) {
|
|
41
|
+
if (!cookieLower.includes("secure")) {
|
|
42
|
+
issues.push("SameSite=None without Secure flag (browser will reject)");
|
|
43
|
+
}
|
|
44
|
+
issues.push("SameSite=None allows cross-site requests (verify this is intentional)");
|
|
45
|
+
}
|
|
46
|
+
// Check for session-like cookies with missing flags
|
|
47
|
+
const isSessionCookie = /^(sess|session|sid|token|auth|jwt|csrf|xsrf|connect\.sid|phpsessid|jsessionid|asp\.net_sessionid)/i.test(cookieName);
|
|
48
|
+
if (issues.length > 0) {
|
|
49
|
+
const severity = isSessionCookie
|
|
50
|
+
? (issues.some(i => i.includes("HttpOnly")) ? "high" : "medium")
|
|
51
|
+
: "low";
|
|
52
|
+
this.reportedCookies.add(cookieKey);
|
|
53
|
+
vulnerabilities.push(this.createVulnerability(`Insecure Cookie: ${cookieName}`, `The cookie '${cookieName}' on ${host} has security issues:\n` +
|
|
54
|
+
issues.map(i => ` • ${i}`).join("\n"), context.url, severity, `Set-Cookie: ${cookie.substring(0, 200)}`, "Set all cookies with: HttpOnly (prevents JS access), " +
|
|
55
|
+
"Secure (HTTPS only), SameSite=Lax or Strict (CSRF protection). " +
|
|
56
|
+
"Example: Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/", "CWE-614"));
|
|
57
|
+
}
|
|
58
|
+
// Check for overly broad domain
|
|
59
|
+
const domainMatch = cookie.match(/domain=([^;]+)/i);
|
|
60
|
+
if (domainMatch) {
|
|
61
|
+
const domain = domainMatch[1].trim();
|
|
62
|
+
if (domain.startsWith(".") && domain.split(".").length <= 2) {
|
|
63
|
+
this.reportedCookies.add(cookieKey + ":domain");
|
|
64
|
+
vulnerabilities.push(this.createVulnerability(`Cookie Domain Too Broad: ${cookieName}`, `The cookie '${cookieName}' has domain set to '${domain}', which allows ` +
|
|
65
|
+
`any subdomain to read this cookie. This increases the attack surface.`, context.url, "low", `Domain=${domain}`, "Set the cookie domain to the most specific subdomain possible.", "CWE-1275"));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Check for missing expiry on session cookies (persistent session)
|
|
69
|
+
if (isSessionCookie && !cookieLower.includes("expires") && !cookieLower.includes("max-age")) {
|
|
70
|
+
// This is actually good practice (session cookie dies with browser close)
|
|
71
|
+
// But if there's _no_ expiry and it's NOT a session cookie, flag it
|
|
72
|
+
}
|
|
73
|
+
else if (isSessionCookie && cookieLower.includes("max-age")) {
|
|
74
|
+
const maxAgeMatch = cookie.match(/max-age=(\d+)/i);
|
|
75
|
+
if (maxAgeMatch) {
|
|
76
|
+
const maxAge = parseInt(maxAgeMatch[1], 10);
|
|
77
|
+
const thirtyDays = 30 * 24 * 60 * 60;
|
|
78
|
+
if (maxAge > thirtyDays) {
|
|
79
|
+
vulnerabilities.push(this.createVulnerability(`Long-Lived Session Cookie: ${cookieName}`, `The session cookie '${cookieName}' has a very long lifetime (${Math.round(maxAge / 86400)} days). ` +
|
|
80
|
+
`Long-lived session tokens increase the window for token theft.`, context.url, "low", `Max-Age=${maxAge} (${Math.round(maxAge / 86400)} days)`, "Set session cookie lifetime to the minimum needed (e.g., 24 hours for most apps).", "CWE-613"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return vulnerabilities;
|
|
86
|
+
}
|
|
87
|
+
reset() {
|
|
88
|
+
this.reportedCookies.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.CookieSecurityPlugin = CookieSecurityPlugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
import { Vulnerability } from "../../core/vulnerability-detector";
|
|
3
|
+
export declare class DebugEndpointPlugin extends BaseVulnerabilityPlugin {
|
|
4
|
+
readonly name = "Debug Endpoint Detector";
|
|
5
|
+
readonly type: "info";
|
|
6
|
+
readonly description = "Probes for common debug, admin, and development endpoints left exposed";
|
|
7
|
+
private readonly reportedPaths;
|
|
8
|
+
/**
|
|
9
|
+
* Common debug/dev endpoints that developers forget to disable before deployment.
|
|
10
|
+
* Each entry has a path, a human-readable name, and expected severity.
|
|
11
|
+
*/
|
|
12
|
+
private readonly debugEndpoints;
|
|
13
|
+
analyzeContent(context: PluginContext, content: string): Promise<Vulnerability[]>;
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DebugEndpointPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class DebugEndpointPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "Debug Endpoint Detector";
|
|
7
|
+
type = "info";
|
|
8
|
+
description = "Probes for common debug, admin, and development endpoints left exposed";
|
|
9
|
+
reportedPaths = new Set();
|
|
10
|
+
/**
|
|
11
|
+
* Common debug/dev endpoints that developers forget to disable before deployment.
|
|
12
|
+
* Each entry has a path, a human-readable name, and expected severity.
|
|
13
|
+
*/
|
|
14
|
+
debugEndpoints = [
|
|
15
|
+
{
|
|
16
|
+
path: "/debug",
|
|
17
|
+
name: "Debug Panel",
|
|
18
|
+
severity: "high",
|
|
19
|
+
indicators: ["debug", "stack", "trace", "error"],
|
|
20
|
+
remediation: "Remove or protect the /debug endpoint behind authentication.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
path: "/__debug__",
|
|
24
|
+
name: "Django Debug Panel",
|
|
25
|
+
severity: "high",
|
|
26
|
+
indicators: ["django", "debug", "toolbar"],
|
|
27
|
+
remediation: "Set DEBUG=False in Django settings for production.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: "/phpinfo.php",
|
|
31
|
+
name: "PHP Info Page",
|
|
32
|
+
severity: "high",
|
|
33
|
+
indicators: ["phpinfo", "PHP Version", "Configuration"],
|
|
34
|
+
remediation: "Remove phpinfo.php from production servers.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: "/server-status",
|
|
38
|
+
name: "Apache Server Status",
|
|
39
|
+
severity: "medium",
|
|
40
|
+
indicators: ["Apache", "Server Version", "Current Time"],
|
|
41
|
+
remediation: "Restrict /server-status to internal IPs only.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: "/server-info",
|
|
45
|
+
name: "Apache Server Info",
|
|
46
|
+
severity: "medium",
|
|
47
|
+
indicators: ["Apache", "Server Information"],
|
|
48
|
+
remediation: "Disable mod_info or restrict access to internal IPs.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
path: "/graphql",
|
|
52
|
+
name: "GraphQL Endpoint (Introspection)",
|
|
53
|
+
severity: "medium",
|
|
54
|
+
indicators: ["graphql", "__schema", "query"],
|
|
55
|
+
remediation: "Disable GraphQL introspection in production.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
path: "/graphiql",
|
|
59
|
+
name: "GraphiQL IDE",
|
|
60
|
+
severity: "high",
|
|
61
|
+
indicators: ["graphiql", "GraphiQL"],
|
|
62
|
+
remediation: "Remove GraphiQL from production deployments.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
path: "/swagger",
|
|
66
|
+
name: "Swagger UI",
|
|
67
|
+
severity: "medium",
|
|
68
|
+
indicators: ["swagger", "api-docs", "openapi"],
|
|
69
|
+
remediation: "Protect Swagger UI behind authentication or remove from production.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: "/swagger-ui.html",
|
|
73
|
+
name: "Swagger UI HTML",
|
|
74
|
+
severity: "medium",
|
|
75
|
+
indicators: ["swagger", "api-docs"],
|
|
76
|
+
remediation: "Protect Swagger UI behind authentication or remove from production.",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
path: "/api-docs",
|
|
80
|
+
name: "API Documentation",
|
|
81
|
+
severity: "low",
|
|
82
|
+
indicators: ["api", "docs", "openapi", "swagger"],
|
|
83
|
+
remediation: "Restrict API docs to authenticated users in production.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: "/actuator",
|
|
87
|
+
name: "Spring Boot Actuator",
|
|
88
|
+
severity: "high",
|
|
89
|
+
indicators: ["actuator", "beans", "health", "info"],
|
|
90
|
+
remediation: "Secure Spring Boot Actuator endpoints with authentication.",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
path: "/actuator/env",
|
|
94
|
+
name: "Spring Boot Environment",
|
|
95
|
+
severity: "critical",
|
|
96
|
+
indicators: ["property", "source", "value"],
|
|
97
|
+
remediation: "Never expose /actuator/env publicly. It reveals environment variables and secrets.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
path: "/actuator/heapdump",
|
|
101
|
+
name: "Spring Boot Heap Dump",
|
|
102
|
+
severity: "critical",
|
|
103
|
+
indicators: [],
|
|
104
|
+
remediation: "Disable heap dump endpoint. It can expose sensitive memory contents.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
path: "/elmah.axd",
|
|
108
|
+
name: "ELMAH Error Log (.NET)",
|
|
109
|
+
severity: "high",
|
|
110
|
+
indicators: ["elmah", "error", "exception"],
|
|
111
|
+
remediation: "Restrict ELMAH access to authenticated administrators.",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
path: "/trace.axd",
|
|
115
|
+
name: ".NET Trace Page",
|
|
116
|
+
severity: "high",
|
|
117
|
+
indicators: ["trace", "request", "response"],
|
|
118
|
+
remediation: "Disable tracing in production web.config.",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
path: "/.env",
|
|
122
|
+
name: "Environment File",
|
|
123
|
+
severity: "critical",
|
|
124
|
+
indicators: ["=", "KEY", "SECRET", "PASSWORD", "DATABASE"],
|
|
125
|
+
remediation: "Never serve .env files. Add them to .gitignore and configure your server to deny access.",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
path: "/wp-config.php",
|
|
129
|
+
name: "WordPress Config",
|
|
130
|
+
severity: "critical",
|
|
131
|
+
indicators: ["DB_NAME", "DB_PASSWORD", "AUTH_KEY"],
|
|
132
|
+
remediation: "Ensure wp-config.php is not accessible via HTTP.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
path: "/.git/config",
|
|
136
|
+
name: "Git Repository Config",
|
|
137
|
+
severity: "critical",
|
|
138
|
+
indicators: ["[core]", "[remote", "repositoryformatversion"],
|
|
139
|
+
remediation: "Block access to .git directory. Your source code is exposed.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
path: "/.git/HEAD",
|
|
143
|
+
name: "Git HEAD Reference",
|
|
144
|
+
severity: "high",
|
|
145
|
+
indicators: ["ref:", "refs/heads/"],
|
|
146
|
+
remediation: "Block access to the entire .git directory in your web server configuration.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
path: "/config.json",
|
|
150
|
+
name: "JSON Config File",
|
|
151
|
+
severity: "medium",
|
|
152
|
+
indicators: ["apiKey", "secret", "password", "database", "connection"],
|
|
153
|
+
remediation: "Do not serve config files via HTTP. Move them outside the web root.",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
path: "/test",
|
|
157
|
+
name: "Test Endpoint",
|
|
158
|
+
severity: "low",
|
|
159
|
+
indicators: ["test", "debug", "hello"],
|
|
160
|
+
remediation: "Remove test endpoints before deploying to production.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
path: "/health",
|
|
164
|
+
name: "Health Check (Verbose)",
|
|
165
|
+
severity: "low",
|
|
166
|
+
indicators: ["database", "connection", "redis", "memory", "uptime"],
|
|
167
|
+
remediation: "Limit health check responses to simple status. Do not expose internal service details.",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
path: "/metrics",
|
|
171
|
+
name: "Prometheus Metrics",
|
|
172
|
+
severity: "medium",
|
|
173
|
+
indicators: ["process_", "http_", "nodejs_", "go_"],
|
|
174
|
+
remediation: "Protect /metrics endpoint behind authentication or restrict to internal networks.",
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
async analyzeContent(context, content) {
|
|
178
|
+
const vulnerabilities = [];
|
|
179
|
+
const baseUrl = new URL(context.url).origin;
|
|
180
|
+
for (const endpoint of this.debugEndpoints) {
|
|
181
|
+
const fullPath = `${baseUrl}${endpoint.path}`;
|
|
182
|
+
if (this.reportedPaths.has(fullPath))
|
|
183
|
+
continue;
|
|
184
|
+
try {
|
|
185
|
+
const response = await context.page.evaluate(async (url) => {
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch(url, {
|
|
188
|
+
method: "GET",
|
|
189
|
+
redirect: "follow",
|
|
190
|
+
credentials: "omit",
|
|
191
|
+
});
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
return { status: res.status, body: text.substring(0, 2000) };
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return { status: 0, body: "" };
|
|
197
|
+
}
|
|
198
|
+
}, fullPath);
|
|
199
|
+
// Check if the endpoint exists and contains debug indicators
|
|
200
|
+
if (response.status >= 200 && response.status < 400) {
|
|
201
|
+
const bodyLower = response.body.toLowerCase();
|
|
202
|
+
// For endpoints with no indicators (like heap dumps), 200 status is enough
|
|
203
|
+
const hasIndicators = endpoint.indicators.length === 0 ||
|
|
204
|
+
endpoint.indicators.some(ind => bodyLower.includes(ind.toLowerCase()));
|
|
205
|
+
if (hasIndicators) {
|
|
206
|
+
this.reportedPaths.add(fullPath);
|
|
207
|
+
vulnerabilities.push(this.createVulnerability(`Exposed ${endpoint.name}`, `The ${endpoint.name} endpoint is accessible at ${endpoint.path}. ` +
|
|
208
|
+
`This can expose sensitive internal information, configuration details, or debug data.`, fullPath, endpoint.severity, `HTTP ${response.status} at ${endpoint.path} (${response.body.substring(0, 100)}...)`, endpoint.remediation, "CWE-215"));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Endpoint not reachable, skip silently
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return vulnerabilities;
|
|
217
|
+
}
|
|
218
|
+
reset() {
|
|
219
|
+
this.reportedPaths.clear();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
exports.DebugEndpointPlugin = DebugEndpointPlugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext, VulnerabilityTestResult } from "../types";
|
|
2
|
+
export declare class DirectoryTraversalPlugin extends BaseVulnerabilityPlugin {
|
|
3
|
+
readonly name = "Directory Traversal / LFI Detector";
|
|
4
|
+
readonly type: "lfi";
|
|
5
|
+
readonly description = "Tests for path traversal and local file inclusion vulnerabilities";
|
|
6
|
+
/**
|
|
7
|
+
* Payloads designed to detect directory traversal / LFI.
|
|
8
|
+
* Each payload targets a well-known file with distinctive content markers.
|
|
9
|
+
*/
|
|
10
|
+
private readonly payloads;
|
|
11
|
+
testParameter(context: PluginContext, param: string, value: string): Promise<VulnerabilityTestResult>;
|
|
12
|
+
private buildTestUrl;
|
|
13
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DirectoryTraversalPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class DirectoryTraversalPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "Directory Traversal / LFI Detector";
|
|
7
|
+
type = "lfi";
|
|
8
|
+
description = "Tests for path traversal and local file inclusion vulnerabilities";
|
|
9
|
+
/**
|
|
10
|
+
* Payloads designed to detect directory traversal / LFI.
|
|
11
|
+
* Each payload targets a well-known file with distinctive content markers.
|
|
12
|
+
*/
|
|
13
|
+
payloads = [
|
|
14
|
+
// Linux/Unix path traversal
|
|
15
|
+
{
|
|
16
|
+
payload: "../../../etc/passwd",
|
|
17
|
+
markers: ["root:", "/bin/bash", "/bin/sh", "nobody:"],
|
|
18
|
+
os: "linux",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
payload: "....//....//....//etc/passwd",
|
|
22
|
+
markers: ["root:", "/bin/bash", "/bin/sh"],
|
|
23
|
+
os: "linux",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
payload: "..%2F..%2F..%2Fetc%2Fpasswd",
|
|
27
|
+
markers: ["root:", "/bin/bash", "/bin/sh"],
|
|
28
|
+
os: "linux",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
payload: "%2e%2e/%2e%2e/%2e%2e/etc/passwd",
|
|
32
|
+
markers: ["root:", "/bin/bash"],
|
|
33
|
+
os: "linux",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
payload: "....\\\\....\\\\....\\\\etc\\\\passwd",
|
|
37
|
+
markers: ["root:", "/bin/bash"],
|
|
38
|
+
os: "linux",
|
|
39
|
+
},
|
|
40
|
+
// Windows path traversal
|
|
41
|
+
{
|
|
42
|
+
payload: "..\\..\\..\\windows\\win.ini",
|
|
43
|
+
markers: ["[fonts]", "[extensions]", "[mci extensions]"],
|
|
44
|
+
os: "windows",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
payload: "..%5C..%5C..%5Cwindows%5Cwin.ini",
|
|
48
|
+
markers: ["[fonts]", "[extensions]"],
|
|
49
|
+
os: "windows",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
payload: "../../../windows/win.ini",
|
|
53
|
+
markers: ["[fonts]", "[extensions]"],
|
|
54
|
+
os: "windows",
|
|
55
|
+
},
|
|
56
|
+
// Null byte injection (legacy PHP)
|
|
57
|
+
{
|
|
58
|
+
payload: "../../../etc/passwd%00",
|
|
59
|
+
markers: ["root:", "/bin/bash"],
|
|
60
|
+
os: "linux",
|
|
61
|
+
},
|
|
62
|
+
// Double encoding
|
|
63
|
+
{
|
|
64
|
+
payload: "%252e%252e%252f%252e%252e%252f%252e%252e%252fetc%252fpasswd",
|
|
65
|
+
markers: ["root:", "/bin/bash"],
|
|
66
|
+
os: "linux",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
async testParameter(context, param, value) {
|
|
70
|
+
for (const { payload, markers, os } of this.payloads) {
|
|
71
|
+
try {
|
|
72
|
+
const testUrl = this.buildTestUrl(context.url, param, payload);
|
|
73
|
+
const response = await context.page.evaluate(async (url) => {
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: "GET",
|
|
77
|
+
redirect: "follow",
|
|
78
|
+
credentials: "omit",
|
|
79
|
+
});
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
return { status: res.status, body: text.substring(0, 5000) };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { status: 0, body: "" };
|
|
85
|
+
}
|
|
86
|
+
}, testUrl);
|
|
87
|
+
if (response.status >= 200 && response.status < 500) {
|
|
88
|
+
const bodyLower = response.body.toLowerCase();
|
|
89
|
+
const matchedMarkers = markers.filter(m => bodyLower.includes(m.toLowerCase()));
|
|
90
|
+
if (matchedMarkers.length >= 2) {
|
|
91
|
+
return this.success(this.createVulnerability("Directory Traversal / Local File Inclusion", `The parameter '${param}' is vulnerable to directory traversal. ` +
|
|
92
|
+
`An attacker can read arbitrary files from the server filesystem (${os}).`, context.url, "critical", `Payload: ${param}=${payload} — matched markers: ${matchedMarkers.join(", ")}`, "Validate and sanitize all file path inputs. Use a whitelist of allowed files/directories. " +
|
|
93
|
+
"Never pass user input directly to file system operations. " +
|
|
94
|
+
"Use path.resolve() and verify the resolved path starts with the expected base directory.", "CWE-22"));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Skip this payload, try next
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return this.failure();
|
|
103
|
+
}
|
|
104
|
+
buildTestUrl(baseUrl, param, payload) {
|
|
105
|
+
const url = new URL(baseUrl);
|
|
106
|
+
url.searchParams.set(param, payload);
|
|
107
|
+
return url.toString();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.DirectoryTraversalPlugin = DirectoryTraversalPlugin;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
import { Vulnerability } from "../../core/vulnerability-detector";
|
|
3
|
+
export declare class OpenRedirectPlugin extends BaseVulnerabilityPlugin {
|
|
4
|
+
readonly name = "Open Redirect Detector";
|
|
5
|
+
readonly type: "redirect";
|
|
6
|
+
readonly description = "Detects open redirect vulnerabilities in URL parameters";
|
|
7
|
+
private readonly redirectParams;
|
|
8
|
+
private readonly testDomains;
|
|
9
|
+
analyzeContent(context: PluginContext, content: string): Promise<Vulnerability[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenRedirectPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class OpenRedirectPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "Open Redirect Detector";
|
|
7
|
+
type = "redirect";
|
|
8
|
+
description = "Detects open redirect vulnerabilities in URL parameters";
|
|
9
|
+
redirectParams = [
|
|
10
|
+
"url", "redirect", "redirect_url", "redirect_uri", "return", "return_url",
|
|
11
|
+
"returnTo", "next", "next_url", "goto", "target", "dest", "destination",
|
|
12
|
+
"rurl", "redir", "out", "continue", "forward", "callback", "callback_url",
|
|
13
|
+
"path", "ref", "site", "view", "to", "link", "logout_redirect",
|
|
14
|
+
];
|
|
15
|
+
testDomains = [
|
|
16
|
+
"https://evil.com",
|
|
17
|
+
"//evil.com",
|
|
18
|
+
"https://evil.com%2f%2f",
|
|
19
|
+
"/\\evil.com",
|
|
20
|
+
"https:evil.com",
|
|
21
|
+
];
|
|
22
|
+
async analyzeContent(context, content) {
|
|
23
|
+
const vulnerabilities = [];
|
|
24
|
+
const url = new URL(context.url);
|
|
25
|
+
// Check if the current URL uses any redirect-like parameters
|
|
26
|
+
for (const param of this.redirectParams) {
|
|
27
|
+
const value = url.searchParams.get(param);
|
|
28
|
+
if (value) {
|
|
29
|
+
// Current URL has a redirect param — test it
|
|
30
|
+
for (const testDomain of this.testDomains) {
|
|
31
|
+
try {
|
|
32
|
+
const testUrl = new URL(context.url);
|
|
33
|
+
testUrl.searchParams.set(param, testDomain);
|
|
34
|
+
const result = await context.page.evaluate(async (targetUrl) => {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(targetUrl, {
|
|
37
|
+
method: "GET",
|
|
38
|
+
redirect: "manual",
|
|
39
|
+
credentials: "omit",
|
|
40
|
+
});
|
|
41
|
+
const location = res.headers.get("location") || "";
|
|
42
|
+
return {
|
|
43
|
+
status: res.status,
|
|
44
|
+
location,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { status: 0, location: "" };
|
|
49
|
+
}
|
|
50
|
+
}, testUrl.toString());
|
|
51
|
+
if (result.status >= 300 && result.status < 400 &&
|
|
52
|
+
result.location.includes("evil.com")) {
|
|
53
|
+
vulnerabilities.push(this.createVulnerability("Open Redirect", `The parameter '${param}' allows redirection to arbitrary external URLs. ` +
|
|
54
|
+
`Attackers can use this for phishing by crafting legitimate-looking URLs that redirect to malicious sites.`, context.url, "medium", `${param}=${testDomain} → Location: ${result.location}`, "Validate redirect URLs against a whitelist of allowed domains. " +
|
|
55
|
+
"Use relative paths instead of full URLs. " +
|
|
56
|
+
"Never redirect to user-supplied URLs without validation.", "CWE-601"));
|
|
57
|
+
break; // One proof is enough for this param
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Skip this test
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return vulnerabilities;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.OpenRedirectPlugin = OpenRedirectPlugin;
|
|
@@ -40,7 +40,17 @@ exports.pdfGenerator = exports.PdfGenerator = void 0;
|
|
|
40
40
|
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
41
41
|
const scan_storage_1 = require("../core/scan-storage");
|
|
42
42
|
const path_1 = __importDefault(require("path"));
|
|
43
|
-
function escapeHtml(text) {
|
|
43
|
+
function escapeHtml(text) {
|
|
44
|
+
if (!text)
|
|
45
|
+
return "";
|
|
46
|
+
return text
|
|
47
|
+
.toString()
|
|
48
|
+
.replace(/&/g, "&")
|
|
49
|
+
.replace(/</g, "<")
|
|
50
|
+
.replace(/>/g, ">")
|
|
51
|
+
.replace(/"/g, """)
|
|
52
|
+
.replace(/'/g, "'");
|
|
53
|
+
}
|
|
44
54
|
function sanitizeFilenamePart(value) {
|
|
45
55
|
return value
|
|
46
56
|
.replace(/[<>:"\/\\|?*\x00-\x1F]/g, "_")
|
|
@@ -307,6 +317,21 @@ function buildPdfHtml(data) {
|
|
|
307
317
|
</div>
|
|
308
318
|
|
|
309
319
|
<div class="sub">Crawled URLs: <span class="mono">${scanResult.metadata.crawledUrls}</span> | Forms tested: <span class="mono">${scanResult.metadata.testedForms}</span> | Requests: <span class="mono">${scanResult.metadata.requestsMade}</span> | Duration: <span class="mono">${(scanResult.duration / 1000).toFixed(2)}s</span></div>
|
|
320
|
+
|
|
321
|
+
<div style="margin-top: 20px; display: flex; align-items: center; gap: 15px; background: rgba(17,26,51,0.55); padding: 15px; border-radius: 12px; border: 1px solid var(--line);">
|
|
322
|
+
<div style="font-size: 24px; font-weight: 900; color: ${scanResult.score > 80 ? "#52c41a" : (scanResult.score > 50 ? "#faad14" : "#ff4d4f")};">
|
|
323
|
+
${scanResult.score}/100
|
|
324
|
+
</div>
|
|
325
|
+
<div style="flex-grow: 1;">
|
|
326
|
+
<div style="font-size: 11px; text-transform: uppercase; color: var(--muted); letter-spacing: 0.5px; margin-bottom: 5px;">Security Posture</div>
|
|
327
|
+
<div style="height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden;">
|
|
328
|
+
<div style="width: ${scanResult.score}%; height: 100%; background: ${scanResult.score > 80 ? "#52c41a" : (scanResult.score > 50 ? "#faad14" : "#ff4d4f")};"></div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div style="font-size: 12px; font-weight: 700; color: var(--muted);">
|
|
332
|
+
${scanResult.score > 80 ? "EXCELLENT" : (scanResult.score > 50 ? "FAIR" : "POOR")}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
310
335
|
|
|
311
336
|
<div class="cards">
|
|
312
337
|
${rows || `<div class="card"><div class="title">No vulnerabilities found</div><div class="v">The scanner did not detect issues in the tested scope.</div></div>`}
|