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.
Files changed (91) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +419 -236
  3. package/dist/agent/confirmation.d.ts +5 -1
  4. package/dist/agent/confirmation.js +29 -9
  5. package/dist/agent/context.js +2 -3
  6. package/dist/agent/orchestrator.d.ts +2 -0
  7. package/dist/agent/orchestrator.js +50 -8
  8. package/dist/agent/prompts/system.d.ts +1 -1
  9. package/dist/agent/prompts/system.js +5 -7
  10. package/dist/agent/skills/health-check.js +22 -2
  11. package/dist/agent/skills/index.d.ts +1 -0
  12. package/dist/agent/skills/index.js +3 -1
  13. package/dist/agent/skills/verify-finding.d.ts +17 -0
  14. package/dist/agent/skills/verify-finding.js +91 -0
  15. package/dist/agent/skills/web-scan.js +46 -0
  16. package/dist/cli.js +156 -149
  17. package/dist/commands/agent.js +38 -38
  18. package/dist/commands/ai.d.ts +2 -0
  19. package/dist/commands/ai.js +112 -0
  20. package/dist/commands/analyze.js +103 -54
  21. package/dist/commands/config.js +55 -29
  22. package/dist/commands/dev.d.ts +2 -0
  23. package/dist/commands/dev.js +236 -0
  24. package/dist/commands/doctor.js +20 -15
  25. package/dist/commands/gate.d.ts +2 -0
  26. package/dist/commands/gate.js +109 -0
  27. package/dist/commands/onboard.js +188 -141
  28. package/dist/commands/report.js +68 -76
  29. package/dist/commands/scan.js +262 -81
  30. package/dist/commands/scans.d.ts +2 -0
  31. package/dist/commands/scans.js +55 -0
  32. package/dist/core/ai-client.d.ts +6 -1
  33. package/dist/core/ai-client.js +80 -12
  34. package/dist/core/ai-payloads.d.ts +17 -0
  35. package/dist/core/ai-payloads.js +54 -0
  36. package/dist/core/config-schema.d.ts +197 -0
  37. package/dist/core/config-schema.js +68 -0
  38. package/dist/core/config-schema.test.d.ts +1 -0
  39. package/dist/core/config-schema.test.js +151 -0
  40. package/dist/core/config.d.ts +8 -31
  41. package/dist/core/config.js +71 -14
  42. package/dist/core/diff-engine.d.ts +12 -0
  43. package/dist/core/diff-engine.js +47 -0
  44. package/dist/core/errors.d.ts +71 -0
  45. package/dist/core/errors.js +162 -0
  46. package/dist/core/scan-index.d.ts +20 -0
  47. package/dist/core/scan-index.js +52 -0
  48. package/dist/core/scan-storage.d.ts +11 -0
  49. package/dist/core/scan-storage.js +69 -0
  50. package/dist/core/scanner.d.ts +95 -13
  51. package/dist/core/scanner.js +342 -248
  52. package/dist/core/server-probe.d.ts +20 -0
  53. package/dist/core/server-probe.js +109 -0
  54. package/dist/core/vulnerability-detector.d.ts +9 -0
  55. package/dist/core/vulnerability-detector.js +46 -15
  56. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  57. package/dist/core/vulnerability-detector.test.js +210 -0
  58. package/dist/index.js +3 -0
  59. package/dist/plugins/PluginManager.d.ts +27 -0
  60. package/dist/plugins/PluginManager.js +166 -0
  61. package/dist/plugins/index.d.ts +12 -0
  62. package/dist/plugins/index.js +29 -0
  63. package/dist/plugins/types.d.ts +55 -0
  64. package/dist/plugins/types.js +25 -0
  65. package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
  66. package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
  67. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  68. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  69. package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
  70. package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
  71. package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
  72. package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
  73. package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
  74. package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
  75. package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
  76. package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -0
  77. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  78. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  79. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  80. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  81. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  82. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  83. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  84. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  85. package/dist/reports/PdfGenerator.d.ts +36 -0
  86. package/dist/reports/PdfGenerator.js +404 -0
  87. package/dist/utils/logger.d.ts +33 -1
  88. package/dist/utils/logger.js +127 -8
  89. package/dist/utils/theme.d.ts +56 -0
  90. package/dist/utils/theme.js +201 -0
  91. package/package.json +6 -3
@@ -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
- class Scanner {
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
- constructor() {
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, // Default: 5 requests per second
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
- async initializeRateLimiter() {
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
- await this.initializeRateLimiter();
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
- logger_1.logger.error(`Scan failed: ${error.message}`);
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
- sanitizePayload(payload) {
112
- // Prevent null bytes and extreme length payloads
113
- if (payload.includes('\0')) {
114
- throw new Error("Payload contains null bytes");
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
- if (payload.length > 10000) {
117
- throw new Error("Payload exceeds maximum length of 10000 characters");
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
- const page = await this.browser.newPage();
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
- this.detector.detectSensitiveData(url, content);
146
- this.detector.detectInfoDisclosure(url, content);
147
- await this.testForms(page, url, timeout);
148
- await this.testUrlParameters(page, url, timeout);
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.slice(0, 10)) {
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
- logger_1.logger.error(`Error crawling ${url}: ${error.message}`);
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 testForms(page, url, timeout) {
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
- const config = await (0, config_1.getConfig)();
166
- const maxConcurrency = config.scan.maxThreads || 5;
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(page, url, name, timeout));
181
- inputTests.push(() => this.testSQLi(page, url, name, timeout));
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
- // Execute tests with concurrency control
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 runWithConcurrency(tasks, maxConcurrency) {
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.testIDOR(baseUrl, param, timeout);
214
- await this.testLFI(page, baseUrl, param, timeout);
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 sanitizedPayload = this.sanitizePayload(payload);
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, sanitizedPayload, content);
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 sanitizedPayload = this.sanitizePayload(payload);
270
- const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
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 (absolute.origin === new URL(baseUrl).origin) {
455
- absoluteLinks.push(absolute.toString());
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
- return [...new Set(absoluteLinks)];
555
+ const deduped = [...new Set(absoluteLinks)];
556
+ return deduped.slice(0, this.maxLinksPerPage);
463
557
  }
464
558
  async close() {
465
559
  if (this.browser) {