kramscan 0.1.1 → 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 +419 -236
- package/dist/agent/confirmation.d.ts +5 -1
- package/dist/agent/confirmation.js +29 -9
- package/dist/agent/context.js +2 -3
- package/dist/agent/orchestrator.d.ts +2 -0
- package/dist/agent/orchestrator.js +50 -8
- package/dist/agent/prompts/system.d.ts +1 -1
- package/dist/agent/prompts/system.js +5 -7
- package/dist/agent/skills/health-check.js +22 -2
- package/dist/agent/skills/index.d.ts +1 -0
- package/dist/agent/skills/index.js +3 -1
- package/dist/agent/skills/verify-finding.d.ts +17 -0
- package/dist/agent/skills/verify-finding.js +91 -0
- package/dist/agent/skills/web-scan.js +46 -0
- package/dist/cli.js +156 -149
- package/dist/commands/agent.js +38 -38
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +112 -0
- package/dist/commands/analyze.js +103 -54
- package/dist/commands/config.js +55 -29
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +236 -0
- package/dist/commands/doctor.js +20 -15
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +109 -0
- package/dist/commands/onboard.js +188 -141
- package/dist/commands/report.js +68 -76
- package/dist/commands/scan.js +262 -81
- package/dist/commands/scans.d.ts +2 -0
- package/dist/commands/scans.js +55 -0
- package/dist/core/ai-client.d.ts +6 -1
- package/dist/core/ai-client.js +80 -12
- package/dist/core/ai-payloads.d.ts +17 -0
- package/dist/core/ai-payloads.js +54 -0
- package/dist/core/config-schema.d.ts +197 -0
- package/dist/core/config-schema.js +68 -0
- package/dist/core/config-schema.test.d.ts +1 -0
- package/dist/core/config-schema.test.js +151 -0
- package/dist/core/config.d.ts +8 -31
- package/dist/core/config.js +71 -14
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +162 -0
- package/dist/core/scan-index.d.ts +20 -0
- package/dist/core/scan-index.js +52 -0
- package/dist/core/scan-storage.d.ts +11 -0
- package/dist/core/scan-storage.js +69 -0
- package/dist/core/scanner.d.ts +95 -13
- package/dist/core/scanner.js +342 -248
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +9 -0
- package/dist/core/vulnerability-detector.js +46 -15
- package/dist/core/vulnerability-detector.test.d.ts +1 -0
- package/dist/core/vulnerability-detector.test.js +210 -0
- package/dist/index.js +3 -0
- package/dist/plugins/PluginManager.d.ts +27 -0
- package/dist/plugins/PluginManager.js +166 -0
- package/dist/plugins/index.d.ts +12 -0
- package/dist/plugins/index.js +29 -0
- package/dist/plugins/types.d.ts +55 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -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/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
- package/dist/reports/PdfGenerator.d.ts +36 -0
- package/dist/reports/PdfGenerator.js +404 -0
- package/dist/utils/logger.d.ts +33 -1
- package/dist/utils/logger.js +127 -8
- package/dist/utils/theme.d.ts +56 -0
- package/dist/utils/theme.js +201 -0
- package/package.json +6 -3
package/dist/core/scanner.js
CHANGED
|
@@ -5,10 +5,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Scanner = void 0;
|
|
7
7
|
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
8
|
+
const events_1 = require("events");
|
|
8
9
|
const vulnerability_detector_1 = require("./vulnerability-detector");
|
|
9
10
|
const config_1 = require("./config");
|
|
11
|
+
const ai_client_1 = require("./ai-client");
|
|
12
|
+
const ai_payloads_1 = require("./ai-payloads");
|
|
10
13
|
const logger_1 = require("../utils/logger");
|
|
11
|
-
|
|
14
|
+
const plugins_1 = require("../plugins");
|
|
15
|
+
const plugins_2 = require("../plugins");
|
|
16
|
+
class Scanner extends events_1.EventEmitter {
|
|
12
17
|
browser = null;
|
|
13
18
|
detector;
|
|
14
19
|
visitedUrls = new Set();
|
|
@@ -18,21 +23,128 @@ class Scanner {
|
|
|
18
23
|
headersChecked = new Set();
|
|
19
24
|
rateLimiter;
|
|
20
25
|
retryConfig;
|
|
21
|
-
|
|
26
|
+
maxConcurrency = 5;
|
|
27
|
+
strictScope = true;
|
|
28
|
+
baseOrigin = null;
|
|
29
|
+
maxPages = 30;
|
|
30
|
+
maxLinksPerPage = 50;
|
|
31
|
+
includePatterns = [];
|
|
32
|
+
excludePatterns = [];
|
|
33
|
+
userAgent = "KramScan/0.2.0";
|
|
34
|
+
scanErrors = [];
|
|
35
|
+
pluginErrors = new Map();
|
|
36
|
+
usePlugins = true;
|
|
37
|
+
useAiPayloads = false;
|
|
38
|
+
payloadGenerator = null;
|
|
39
|
+
constructor(usePlugins = true) {
|
|
40
|
+
super();
|
|
41
|
+
this.usePlugins = usePlugins;
|
|
22
42
|
this.detector = new vulnerability_detector_1.VulnerabilityDetector();
|
|
43
|
+
this.detector.setOnVulnerabilityFound((vuln) => {
|
|
44
|
+
this.emit("vuln:found", { vulnerability: vuln });
|
|
45
|
+
});
|
|
23
46
|
this.rateLimiter = {
|
|
24
47
|
lastRequestTime: 0,
|
|
25
|
-
minInterval: 200,
|
|
48
|
+
minInterval: 200,
|
|
26
49
|
};
|
|
27
50
|
this.retryConfig = {
|
|
28
51
|
maxRetries: 3,
|
|
29
52
|
baseDelay: 1000,
|
|
30
53
|
maxDelay: 10000,
|
|
31
54
|
};
|
|
55
|
+
// Register default plugins
|
|
56
|
+
if (usePlugins) {
|
|
57
|
+
this.registerDefaultPlugins();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
registerDefaultPlugins() {
|
|
61
|
+
plugins_1.pluginManager.register(new plugins_2.XSSPlugin());
|
|
62
|
+
plugins_1.pluginManager.register(new plugins_2.SQLInjectionPlugin());
|
|
63
|
+
plugins_1.pluginManager.register(new plugins_2.SecurityHeadersPlugin());
|
|
64
|
+
plugins_1.pluginManager.register(new plugins_2.SensitiveDataPlugin());
|
|
65
|
+
plugins_1.pluginManager.register(new plugins_2.CSRFPlugin());
|
|
66
|
+
plugins_1.pluginManager.register(new plugins_2.CORSAnalyzerPlugin());
|
|
67
|
+
plugins_1.pluginManager.register(new plugins_2.DebugEndpointPlugin());
|
|
68
|
+
plugins_1.pluginManager.register(new plugins_2.DirectoryTraversalPlugin());
|
|
69
|
+
plugins_1.pluginManager.register(new plugins_2.CookieSecurityPlugin());
|
|
70
|
+
plugins_1.pluginManager.register(new plugins_2.OpenRedirectPlugin());
|
|
71
|
+
}
|
|
72
|
+
// Type-safe event emitter methods
|
|
73
|
+
emit(event, data) {
|
|
74
|
+
return super.emit(event, data);
|
|
75
|
+
}
|
|
76
|
+
on(event, listener) {
|
|
77
|
+
return super.on(event, listener);
|
|
32
78
|
}
|
|
33
|
-
|
|
79
|
+
once(event, listener) {
|
|
80
|
+
return super.once(event, listener);
|
|
81
|
+
}
|
|
82
|
+
getScanErrors() {
|
|
83
|
+
return [...this.scanErrors];
|
|
84
|
+
}
|
|
85
|
+
getPluginErrors() {
|
|
86
|
+
return new Map(this.pluginErrors);
|
|
87
|
+
}
|
|
88
|
+
async initializeScanSettings(targetUrl, options) {
|
|
34
89
|
const config = await (0, config_1.getConfig)();
|
|
35
90
|
this.rateLimiter.minInterval = 1000 / (config.scan.rateLimitPerSecond || 5);
|
|
91
|
+
this.maxConcurrency = Math.max(1, config.scan.maxThreads || 5);
|
|
92
|
+
this.strictScope = options.strictScope ?? (config.scan.strictScope ?? true);
|
|
93
|
+
this.baseOrigin = new URL(targetUrl).origin;
|
|
94
|
+
this.userAgent = config.scan.userAgent || this.userAgent;
|
|
95
|
+
// Load scan profile
|
|
96
|
+
const profileName = options.profile || config.scan.defaultProfile || "balanced";
|
|
97
|
+
const profile = await (0, config_1.getScanProfile)(profileName);
|
|
98
|
+
this.maxPages = Math.max(1, options.maxPages ?? profile?.maxPages ?? 30);
|
|
99
|
+
this.maxLinksPerPage = Math.max(1, options.maxLinksPerPage ?? profile?.maxLinksPerPage ?? 50);
|
|
100
|
+
// Initialize AI payload generator if requested and AI is enabled
|
|
101
|
+
this.useAiPayloads = options.useAiPayloads ?? false;
|
|
102
|
+
if (this.useAiPayloads && config.ai.enabled) {
|
|
103
|
+
try {
|
|
104
|
+
const aiClient = await (0, ai_client_1.createAIClient)();
|
|
105
|
+
this.payloadGenerator = new ai_payloads_1.PayloadGenerator(aiClient);
|
|
106
|
+
logger_1.logger.info("AI contextual payload generation enabled");
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
logger_1.logger.warn(`Failed to initialize AI payload generator: ${err.message}`);
|
|
110
|
+
this.useAiPayloads = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const compileList = (values) => {
|
|
114
|
+
if (!values || values.length === 0)
|
|
115
|
+
return [];
|
|
116
|
+
const patterns = [];
|
|
117
|
+
for (const raw of values) {
|
|
118
|
+
try {
|
|
119
|
+
patterns.push(new RegExp(raw));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
logger_1.logger.warn(`Invalid regex pattern ignored: ${raw}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return patterns;
|
|
126
|
+
};
|
|
127
|
+
this.includePatterns = compileList(options.include);
|
|
128
|
+
this.excludePatterns = compileList(options.exclude);
|
|
129
|
+
}
|
|
130
|
+
resetScanState() {
|
|
131
|
+
this.detector.clear();
|
|
132
|
+
this.visitedUrls.clear();
|
|
133
|
+
this.crawledUrls = 0;
|
|
134
|
+
this.testedForms = 0;
|
|
135
|
+
this.requestsMade = 0;
|
|
136
|
+
this.headersChecked.clear();
|
|
137
|
+
this.scanErrors = [];
|
|
138
|
+
this.pluginErrors.clear();
|
|
139
|
+
this.rateLimiter.lastRequestTime = 0;
|
|
140
|
+
this.removeAllListeners();
|
|
141
|
+
// Reset plugin manager state
|
|
142
|
+
if (this.usePlugins) {
|
|
143
|
+
const securityHeadersPlugin = plugins_1.pluginManager.getPlugin("Security Headers Analyzer");
|
|
144
|
+
if (securityHeadersPlugin && typeof securityHeadersPlugin.reset === "function") {
|
|
145
|
+
securityHeadersPlugin.reset();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
36
148
|
}
|
|
37
149
|
async initialize(options = {}) {
|
|
38
150
|
const headless = options.headless ?? true;
|
|
@@ -51,16 +163,20 @@ class Scanner {
|
|
|
51
163
|
const startTime = Date.now();
|
|
52
164
|
const depth = options.depth ?? 2;
|
|
53
165
|
const timeout = options.timeout ?? 30000;
|
|
54
|
-
|
|
166
|
+
this.resetScanState();
|
|
167
|
+
await this.initializeScanSettings(targetUrl, options);
|
|
55
168
|
if (!this.browser) {
|
|
56
169
|
await this.initialize(options);
|
|
57
170
|
}
|
|
171
|
+
this.emit("scan:start", { target: targetUrl, options });
|
|
58
172
|
logger_1.logger.info(`Starting scan of ${targetUrl} (depth: ${depth}, timeout: ${timeout}ms)`);
|
|
59
173
|
try {
|
|
60
174
|
await this.crawl(targetUrl, depth, timeout);
|
|
61
175
|
}
|
|
62
176
|
catch (error) {
|
|
63
|
-
|
|
177
|
+
const err = error;
|
|
178
|
+
this.emit("scan:error", { error: err });
|
|
179
|
+
logger_1.logger.error(`Scan failed: ${err.message}`);
|
|
64
180
|
throw error;
|
|
65
181
|
}
|
|
66
182
|
finally {
|
|
@@ -78,7 +194,9 @@ class Scanner {
|
|
|
78
194
|
testedForms: this.testedForms,
|
|
79
195
|
requestsMade: this.requestsMade,
|
|
80
196
|
},
|
|
197
|
+
score: this.detector.calculateScore(),
|
|
81
198
|
};
|
|
199
|
+
this.emit("scan:complete", { result });
|
|
82
200
|
return result;
|
|
83
201
|
}
|
|
84
202
|
async applyRateLimit() {
|
|
@@ -108,62 +226,198 @@ class Scanner {
|
|
|
108
226
|
}
|
|
109
227
|
throw new Error(`Failed after ${this.retryConfig.maxRetries + 1} attempts: ${lastError?.message}`);
|
|
110
228
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
229
|
+
async createInstrumentedPage() {
|
|
230
|
+
const page = await this.browser.newPage();
|
|
231
|
+
await page.setUserAgent(this.userAgent);
|
|
232
|
+
await page.setRequestInterception(true);
|
|
233
|
+
page.on("request", (request) => {
|
|
234
|
+
this.requestsMade++;
|
|
235
|
+
const resourceType = request.resourceType();
|
|
236
|
+
if (resourceType === "image" || resourceType === "font" || resourceType === "media") {
|
|
237
|
+
request.abort();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
request.continue();
|
|
241
|
+
});
|
|
242
|
+
return page;
|
|
243
|
+
}
|
|
244
|
+
async runInIsolatedPage(operation) {
|
|
245
|
+
const page = await this.createInstrumentedPage();
|
|
246
|
+
try {
|
|
247
|
+
return await operation(page);
|
|
115
248
|
}
|
|
116
|
-
|
|
117
|
-
|
|
249
|
+
finally {
|
|
250
|
+
await page.close().catch((err) => logger_1.logger.debug(`Error closing page: ${err.message}`));
|
|
118
251
|
}
|
|
119
|
-
return payload;
|
|
120
252
|
}
|
|
121
253
|
async crawl(url, depth, timeout) {
|
|
254
|
+
if (this.crawledUrls >= this.maxPages) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
122
257
|
if (depth === 0 || this.visitedUrls.has(url)) {
|
|
123
258
|
return;
|
|
124
259
|
}
|
|
125
260
|
this.visitedUrls.add(url);
|
|
126
261
|
this.crawledUrls++;
|
|
127
|
-
|
|
262
|
+
this.emit("crawl:start", { url, depth });
|
|
263
|
+
this.emit("crawl:page", { url, crawledCount: this.crawledUrls, maxPages: this.maxPages });
|
|
264
|
+
this.emit("progress", {
|
|
265
|
+
stage: "crawling",
|
|
266
|
+
current: this.crawledUrls,
|
|
267
|
+
total: this.maxPages,
|
|
268
|
+
message: `Crawling: ${url}`
|
|
269
|
+
});
|
|
270
|
+
const page = await this.createInstrumentedPage();
|
|
128
271
|
try {
|
|
129
|
-
await page.setRequestInterception(true);
|
|
130
|
-
page.on("request", (request) => {
|
|
131
|
-
this.requestsMade++;
|
|
132
|
-
request.continue();
|
|
133
|
-
});
|
|
134
272
|
const response = await this.withRetry(() => page.goto(url, { waitUntil: "networkidle2", timeout }), `crawl ${url}`);
|
|
135
273
|
if (!response) {
|
|
136
274
|
logger_1.logger.warn(`No response from ${url}`);
|
|
275
|
+
this.scanErrors.push({ url, error: "No response" });
|
|
137
276
|
return;
|
|
138
277
|
}
|
|
139
|
-
const host = new URL(url).host;
|
|
140
|
-
if (!this.headersChecked.has(host)) {
|
|
141
|
-
this.detector.analyzeSecurityHeaders(url, response.headers());
|
|
142
|
-
this.headersChecked.add(host);
|
|
143
|
-
}
|
|
144
278
|
const content = await page.content();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
279
|
+
const headers = response.headers();
|
|
280
|
+
// Analyze with plugins
|
|
281
|
+
if (this.usePlugins) {
|
|
282
|
+
await this.runPlugins(page, url, content, headers, timeout);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// Fallback to legacy detector
|
|
286
|
+
await this.runLegacyDetection(page, url, content, headers, timeout);
|
|
287
|
+
}
|
|
149
288
|
if (depth > 1) {
|
|
150
289
|
const links = await this.extractLinks(page, url);
|
|
151
|
-
for (const link of links
|
|
290
|
+
for (const link of links) {
|
|
152
291
|
await this.crawl(link, depth - 1, timeout);
|
|
153
292
|
}
|
|
154
293
|
}
|
|
294
|
+
this.emit("crawl:complete", { url });
|
|
155
295
|
}
|
|
156
296
|
catch (error) {
|
|
157
|
-
|
|
297
|
+
const err = error;
|
|
298
|
+
this.scanErrors.push({ url, error: err.message });
|
|
299
|
+
this.emit("crawl:error", { url, error: err });
|
|
300
|
+
logger_1.logger.error(`Error crawling ${url}: ${err.message}`);
|
|
158
301
|
}
|
|
159
302
|
finally {
|
|
160
303
|
await page.close().catch(err => logger_1.logger.debug(`Error closing page: ${err.message}`));
|
|
161
304
|
}
|
|
162
305
|
}
|
|
163
|
-
async
|
|
306
|
+
async runPlugins(page, url, content, headers, timeout) {
|
|
307
|
+
const context = {
|
|
308
|
+
page,
|
|
309
|
+
url,
|
|
310
|
+
baseUrl: this.baseOrigin || url,
|
|
311
|
+
timeout,
|
|
312
|
+
userAgent: this.userAgent,
|
|
313
|
+
payloadGenerator: this.payloadGenerator,
|
|
314
|
+
};
|
|
315
|
+
// Analyze headers
|
|
316
|
+
const host = new URL(url).host;
|
|
317
|
+
if (!this.headersChecked.has(host)) {
|
|
318
|
+
const headerResults = await plugins_1.pluginManager.analyzeHeaders(context, headers);
|
|
319
|
+
this.processPluginResults(headerResults);
|
|
320
|
+
this.headersChecked.add(host);
|
|
321
|
+
}
|
|
322
|
+
// Analyze content
|
|
323
|
+
const contentResults = await plugins_1.pluginManager.analyzeContent(context, content);
|
|
324
|
+
this.processPluginResults(contentResults);
|
|
325
|
+
// Test URL parameters
|
|
326
|
+
await this.testUrlParametersWithPlugins(page, url, timeout);
|
|
327
|
+
// Test forms
|
|
328
|
+
await this.testFormsWithPlugins(page, url, timeout);
|
|
329
|
+
}
|
|
330
|
+
processPluginResults(results) {
|
|
331
|
+
for (const result of results) {
|
|
332
|
+
// Track plugin errors
|
|
333
|
+
if (result.errors.length > 0) {
|
|
334
|
+
const existing = this.pluginErrors.get(result.plugin) || [];
|
|
335
|
+
this.pluginErrors.set(result.plugin, [...existing, ...result.errors]);
|
|
336
|
+
}
|
|
337
|
+
// Add vulnerabilities to detector
|
|
338
|
+
for (const vuln of result.vulnerabilities) {
|
|
339
|
+
this.detector.addVulnerability(vuln);
|
|
340
|
+
}
|
|
341
|
+
// Emit event for monitoring
|
|
342
|
+
this.emit("plugin:execute", {
|
|
343
|
+
plugin: result.plugin,
|
|
344
|
+
url: result.errors[0]?.url || "",
|
|
345
|
+
duration: result.duration,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async testUrlParametersWithPlugins(page, url, timeout) {
|
|
350
|
+
try {
|
|
351
|
+
const urlObj = new URL(url);
|
|
352
|
+
const params = Array.from(urlObj.searchParams.keys());
|
|
353
|
+
for (const param of params) {
|
|
354
|
+
const value = urlObj.searchParams.get(param) || "";
|
|
355
|
+
const context = {
|
|
356
|
+
page,
|
|
357
|
+
url,
|
|
358
|
+
baseUrl: this.baseOrigin || url,
|
|
359
|
+
timeout,
|
|
360
|
+
userAgent: this.userAgent,
|
|
361
|
+
payloadGenerator: this.payloadGenerator,
|
|
362
|
+
};
|
|
363
|
+
const results = await plugins_1.pluginManager.testParameter(context, param, value);
|
|
364
|
+
this.processPluginResults(results);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
logger_1.logger.debug(`Error testing URL parameters with plugins: ${error.message}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async testFormsWithPlugins(page, url, timeout) {
|
|
164
372
|
const forms = await page.$$("form");
|
|
165
|
-
|
|
166
|
-
|
|
373
|
+
if (forms.length > 0) {
|
|
374
|
+
this.emit("form:test", { url, formCount: forms.length });
|
|
375
|
+
}
|
|
376
|
+
for (const form of forms) {
|
|
377
|
+
this.testedForms++;
|
|
378
|
+
try {
|
|
379
|
+
const inputs = await form.$$eval("input, textarea, select", (elements) => elements.map((el) => ({
|
|
380
|
+
name: el.getAttribute("name") || "",
|
|
381
|
+
type: el.getAttribute("type") || "text",
|
|
382
|
+
value: el.value || "",
|
|
383
|
+
})).filter((input) => input.name && input.type !== "hidden" && input.type !== "submit"));
|
|
384
|
+
const formData = {
|
|
385
|
+
action: await form.evaluate((el) => el.action || ""),
|
|
386
|
+
method: await form.evaluate((el) => el.method || "GET"),
|
|
387
|
+
inputs,
|
|
388
|
+
};
|
|
389
|
+
const context = {
|
|
390
|
+
page,
|
|
391
|
+
url,
|
|
392
|
+
baseUrl: this.baseOrigin || url,
|
|
393
|
+
timeout,
|
|
394
|
+
userAgent: this.userAgent,
|
|
395
|
+
payloadGenerator: this.payloadGenerator,
|
|
396
|
+
};
|
|
397
|
+
const results = await plugins_1.pluginManager.testFormInput(context, formData);
|
|
398
|
+
this.processPluginResults(results);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
logger_1.logger.debug(`Error testing form with plugins: ${error.message}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async runLegacyDetection(page, url, content, headers, timeout) {
|
|
406
|
+
const host = new URL(url).host;
|
|
407
|
+
if (!this.headersChecked.has(host)) {
|
|
408
|
+
this.detector.analyzeSecurityHeaders(url, headers);
|
|
409
|
+
this.headersChecked.add(host);
|
|
410
|
+
}
|
|
411
|
+
this.detector.detectSensitiveData(url, content);
|
|
412
|
+
this.detector.detectInfoDisclosure(url, content);
|
|
413
|
+
await this.testFormsLegacy(page, url, timeout);
|
|
414
|
+
await this.testUrlParametersLegacy(page, url, timeout);
|
|
415
|
+
}
|
|
416
|
+
async testFormsLegacy(page, url, timeout) {
|
|
417
|
+
const forms = await page.$$("form");
|
|
418
|
+
if (forms.length > 0) {
|
|
419
|
+
this.emit("form:test", { url, formCount: forms.length });
|
|
420
|
+
}
|
|
167
421
|
for (const form of forms) {
|
|
168
422
|
this.testedForms++;
|
|
169
423
|
try {
|
|
@@ -177,66 +431,60 @@ class Scanner {
|
|
|
177
431
|
if (!name || type === "hidden" || type === "submit") {
|
|
178
432
|
continue;
|
|
179
433
|
}
|
|
180
|
-
inputTests.push(() => this.testXSS(
|
|
181
|
-
inputTests.push(() => this.testSQLi(
|
|
182
|
-
inputTests.push(() => this.testLFI(page, url, name, timeout));
|
|
183
|
-
inputTests.push(() => this.testCMDI(page, url, name, timeout));
|
|
434
|
+
inputTests.push(() => this.runInIsolatedPage((testPage) => this.testXSS(testPage, url, name, timeout)));
|
|
435
|
+
inputTests.push(() => this.runInIsolatedPage((testPage) => this.testSQLi(testPage, url, name, timeout)));
|
|
184
436
|
}
|
|
185
|
-
|
|
186
|
-
await this.runWithConcurrency(inputTests, maxConcurrency);
|
|
437
|
+
await this.runWithConcurrency(inputTests, this.maxConcurrency);
|
|
187
438
|
}
|
|
188
439
|
catch (error) {
|
|
189
440
|
logger_1.logger.debug(`Error testing form: ${error.message}`);
|
|
190
441
|
}
|
|
191
442
|
}
|
|
192
443
|
}
|
|
193
|
-
async
|
|
194
|
-
const executing = [];
|
|
195
|
-
for (const task of tasks) {
|
|
196
|
-
const promise = task().catch((error) => {
|
|
197
|
-
logger_1.logger.debug(`Task failed: ${error.message}`);
|
|
198
|
-
return undefined;
|
|
199
|
-
});
|
|
200
|
-
executing.push(promise);
|
|
201
|
-
if (executing.length >= maxConcurrency) {
|
|
202
|
-
await Promise.race(executing);
|
|
203
|
-
executing.splice(executing.findIndex(p => p === promise), 1);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
await Promise.all(executing);
|
|
207
|
-
}
|
|
208
|
-
async testUrlParameters(page, baseUrl, timeout) {
|
|
444
|
+
async testUrlParametersLegacy(page, baseUrl, timeout) {
|
|
209
445
|
try {
|
|
210
446
|
const url = new URL(baseUrl);
|
|
211
447
|
const params = Array.from(url.searchParams.keys());
|
|
212
448
|
for (const param of params) {
|
|
213
|
-
await this.
|
|
214
|
-
await this.
|
|
215
|
-
await this.testPathTraversal(page, baseUrl, param, timeout);
|
|
216
|
-
await this.testCMDI(page, baseUrl, param, timeout);
|
|
217
|
-
await this.testSSRF(page, baseUrl, param, timeout);
|
|
218
|
-
await this.testOpenRedirect(page, baseUrl, param, timeout);
|
|
449
|
+
await this.testXSS(page, baseUrl, param, timeout);
|
|
450
|
+
await this.testSQLi(page, baseUrl, param, timeout);
|
|
219
451
|
}
|
|
220
452
|
}
|
|
221
453
|
catch (error) {
|
|
222
454
|
logger_1.logger.debug(`Error testing URL parameters: ${error.message}`);
|
|
223
455
|
}
|
|
224
456
|
}
|
|
457
|
+
async runWithConcurrency(tasks, maxConcurrency) {
|
|
458
|
+
if (tasks.length === 0) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const workerCount = Math.max(1, Math.min(maxConcurrency, tasks.length));
|
|
462
|
+
let nextIndex = 0;
|
|
463
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
464
|
+
while (nextIndex < tasks.length) {
|
|
465
|
+
const currentTask = tasks[nextIndex++];
|
|
466
|
+
try {
|
|
467
|
+
await currentTask();
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
logger_1.logger.debug(`Task failed: ${error.message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
await Promise.all(workers);
|
|
475
|
+
}
|
|
225
476
|
async testXSS(page, url, param, timeout) {
|
|
226
477
|
const payloads = [
|
|
227
478
|
"<script>alert('XSS')</script>",
|
|
228
479
|
'"><script>alert(1)</script>',
|
|
229
480
|
"<img src=x onerror=alert(1)>",
|
|
230
|
-
"'-alert(1)-'",
|
|
231
|
-
"<svg/onload=alert(1)>",
|
|
232
481
|
];
|
|
233
482
|
for (const payload of payloads) {
|
|
234
483
|
try {
|
|
235
|
-
const
|
|
236
|
-
const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
|
|
484
|
+
const testUrl = this.buildTestUrl(url, param, payload);
|
|
237
485
|
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `XSS test for ${param}`);
|
|
238
486
|
const content = await page.content();
|
|
239
|
-
this.detector.detectXSS(url, param,
|
|
487
|
+
this.detector.detectXSS(url, param, payload, content);
|
|
240
488
|
}
|
|
241
489
|
catch (error) {
|
|
242
490
|
logger_1.logger.debug(`XSS test failed for ${param}: ${error.message}`);
|
|
@@ -244,31 +492,11 @@ class Scanner {
|
|
|
244
492
|
}
|
|
245
493
|
}
|
|
246
494
|
async testSQLi(page, url, param, timeout) {
|
|
247
|
-
const errorBasedPayloads = [
|
|
248
|
-
"'",
|
|
249
|
-
"1' OR '1'='1",
|
|
250
|
-
"1; DROP TABLE users--",
|
|
251
|
-
"' OR 1=1--",
|
|
252
|
-
"' UNION SELECT 1--",
|
|
253
|
-
"1' AND '1'='1",
|
|
254
|
-
];
|
|
255
|
-
const timeBasedPayloads = [
|
|
256
|
-
"' AND SLEEP(5)--",
|
|
257
|
-
"1' AND SLEEP(5)--",
|
|
258
|
-
"'; WAITFOR DELAY '00:00:05'--",
|
|
259
|
-
];
|
|
260
|
-
try {
|
|
261
|
-
await page.content();
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
logger_1.logger.debug(`Could not get original response for SQLi test: ${error.message}`);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
495
|
+
const errorBasedPayloads = ["'", "1' OR '1'='1", "' OR 1=1--"];
|
|
267
496
|
for (const payload of errorBasedPayloads) {
|
|
268
497
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `SQLi error test for ${param}`);
|
|
498
|
+
const testUrl = this.buildTestUrl(url, param, payload);
|
|
499
|
+
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `SQLi test for ${param}`);
|
|
272
500
|
const content = await page.content();
|
|
273
501
|
this.detector.detectSQLi(url, param, content);
|
|
274
502
|
}
|
|
@@ -276,162 +504,6 @@ class Scanner {
|
|
|
276
504
|
logger_1.logger.debug(`SQLi test failed for ${param}: ${error.message}`);
|
|
277
505
|
}
|
|
278
506
|
}
|
|
279
|
-
const startTime = Date.now();
|
|
280
|
-
for (const payload of timeBasedPayloads) {
|
|
281
|
-
try {
|
|
282
|
-
const sanitizedPayload = this.sanitizePayload(payload);
|
|
283
|
-
const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
|
|
284
|
-
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `Blind SQLi test for ${param}`);
|
|
285
|
-
const testTime = Date.now() - startTime;
|
|
286
|
-
this.detector.detectBlindSQLi(url, param, 0, testTime);
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
logger_1.logger.debug(`Blind SQLi test failed for ${param}: ${error.message}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
async testLFI(page, url, param, timeout) {
|
|
294
|
-
const lfiPayloads = [
|
|
295
|
-
"../../../../etc/passwd",
|
|
296
|
-
"../../../../../../etc/passwd",
|
|
297
|
-
"..\\..\\..\\..\\..\\windows\\system32\\drivers\\etc\\hosts",
|
|
298
|
-
"..%2f..%2f..%2f..%2fetc%2fpasswd",
|
|
299
|
-
"/etc/passwd",
|
|
300
|
-
"/etc/shadow",
|
|
301
|
-
];
|
|
302
|
-
for (const payload of lfiPayloads) {
|
|
303
|
-
try {
|
|
304
|
-
const sanitizedPayload = this.sanitizePayload(payload);
|
|
305
|
-
const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
|
|
306
|
-
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `LFI test for ${param}`);
|
|
307
|
-
const content = await page.content();
|
|
308
|
-
this.detector.detectLFI(url, param, sanitizedPayload, content);
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
logger_1.logger.debug(`LFI test failed for ${param}: ${error.message}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
async testPathTraversal(page, url, param, timeout) {
|
|
316
|
-
const traversalPayloads = [
|
|
317
|
-
"../../../../etc/passwd",
|
|
318
|
-
"..%2f..%2f..%2f..%2fetc%2fpasswd",
|
|
319
|
-
"....//....//....//etc/passwd",
|
|
320
|
-
"..\\..\\..\\..\\windows\\system32\\config\\sam",
|
|
321
|
-
];
|
|
322
|
-
for (const payload of traversalPayloads) {
|
|
323
|
-
try {
|
|
324
|
-
const sanitizedPayload = this.sanitizePayload(payload);
|
|
325
|
-
const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
|
|
326
|
-
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `Path traversal test for ${param}`);
|
|
327
|
-
const content = await page.content();
|
|
328
|
-
this.detector.detectPathTraversal(url, param, sanitizedPayload, content);
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
logger_1.logger.debug(`Path traversal test failed for ${param}: ${error.message}`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
async testCMDI(page, url, param, timeout) {
|
|
336
|
-
const cmdiPayloads = [
|
|
337
|
-
"; whoami",
|
|
338
|
-
"| whoami",
|
|
339
|
-
"`whoami`",
|
|
340
|
-
"$(whoami)",
|
|
341
|
-
"; id",
|
|
342
|
-
"| id",
|
|
343
|
-
"; cat /etc/passwd",
|
|
344
|
-
"| ls -la",
|
|
345
|
-
"& ping -c 3 127.0.0.1",
|
|
346
|
-
"&& whoami",
|
|
347
|
-
];
|
|
348
|
-
for (const payload of cmdiPayloads) {
|
|
349
|
-
try {
|
|
350
|
-
const sanitizedPayload = this.sanitizePayload(payload);
|
|
351
|
-
const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
|
|
352
|
-
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `CMDI test for ${param}`);
|
|
353
|
-
const content = await page.content();
|
|
354
|
-
this.detector.detectCMDI(url, param, sanitizedPayload, content);
|
|
355
|
-
}
|
|
356
|
-
catch (error) {
|
|
357
|
-
logger_1.logger.debug(`CMDI test failed for ${param}: ${error.message}`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
async testSSRF(page, url, param, _timeout) {
|
|
362
|
-
const ssrfPayloads = [
|
|
363
|
-
"http://localhost",
|
|
364
|
-
"http://127.0.0.1",
|
|
365
|
-
"http://169.254.169.254/latest/meta-data/",
|
|
366
|
-
"https://metadata.google.internal/",
|
|
367
|
-
"http://0.0.0.0",
|
|
368
|
-
"http://[::1]",
|
|
369
|
-
];
|
|
370
|
-
for (const payload of ssrfPayloads) {
|
|
371
|
-
try {
|
|
372
|
-
const sanitizedPayload = this.sanitizePayload(payload);
|
|
373
|
-
const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
|
|
374
|
-
try {
|
|
375
|
-
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout: 5000 }), `SSRF test for ${param}`);
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// Expected to fail for internal URLs
|
|
379
|
-
}
|
|
380
|
-
const content = await page.content();
|
|
381
|
-
this.detector.detectSSRF(url, param, sanitizedPayload, content);
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
logger_1.logger.debug(`SSRF test failed for ${param}: ${error.message}`);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
async testOpenRedirect(page, url, param, _timeout) {
|
|
389
|
-
const redirectPayloads = [
|
|
390
|
-
"https://evil.com",
|
|
391
|
-
"https://google.com",
|
|
392
|
-
"//evil.com",
|
|
393
|
-
"///evil.com",
|
|
394
|
-
"https://google.com%23",
|
|
395
|
-
"javascript:alert(1)",
|
|
396
|
-
];
|
|
397
|
-
for (const payload of redirectPayloads) {
|
|
398
|
-
try {
|
|
399
|
-
const sanitizedPayload = this.sanitizePayload(payload);
|
|
400
|
-
const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
|
|
401
|
-
const response = await this.withRetry(() => page.goto(testUrl, { waitUntil: "domcontentloaded", timeout: 5000 }), `Open redirect test for ${param}`);
|
|
402
|
-
if (response) {
|
|
403
|
-
const finalUrl = response.url();
|
|
404
|
-
this.detector.detectOpenRedirect(url, param, sanitizedPayload, finalUrl);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
catch (error) {
|
|
408
|
-
logger_1.logger.debug(`Open redirect test failed for ${param}: ${error.message}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
async testIDOR(baseUrl, param, timeout) {
|
|
413
|
-
const page = await this.browser.newPage();
|
|
414
|
-
try {
|
|
415
|
-
const url = new URL(baseUrl);
|
|
416
|
-
const originalValue = url.searchParams.get(param);
|
|
417
|
-
if (!originalValue || isNaN(Number(originalValue))) {
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
const originalResponse = await this.withRetry(() => page.goto(baseUrl, { waitUntil: "networkidle2", timeout }), `IDOR original request for ${param}`);
|
|
421
|
-
const originalContent = originalResponse ? await originalResponse.text() : "";
|
|
422
|
-
const modifiedValue = String(Number(originalValue) + 1);
|
|
423
|
-
url.searchParams.set(param, modifiedValue);
|
|
424
|
-
const testUrl = url.toString();
|
|
425
|
-
const testResponse = await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `IDOR test request for ${param}`);
|
|
426
|
-
const testContent = testResponse ? await testResponse.text() : "";
|
|
427
|
-
this.detector.detectIDOR(baseUrl, param, originalValue, testContent, originalContent);
|
|
428
|
-
}
|
|
429
|
-
catch (error) {
|
|
430
|
-
logger_1.logger.debug(`IDOR test failed for ${param}: ${error.message}`);
|
|
431
|
-
}
|
|
432
|
-
finally {
|
|
433
|
-
await page.close().catch(err => logger_1.logger.debug(`Error closing page: ${err.message}`));
|
|
434
|
-
}
|
|
435
507
|
}
|
|
436
508
|
buildTestUrl(baseUrl, param, value) {
|
|
437
509
|
try {
|
|
@@ -446,20 +518,42 @@ class Scanner {
|
|
|
446
518
|
async extractLinks(page, baseUrl) {
|
|
447
519
|
const links = await page.$$eval("a[href]", (anchors) => anchors.map((a) => a.getAttribute("href")).filter(Boolean));
|
|
448
520
|
const absoluteLinks = [];
|
|
521
|
+
const baseOrigin = this.baseOrigin || new URL(baseUrl).origin;
|
|
522
|
+
const isAllowedByPatterns = (candidate) => {
|
|
523
|
+
const asString = candidate.toString();
|
|
524
|
+
for (const pattern of this.excludePatterns) {
|
|
525
|
+
if (pattern.test(asString)) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (this.includePatterns.length > 0) {
|
|
530
|
+
return this.includePatterns.some((pattern) => pattern.test(asString));
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
};
|
|
449
534
|
for (const link of links) {
|
|
450
535
|
if (!link)
|
|
451
536
|
continue;
|
|
452
537
|
try {
|
|
453
538
|
const absolute = new URL(link, baseUrl);
|
|
454
|
-
if (
|
|
455
|
-
|
|
539
|
+
if (!["http:", "https:"].includes(absolute.protocol)) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (this.strictScope && absolute.origin !== baseOrigin) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (!isAllowedByPatterns(absolute)) {
|
|
546
|
+
continue;
|
|
456
547
|
}
|
|
548
|
+
absolute.hash = "";
|
|
549
|
+
absoluteLinks.push(absolute.toString());
|
|
457
550
|
}
|
|
458
551
|
catch {
|
|
459
552
|
// Skip invalid URLs
|
|
460
553
|
}
|
|
461
554
|
}
|
|
462
|
-
|
|
555
|
+
const deduped = [...new Set(absoluteLinks)];
|
|
556
|
+
return deduped.slice(0, this.maxLinksPerPage);
|
|
463
557
|
}
|
|
464
558
|
async close() {
|
|
465
559
|
if (this.browser) {
|