kramscan 0.2.0 → 0.3.0
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 +6 -0
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +236 -0
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +109 -0
- package/dist/commands/scan.js +1 -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/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 +6 -3
|
@@ -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>`}
|
package/dist/utils/theme.d.ts
CHANGED
package/dist/utils/theme.js
CHANGED
|
@@ -127,7 +127,7 @@ function getSeverityColor(severity) {
|
|
|
127
127
|
}
|
|
128
128
|
// ─── Scan Summary Display ──────────────────────────────────────────
|
|
129
129
|
function displayScanSummary(result) {
|
|
130
|
-
const { target, duration, metadata, summary, vulnerabilities, filepath, pdfPath } = result;
|
|
130
|
+
const { target, duration, metadata, summary, vulnerabilities, filepath, pdfPath, score } = result;
|
|
131
131
|
// Scan Summary
|
|
132
132
|
console.log("");
|
|
133
133
|
console.log(exports.theme.brightWhite.bold("📊 Scan Summary"));
|
|
@@ -138,6 +138,12 @@ function displayScanSummary(result) {
|
|
|
138
138
|
console.log(exports.theme.white("URLs Crawled:"), exports.theme.cyan(metadata.crawledUrls));
|
|
139
139
|
console.log(exports.theme.white("Forms Tested:"), exports.theme.cyan(metadata.testedForms));
|
|
140
140
|
console.log(exports.theme.white("Requests Made:"), exports.theme.cyan(metadata.requestsMade));
|
|
141
|
+
// Security Score Display
|
|
142
|
+
const scoreColor = score > 80 ? exports.theme.success : (score > 50 ? exports.theme.warning : exports.theme.error);
|
|
143
|
+
const scoreLabel = score > 80 ? "EXCELLENT" : (score > 50 ? "FAIR" : "POOR");
|
|
144
|
+
console.log("");
|
|
145
|
+
console.log(` ${exports.theme.white("Security Score:")} ${scoreColor.bold(score + "/100")} ${exports.theme.gray(`(${scoreLabel})`)}`);
|
|
146
|
+
console.log(` ${scoreColor("█".repeat(Math.round(score / 5)) + exports.theme.dim("█".repeat(20 - Math.round(score / 5))))}`);
|
|
141
147
|
console.log("");
|
|
142
148
|
// Vulnerability summary
|
|
143
149
|
console.log(exports.theme.brightWhite.bold("🛡️ Vulnerabilities Found"));
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kramscan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "KramScan CLI — AI-powered web app security testing",
|
|
5
|
-
"author": "Akram Shaikh
|
|
5
|
+
"author": "Akram Shaikh (https://akramshaikh.me)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"security",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"web-security",
|
|
14
14
|
"analysis"
|
|
15
15
|
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
16
19
|
"repository": {
|
|
17
20
|
"type": "git",
|
|
18
21
|
"url": "https://github.com/shaikhakramshakil/kramscan.git"
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
"lint": "eslint src --ext .ts",
|
|
46
49
|
"lint:fix": "eslint src --ext .ts --fix",
|
|
47
50
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
48
|
-
"prepublishOnly": "npm run clean && npm run build"
|
|
51
|
+
"prepublishOnly": "npm test && npm run clean && npm run build"
|
|
49
52
|
},
|
|
50
53
|
"dependencies": {
|
|
51
54
|
"@anthropic-ai/sdk": "^0.31.0",
|