kramscan 0.1.0 → 0.2.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/README.md +392 -87
- package/dist/agent/confirmation.d.ts +38 -0
- package/dist/agent/confirmation.js +210 -0
- package/dist/agent/context.d.ts +81 -0
- package/dist/agent/context.js +227 -0
- package/dist/agent/index.d.ts +10 -0
- package/dist/agent/index.js +32 -0
- package/dist/agent/orchestrator.d.ts +63 -0
- package/dist/agent/orchestrator.js +370 -0
- package/dist/agent/prompts/system.d.ts +6 -0
- package/dist/agent/prompts/system.js +116 -0
- package/dist/agent/skill-registry.d.ts +78 -0
- package/dist/agent/skill-registry.js +202 -0
- package/dist/agent/skills/analyze-findings.d.ts +22 -0
- package/dist/agent/skills/analyze-findings.js +191 -0
- package/dist/agent/skills/generate-report.d.ts +26 -0
- package/dist/agent/skills/generate-report.js +436 -0
- package/dist/agent/skills/health-check.d.ts +28 -0
- package/dist/agent/skills/health-check.js +344 -0
- package/dist/agent/skills/index.d.ts +9 -0
- package/dist/agent/skills/index.js +17 -0
- 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.d.ts +22 -0
- package/dist/agent/skills/web-scan.js +203 -0
- package/dist/agent/types.d.ts +141 -0
- package/dist/agent/types.js +16 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +176 -139
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +250 -0
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +112 -0
- package/dist/commands/analyze.js +104 -55
- package/dist/commands/config.js +63 -37
- package/dist/commands/doctor.js +22 -17
- package/dist/commands/onboard.js +190 -125
- package/dist/commands/report.js +69 -77
- package/dist/commands/scan.js +261 -81
- package/dist/commands/scans.d.ts +2 -0
- package/dist/commands/scans.js +51 -0
- package/dist/core/ai-client.d.ts +7 -2
- package/dist/core/ai-client.js +231 -20
- 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 +17 -36
- package/dist/core/config.js +261 -20
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +162 -0
- package/dist/core/scan-index.d.ts +19 -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 +101 -4
- package/dist/core/scanner.js +432 -63
- package/dist/core/vulnerability-detector.d.ts +18 -2
- package/dist/core/vulnerability-detector.js +349 -38
- 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 +7 -0
- package/dist/plugins/index.js +19 -0
- package/dist/plugins/types.d.ts +55 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -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 +379 -0
- package/dist/utils/logger.d.ts +33 -1
- package/dist/utils/logger.js +127 -8
- package/dist/utils/theme.d.ts +55 -0
- package/dist/utils/theme.js +195 -0
- package/package.json +27 -6
- package/dist/core/executor.d.ts +0 -2
- package/dist/core/executor.js +0 -74
- package/dist/core/logger.d.ts +0 -12
- package/dist/core/logger.js +0 -51
- package/dist/core/registry.d.ts +0 -3
- package/dist/core/registry.js +0 -35
- package/dist/core/storage.d.ts +0 -4
- package/dist/core/storage.js +0 -39
- package/dist/core/types.d.ts +0 -24
- package/dist/core/types.js +0 -2
- package/dist/skills/base.d.ts +0 -8
- package/dist/skills/base.js +0 -6
- package/dist/skills/builtin.d.ts +0 -4
- package/dist/skills/builtin.js +0 -71
- package/dist/skills/loader.d.ts +0 -2
- package/dist/skills/loader.js +0 -27
- package/dist/skills/types.d.ts +0 -46
- package/dist/skills/types.js +0 -2
package/dist/core/scanner.js
CHANGED
|
@@ -5,21 +5,143 @@ 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();
|
|
15
20
|
crawledUrls = 0;
|
|
16
21
|
testedForms = 0;
|
|
17
22
|
requestsMade = 0;
|
|
18
|
-
|
|
23
|
+
headersChecked = new Set();
|
|
24
|
+
rateLimiter;
|
|
25
|
+
retryConfig;
|
|
26
|
+
maxConcurrency = 5;
|
|
27
|
+
strictScope = true;
|
|
28
|
+
baseOrigin = null;
|
|
29
|
+
maxPages = 30;
|
|
30
|
+
maxLinksPerPage = 50;
|
|
31
|
+
includePatterns = [];
|
|
32
|
+
excludePatterns = [];
|
|
33
|
+
userAgent = "KramScan/0.1.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;
|
|
19
42
|
this.detector = new vulnerability_detector_1.VulnerabilityDetector();
|
|
43
|
+
this.detector.setOnVulnerabilityFound((vuln) => {
|
|
44
|
+
this.emit("vuln:found", { vulnerability: vuln });
|
|
45
|
+
});
|
|
46
|
+
this.rateLimiter = {
|
|
47
|
+
lastRequestTime: 0,
|
|
48
|
+
minInterval: 200,
|
|
49
|
+
};
|
|
50
|
+
this.retryConfig = {
|
|
51
|
+
maxRetries: 3,
|
|
52
|
+
baseDelay: 1000,
|
|
53
|
+
maxDelay: 10000,
|
|
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
|
+
}
|
|
67
|
+
// Type-safe event emitter methods
|
|
68
|
+
emit(event, data) {
|
|
69
|
+
return super.emit(event, data);
|
|
70
|
+
}
|
|
71
|
+
on(event, listener) {
|
|
72
|
+
return super.on(event, listener);
|
|
73
|
+
}
|
|
74
|
+
once(event, listener) {
|
|
75
|
+
return super.once(event, listener);
|
|
76
|
+
}
|
|
77
|
+
getScanErrors() {
|
|
78
|
+
return [...this.scanErrors];
|
|
79
|
+
}
|
|
80
|
+
getPluginErrors() {
|
|
81
|
+
return new Map(this.pluginErrors);
|
|
82
|
+
}
|
|
83
|
+
async initializeScanSettings(targetUrl, options) {
|
|
84
|
+
const config = await (0, config_1.getConfig)();
|
|
85
|
+
this.rateLimiter.minInterval = 1000 / (config.scan.rateLimitPerSecond || 5);
|
|
86
|
+
this.maxConcurrency = Math.max(1, config.scan.maxThreads || 5);
|
|
87
|
+
this.strictScope = options.strictScope ?? (config.scan.strictScope ?? true);
|
|
88
|
+
this.baseOrigin = new URL(targetUrl).origin;
|
|
89
|
+
this.userAgent = config.scan.userAgent || this.userAgent;
|
|
90
|
+
// Load scan profile
|
|
91
|
+
const profileName = options.profile || config.scan.defaultProfile || "balanced";
|
|
92
|
+
const profile = await (0, config_1.getScanProfile)(profileName);
|
|
93
|
+
this.maxPages = Math.max(1, options.maxPages ?? profile?.maxPages ?? 30);
|
|
94
|
+
this.maxLinksPerPage = Math.max(1, options.maxLinksPerPage ?? profile?.maxLinksPerPage ?? 50);
|
|
95
|
+
// Initialize AI payload generator if requested and AI is enabled
|
|
96
|
+
this.useAiPayloads = options.useAiPayloads ?? false;
|
|
97
|
+
if (this.useAiPayloads && config.ai.enabled) {
|
|
98
|
+
try {
|
|
99
|
+
const aiClient = await (0, ai_client_1.createAIClient)();
|
|
100
|
+
this.payloadGenerator = new ai_payloads_1.PayloadGenerator(aiClient);
|
|
101
|
+
logger_1.logger.info("AI contextual payload generation enabled");
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger_1.logger.warn(`Failed to initialize AI payload generator: ${err.message}`);
|
|
105
|
+
this.useAiPayloads = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const compileList = (values) => {
|
|
109
|
+
if (!values || values.length === 0)
|
|
110
|
+
return [];
|
|
111
|
+
const patterns = [];
|
|
112
|
+
for (const raw of values) {
|
|
113
|
+
try {
|
|
114
|
+
patterns.push(new RegExp(raw));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
logger_1.logger.warn(`Invalid regex pattern ignored: ${raw}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return patterns;
|
|
121
|
+
};
|
|
122
|
+
this.includePatterns = compileList(options.include);
|
|
123
|
+
this.excludePatterns = compileList(options.exclude);
|
|
124
|
+
}
|
|
125
|
+
resetScanState() {
|
|
126
|
+
this.detector.clear();
|
|
127
|
+
this.visitedUrls.clear();
|
|
128
|
+
this.crawledUrls = 0;
|
|
129
|
+
this.testedForms = 0;
|
|
130
|
+
this.requestsMade = 0;
|
|
131
|
+
this.headersChecked.clear();
|
|
132
|
+
this.scanErrors = [];
|
|
133
|
+
this.pluginErrors.clear();
|
|
134
|
+
this.rateLimiter.lastRequestTime = 0;
|
|
135
|
+
this.removeAllListeners();
|
|
136
|
+
// Reset plugin manager state
|
|
137
|
+
if (this.usePlugins) {
|
|
138
|
+
const securityHeadersPlugin = plugins_1.pluginManager.getPlugin("Security Headers Analyzer");
|
|
139
|
+
if (securityHeadersPlugin && typeof securityHeadersPlugin.reset === "function") {
|
|
140
|
+
securityHeadersPlugin.reset();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
20
143
|
}
|
|
21
144
|
async initialize(options = {}) {
|
|
22
|
-
const config = (0, config_1.getConfig)();
|
|
23
145
|
const headless = options.headless ?? true;
|
|
24
146
|
this.browser = await puppeteer_1.default.launch({
|
|
25
147
|
headless,
|
|
@@ -36,14 +158,25 @@ class Scanner {
|
|
|
36
158
|
const startTime = Date.now();
|
|
37
159
|
const depth = options.depth ?? 2;
|
|
38
160
|
const timeout = options.timeout ?? 30000;
|
|
161
|
+
this.resetScanState();
|
|
162
|
+
await this.initializeScanSettings(targetUrl, options);
|
|
39
163
|
if (!this.browser) {
|
|
40
164
|
await this.initialize(options);
|
|
41
165
|
}
|
|
166
|
+
this.emit("scan:start", { target: targetUrl, options });
|
|
42
167
|
logger_1.logger.info(`Starting scan of ${targetUrl} (depth: ${depth}, timeout: ${timeout}ms)`);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
168
|
+
try {
|
|
169
|
+
await this.crawl(targetUrl, depth, timeout);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const err = error;
|
|
173
|
+
this.emit("scan:error", { error: err });
|
|
174
|
+
logger_1.logger.error(`Scan failed: ${err.message}`);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await this.close();
|
|
179
|
+
}
|
|
47
180
|
const duration = Date.now() - startTime;
|
|
48
181
|
const result = {
|
|
49
182
|
target: targetUrl,
|
|
@@ -57,78 +190,283 @@ class Scanner {
|
|
|
57
190
|
requestsMade: this.requestsMade,
|
|
58
191
|
},
|
|
59
192
|
};
|
|
193
|
+
this.emit("scan:complete", { result });
|
|
60
194
|
return result;
|
|
61
195
|
}
|
|
196
|
+
async applyRateLimit() {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const timeSinceLastRequest = now - this.rateLimiter.lastRequestTime;
|
|
199
|
+
if (timeSinceLastRequest < this.rateLimiter.minInterval) {
|
|
200
|
+
const delay = this.rateLimiter.minInterval - timeSinceLastRequest;
|
|
201
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
202
|
+
}
|
|
203
|
+
this.rateLimiter.lastRequestTime = Date.now();
|
|
204
|
+
}
|
|
205
|
+
async withRetry(operation, context) {
|
|
206
|
+
let lastError = null;
|
|
207
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
208
|
+
try {
|
|
209
|
+
await this.applyRateLimit();
|
|
210
|
+
return await operation();
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
lastError = error;
|
|
214
|
+
if (attempt < this.retryConfig.maxRetries) {
|
|
215
|
+
const delay = Math.min(this.retryConfig.baseDelay * Math.pow(2, attempt), this.retryConfig.maxDelay);
|
|
216
|
+
logger_1.logger.debug(`Retry ${attempt + 1}/${this.retryConfig.maxRetries} for ${context} after ${delay}ms`);
|
|
217
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`Failed after ${this.retryConfig.maxRetries + 1} attempts: ${lastError?.message}`);
|
|
222
|
+
}
|
|
223
|
+
async createInstrumentedPage() {
|
|
224
|
+
const page = await this.browser.newPage();
|
|
225
|
+
await page.setUserAgent(this.userAgent);
|
|
226
|
+
await page.setRequestInterception(true);
|
|
227
|
+
page.on("request", (request) => {
|
|
228
|
+
this.requestsMade++;
|
|
229
|
+
const resourceType = request.resourceType();
|
|
230
|
+
if (resourceType === "image" || resourceType === "font" || resourceType === "media") {
|
|
231
|
+
request.abort();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
request.continue();
|
|
235
|
+
});
|
|
236
|
+
return page;
|
|
237
|
+
}
|
|
238
|
+
async runInIsolatedPage(operation) {
|
|
239
|
+
const page = await this.createInstrumentedPage();
|
|
240
|
+
try {
|
|
241
|
+
return await operation(page);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await page.close().catch((err) => logger_1.logger.debug(`Error closing page: ${err.message}`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
62
247
|
async crawl(url, depth, timeout) {
|
|
248
|
+
if (this.crawledUrls >= this.maxPages) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
63
251
|
if (depth === 0 || this.visitedUrls.has(url)) {
|
|
64
252
|
return;
|
|
65
253
|
}
|
|
66
254
|
this.visitedUrls.add(url);
|
|
67
255
|
this.crawledUrls++;
|
|
68
|
-
|
|
256
|
+
this.emit("crawl:start", { url, depth });
|
|
257
|
+
this.emit("crawl:page", { url, crawledCount: this.crawledUrls, maxPages: this.maxPages });
|
|
258
|
+
this.emit("progress", {
|
|
259
|
+
stage: "crawling",
|
|
260
|
+
current: this.crawledUrls,
|
|
261
|
+
total: this.maxPages,
|
|
262
|
+
message: `Crawling: ${url}`
|
|
263
|
+
});
|
|
264
|
+
const page = await this.createInstrumentedPage();
|
|
69
265
|
try {
|
|
70
|
-
|
|
71
|
-
await page.setRequestInterception(true);
|
|
72
|
-
page.on("request", (request) => {
|
|
73
|
-
this.requestsMade++;
|
|
74
|
-
request.continue();
|
|
75
|
-
});
|
|
76
|
-
// Navigate to page
|
|
77
|
-
const response = await page.goto(url, {
|
|
78
|
-
waitUntil: "networkidle2",
|
|
79
|
-
timeout,
|
|
80
|
-
});
|
|
266
|
+
const response = await this.withRetry(() => page.goto(url, { waitUntil: "networkidle2", timeout }), `crawl ${url}`);
|
|
81
267
|
if (!response) {
|
|
82
268
|
logger_1.logger.warn(`No response from ${url}`);
|
|
269
|
+
this.scanErrors.push({ url, error: "No response" });
|
|
83
270
|
return;
|
|
84
271
|
}
|
|
85
|
-
// Get response headers
|
|
86
|
-
const headers = response.headers();
|
|
87
|
-
this.detector.analyzeSecurityHeaders(url, headers);
|
|
88
|
-
// Get page content
|
|
89
272
|
const content = await page.content();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
273
|
+
const headers = response.headers();
|
|
274
|
+
// Analyze with plugins
|
|
275
|
+
if (this.usePlugins) {
|
|
276
|
+
await this.runPlugins(page, url, content, headers, timeout);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Fallback to legacy detector
|
|
280
|
+
await this.runLegacyDetection(page, url, content, headers, timeout);
|
|
281
|
+
}
|
|
95
282
|
if (depth > 1) {
|
|
96
283
|
const links = await this.extractLinks(page, url);
|
|
97
|
-
for (const link of links
|
|
98
|
-
// Limit to 10 links per page
|
|
284
|
+
for (const link of links) {
|
|
99
285
|
await this.crawl(link, depth - 1, timeout);
|
|
100
286
|
}
|
|
101
287
|
}
|
|
288
|
+
this.emit("crawl:complete", { url });
|
|
102
289
|
}
|
|
103
290
|
catch (error) {
|
|
104
|
-
|
|
291
|
+
const err = error;
|
|
292
|
+
this.scanErrors.push({ url, error: err.message });
|
|
293
|
+
this.emit("crawl:error", { url, error: err });
|
|
294
|
+
logger_1.logger.error(`Error crawling ${url}: ${err.message}`);
|
|
105
295
|
}
|
|
106
296
|
finally {
|
|
107
|
-
await page.close();
|
|
297
|
+
await page.close().catch(err => logger_1.logger.debug(`Error closing page: ${err.message}`));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async runPlugins(page, url, content, headers, timeout) {
|
|
301
|
+
const context = {
|
|
302
|
+
page,
|
|
303
|
+
url,
|
|
304
|
+
baseUrl: this.baseOrigin || url,
|
|
305
|
+
timeout,
|
|
306
|
+
userAgent: this.userAgent,
|
|
307
|
+
payloadGenerator: this.payloadGenerator,
|
|
308
|
+
};
|
|
309
|
+
// Analyze headers
|
|
310
|
+
const host = new URL(url).host;
|
|
311
|
+
if (!this.headersChecked.has(host)) {
|
|
312
|
+
const headerResults = await plugins_1.pluginManager.analyzeHeaders(context, headers);
|
|
313
|
+
this.processPluginResults(headerResults);
|
|
314
|
+
this.headersChecked.add(host);
|
|
108
315
|
}
|
|
316
|
+
// Analyze content
|
|
317
|
+
const contentResults = await plugins_1.pluginManager.analyzeContent(context, content);
|
|
318
|
+
this.processPluginResults(contentResults);
|
|
319
|
+
// Test URL parameters
|
|
320
|
+
await this.testUrlParametersWithPlugins(page, url, timeout);
|
|
321
|
+
// Test forms
|
|
322
|
+
await this.testFormsWithPlugins(page, url, timeout);
|
|
109
323
|
}
|
|
110
|
-
|
|
324
|
+
processPluginResults(results) {
|
|
325
|
+
for (const result of results) {
|
|
326
|
+
// Track plugin errors
|
|
327
|
+
if (result.errors.length > 0) {
|
|
328
|
+
const existing = this.pluginErrors.get(result.plugin) || [];
|
|
329
|
+
this.pluginErrors.set(result.plugin, [...existing, ...result.errors]);
|
|
330
|
+
}
|
|
331
|
+
// Add vulnerabilities to detector
|
|
332
|
+
for (const vuln of result.vulnerabilities) {
|
|
333
|
+
this.detector.addVulnerability(vuln);
|
|
334
|
+
}
|
|
335
|
+
// Emit event for monitoring
|
|
336
|
+
this.emit("plugin:execute", {
|
|
337
|
+
plugin: result.plugin,
|
|
338
|
+
url: result.errors[0]?.url || "",
|
|
339
|
+
duration: result.duration,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async testUrlParametersWithPlugins(page, url, timeout) {
|
|
344
|
+
try {
|
|
345
|
+
const urlObj = new URL(url);
|
|
346
|
+
const params = Array.from(urlObj.searchParams.keys());
|
|
347
|
+
for (const param of params) {
|
|
348
|
+
const value = urlObj.searchParams.get(param) || "";
|
|
349
|
+
const context = {
|
|
350
|
+
page,
|
|
351
|
+
url,
|
|
352
|
+
baseUrl: this.baseOrigin || url,
|
|
353
|
+
timeout,
|
|
354
|
+
userAgent: this.userAgent,
|
|
355
|
+
payloadGenerator: this.payloadGenerator,
|
|
356
|
+
};
|
|
357
|
+
const results = await plugins_1.pluginManager.testParameter(context, param, value);
|
|
358
|
+
this.processPluginResults(results);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
logger_1.logger.debug(`Error testing URL parameters with plugins: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async testFormsWithPlugins(page, url, timeout) {
|
|
111
366
|
const forms = await page.$$("form");
|
|
367
|
+
if (forms.length > 0) {
|
|
368
|
+
this.emit("form:test", { url, formCount: forms.length });
|
|
369
|
+
}
|
|
112
370
|
for (const form of forms) {
|
|
113
371
|
this.testedForms++;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
372
|
+
try {
|
|
373
|
+
const inputs = await form.$$eval("input, textarea, select", (elements) => elements.map((el) => ({
|
|
374
|
+
name: el.getAttribute("name") || "",
|
|
375
|
+
type: el.getAttribute("type") || "text",
|
|
376
|
+
value: el.value || "",
|
|
377
|
+
})).filter((input) => input.name && input.type !== "hidden" && input.type !== "submit"));
|
|
378
|
+
const formData = {
|
|
379
|
+
action: await form.evaluate((el) => el.action || ""),
|
|
380
|
+
method: await form.evaluate((el) => el.method || "GET"),
|
|
381
|
+
inputs,
|
|
382
|
+
};
|
|
383
|
+
const context = {
|
|
384
|
+
page,
|
|
385
|
+
url,
|
|
386
|
+
baseUrl: this.baseOrigin || url,
|
|
387
|
+
timeout,
|
|
388
|
+
userAgent: this.userAgent,
|
|
389
|
+
payloadGenerator: this.payloadGenerator,
|
|
390
|
+
};
|
|
391
|
+
const results = await plugins_1.pluginManager.testFormInput(context, formData);
|
|
392
|
+
this.processPluginResults(results);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
logger_1.logger.debug(`Error testing form with plugins: ${error.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async runLegacyDetection(page, url, content, headers, timeout) {
|
|
400
|
+
const host = new URL(url).host;
|
|
401
|
+
if (!this.headersChecked.has(host)) {
|
|
402
|
+
this.detector.analyzeSecurityHeaders(url, headers);
|
|
403
|
+
this.headersChecked.add(host);
|
|
404
|
+
}
|
|
405
|
+
this.detector.detectSensitiveData(url, content);
|
|
406
|
+
this.detector.detectInfoDisclosure(url, content);
|
|
407
|
+
await this.testFormsLegacy(page, url, timeout);
|
|
408
|
+
await this.testUrlParametersLegacy(page, url, timeout);
|
|
409
|
+
}
|
|
410
|
+
async testFormsLegacy(page, url, timeout) {
|
|
411
|
+
const forms = await page.$$("form");
|
|
412
|
+
if (forms.length > 0) {
|
|
413
|
+
this.emit("form:test", { url, formCount: forms.length });
|
|
414
|
+
}
|
|
415
|
+
for (const form of forms) {
|
|
416
|
+
this.testedForms++;
|
|
417
|
+
try {
|
|
418
|
+
const formHtml = await form.evaluate((el) => el.outerHTML);
|
|
419
|
+
this.detector.detectCSRF(url, formHtml);
|
|
420
|
+
const inputs = await form.$$("input, textarea, select");
|
|
421
|
+
const inputTests = [];
|
|
422
|
+
for (const input of inputs) {
|
|
423
|
+
const name = await input.evaluate((el) => el.getAttribute("name"));
|
|
424
|
+
const type = await input.evaluate((el) => el.getAttribute("type"));
|
|
425
|
+
if (!name || type === "hidden" || type === "submit") {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
inputTests.push(() => this.runInIsolatedPage((testPage) => this.testXSS(testPage, url, name, timeout)));
|
|
429
|
+
inputTests.push(() => this.runInIsolatedPage((testPage) => this.testSQLi(testPage, url, name, timeout)));
|
|
124
430
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
431
|
+
await this.runWithConcurrency(inputTests, this.maxConcurrency);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
logger_1.logger.debug(`Error testing form: ${error.message}`);
|
|
129
435
|
}
|
|
130
436
|
}
|
|
131
437
|
}
|
|
438
|
+
async testUrlParametersLegacy(page, baseUrl, timeout) {
|
|
439
|
+
try {
|
|
440
|
+
const url = new URL(baseUrl);
|
|
441
|
+
const params = Array.from(url.searchParams.keys());
|
|
442
|
+
for (const param of params) {
|
|
443
|
+
await this.testXSS(page, baseUrl, param, timeout);
|
|
444
|
+
await this.testSQLi(page, baseUrl, param, timeout);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
logger_1.logger.debug(`Error testing URL parameters: ${error.message}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async runWithConcurrency(tasks, maxConcurrency) {
|
|
452
|
+
if (tasks.length === 0) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const workerCount = Math.max(1, Math.min(maxConcurrency, tasks.length));
|
|
456
|
+
let nextIndex = 0;
|
|
457
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
458
|
+
while (nextIndex < tasks.length) {
|
|
459
|
+
const currentTask = tasks[nextIndex++];
|
|
460
|
+
try {
|
|
461
|
+
await currentTask();
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
logger_1.logger.debug(`Task failed: ${error.message}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
await Promise.all(workers);
|
|
469
|
+
}
|
|
132
470
|
async testXSS(page, url, param, timeout) {
|
|
133
471
|
const payloads = [
|
|
134
472
|
"<script>alert('XSS')</script>",
|
|
@@ -137,9 +475,8 @@ class Scanner {
|
|
|
137
475
|
];
|
|
138
476
|
for (const payload of payloads) {
|
|
139
477
|
try {
|
|
140
|
-
// Try to inject payload
|
|
141
478
|
const testUrl = this.buildTestUrl(url, param, payload);
|
|
142
|
-
await page.goto(testUrl, { waitUntil: "networkidle2", timeout });
|
|
479
|
+
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `XSS test for ${param}`);
|
|
143
480
|
const content = await page.content();
|
|
144
481
|
this.detector.detectXSS(url, param, payload, content);
|
|
145
482
|
}
|
|
@@ -149,11 +486,11 @@ class Scanner {
|
|
|
149
486
|
}
|
|
150
487
|
}
|
|
151
488
|
async testSQLi(page, url, param, timeout) {
|
|
152
|
-
const
|
|
153
|
-
for (const payload of
|
|
489
|
+
const errorBasedPayloads = ["'", "1' OR '1'='1", "' OR 1=1--"];
|
|
490
|
+
for (const payload of errorBasedPayloads) {
|
|
154
491
|
try {
|
|
155
492
|
const testUrl = this.buildTestUrl(url, param, payload);
|
|
156
|
-
await page.goto(testUrl, { waitUntil: "networkidle2", timeout });
|
|
493
|
+
await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `SQLi test for ${param}`);
|
|
157
494
|
const content = await page.content();
|
|
158
495
|
this.detector.detectSQLi(url, param, content);
|
|
159
496
|
}
|
|
@@ -163,34 +500,66 @@ class Scanner {
|
|
|
163
500
|
}
|
|
164
501
|
}
|
|
165
502
|
buildTestUrl(baseUrl, param, value) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
503
|
+
try {
|
|
504
|
+
const url = new URL(baseUrl);
|
|
505
|
+
url.searchParams.set(param, value);
|
|
506
|
+
return url.toString();
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
throw new Error(`Invalid URL: ${baseUrl}`);
|
|
510
|
+
}
|
|
169
511
|
}
|
|
170
512
|
async extractLinks(page, baseUrl) {
|
|
171
513
|
const links = await page.$$eval("a[href]", (anchors) => anchors.map((a) => a.getAttribute("href")).filter(Boolean));
|
|
172
|
-
const base = new URL(baseUrl);
|
|
173
514
|
const absoluteLinks = [];
|
|
515
|
+
const baseOrigin = this.baseOrigin || new URL(baseUrl).origin;
|
|
516
|
+
const isAllowedByPatterns = (candidate) => {
|
|
517
|
+
const asString = candidate.toString();
|
|
518
|
+
for (const pattern of this.excludePatterns) {
|
|
519
|
+
if (pattern.test(asString)) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (this.includePatterns.length > 0) {
|
|
524
|
+
return this.includePatterns.some((pattern) => pattern.test(asString));
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
};
|
|
174
528
|
for (const link of links) {
|
|
175
529
|
if (!link)
|
|
176
530
|
continue;
|
|
177
531
|
try {
|
|
178
532
|
const absolute = new URL(link, baseUrl);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
533
|
+
if (!["http:", "https:"].includes(absolute.protocol)) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (this.strictScope && absolute.origin !== baseOrigin) {
|
|
537
|
+
continue;
|
|
182
538
|
}
|
|
539
|
+
if (!isAllowedByPatterns(absolute)) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
absolute.hash = "";
|
|
543
|
+
absoluteLinks.push(absolute.toString());
|
|
183
544
|
}
|
|
184
545
|
catch {
|
|
185
|
-
//
|
|
546
|
+
// Skip invalid URLs
|
|
186
547
|
}
|
|
187
548
|
}
|
|
188
|
-
|
|
549
|
+
const deduped = [...new Set(absoluteLinks)];
|
|
550
|
+
return deduped.slice(0, this.maxLinksPerPage);
|
|
189
551
|
}
|
|
190
552
|
async close() {
|
|
191
553
|
if (this.browser) {
|
|
192
|
-
|
|
193
|
-
|
|
554
|
+
try {
|
|
555
|
+
await this.browser.close();
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
logger_1.logger.debug(`Error closing browser: ${error.message}`);
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
this.browser = null;
|
|
562
|
+
}
|
|
194
563
|
}
|
|
195
564
|
}
|
|
196
565
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export type VulnerabilityType = "xss" | "sqli" | "csrf" | "header" | "sensitive_data" | "idor" | "lfi" | "cmdi" | "ssrf" | "redirect" | "info" | "other";
|
|
2
|
+
export type Severity = "critical" | "high" | "medium" | "low" | "info";
|
|
1
3
|
export interface Vulnerability {
|
|
2
|
-
type:
|
|
3
|
-
severity:
|
|
4
|
+
type: VulnerabilityType;
|
|
5
|
+
severity: Severity;
|
|
4
6
|
title: string;
|
|
5
7
|
description: string;
|
|
6
8
|
url: string;
|
|
@@ -29,11 +31,25 @@ export interface ScanResult {
|
|
|
29
31
|
}
|
|
30
32
|
export declare class VulnerabilityDetector {
|
|
31
33
|
private vulnerabilities;
|
|
34
|
+
private reportedHeaders;
|
|
35
|
+
private reportedPaths;
|
|
36
|
+
private onVulnerabilityFound?;
|
|
37
|
+
setOnVulnerabilityFound(callback: (vuln: Vulnerability) => void): void;
|
|
38
|
+
addVulnerability(vuln: Vulnerability): void;
|
|
32
39
|
detectXSS(url: string, param: string, payload: string, response: string): void;
|
|
40
|
+
detectStoredXSS(url: string, payload: string, response: string): void;
|
|
33
41
|
detectSQLi(url: string, param: string, errorResponse: string): void;
|
|
42
|
+
detectBlindSQLi(url: string, param: string, originalTime: number, testTime: number): void;
|
|
34
43
|
detectCSRF(url: string, formHtml: string): void;
|
|
35
44
|
analyzeSecurityHeaders(url: string, headers: Record<string, string>): void;
|
|
36
45
|
detectSensitiveData(url: string, response: string): void;
|
|
46
|
+
detectIDOR(url: string, paramName: string, paramValue: string, testResponse: string, originalResponse: string): void;
|
|
47
|
+
detectLFI(url: string, param: string, payload: string, response: string): void;
|
|
48
|
+
detectPathTraversal(url: string, param: string, payload: string, response: string): void;
|
|
49
|
+
detectCMDI(url: string, param: string, payload: string, response: string): void;
|
|
50
|
+
detectSSRF(url: string, param: string, payload: string, response: string): void;
|
|
51
|
+
detectOpenRedirect(url: string, param: string, payload: string, finalUrl: string): void;
|
|
52
|
+
detectInfoDisclosure(url: string, response: string): void;
|
|
37
53
|
getVulnerabilities(): Vulnerability[];
|
|
38
54
|
getSummary(): {
|
|
39
55
|
total: number;
|