kramscan 0.1.1 → 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.
Files changed (72) hide show
  1. package/README.md +392 -236
  2. package/dist/agent/confirmation.d.ts +5 -1
  3. package/dist/agent/confirmation.js +29 -9
  4. package/dist/agent/context.js +2 -3
  5. package/dist/agent/orchestrator.d.ts +2 -0
  6. package/dist/agent/orchestrator.js +50 -8
  7. package/dist/agent/prompts/system.d.ts +1 -1
  8. package/dist/agent/prompts/system.js +5 -7
  9. package/dist/agent/skills/health-check.js +22 -2
  10. package/dist/agent/skills/index.d.ts +1 -0
  11. package/dist/agent/skills/index.js +3 -1
  12. package/dist/agent/skills/verify-finding.d.ts +17 -0
  13. package/dist/agent/skills/verify-finding.js +91 -0
  14. package/dist/agent/skills/web-scan.js +46 -0
  15. package/dist/cli.js +150 -149
  16. package/dist/commands/agent.js +38 -38
  17. package/dist/commands/ai.d.ts +2 -0
  18. package/dist/commands/ai.js +112 -0
  19. package/dist/commands/analyze.js +103 -54
  20. package/dist/commands/config.js +55 -29
  21. package/dist/commands/doctor.js +20 -15
  22. package/dist/commands/onboard.js +188 -141
  23. package/dist/commands/report.js +68 -76
  24. package/dist/commands/scan.js +261 -81
  25. package/dist/commands/scans.d.ts +2 -0
  26. package/dist/commands/scans.js +51 -0
  27. package/dist/core/ai-client.d.ts +6 -1
  28. package/dist/core/ai-client.js +80 -12
  29. package/dist/core/ai-payloads.d.ts +17 -0
  30. package/dist/core/ai-payloads.js +54 -0
  31. package/dist/core/config-schema.d.ts +197 -0
  32. package/dist/core/config-schema.js +68 -0
  33. package/dist/core/config-schema.test.d.ts +1 -0
  34. package/dist/core/config-schema.test.js +151 -0
  35. package/dist/core/config.d.ts +8 -31
  36. package/dist/core/config.js +68 -11
  37. package/dist/core/errors.d.ts +71 -0
  38. package/dist/core/errors.js +162 -0
  39. package/dist/core/scan-index.d.ts +19 -0
  40. package/dist/core/scan-index.js +52 -0
  41. package/dist/core/scan-storage.d.ts +11 -0
  42. package/dist/core/scan-storage.js +69 -0
  43. package/dist/core/scanner.d.ts +95 -13
  44. package/dist/core/scanner.js +336 -248
  45. package/dist/core/vulnerability-detector.d.ts +3 -0
  46. package/dist/core/vulnerability-detector.js +25 -15
  47. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  48. package/dist/core/vulnerability-detector.test.js +210 -0
  49. package/dist/index.js +3 -0
  50. package/dist/plugins/PluginManager.d.ts +27 -0
  51. package/dist/plugins/PluginManager.js +166 -0
  52. package/dist/plugins/index.d.ts +7 -0
  53. package/dist/plugins/index.js +19 -0
  54. package/dist/plugins/types.d.ts +55 -0
  55. package/dist/plugins/types.js +25 -0
  56. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  57. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  58. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  59. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  60. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  61. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  62. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  63. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  64. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  65. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  66. package/dist/reports/PdfGenerator.d.ts +36 -0
  67. package/dist/reports/PdfGenerator.js +379 -0
  68. package/dist/utils/logger.d.ts +33 -1
  69. package/dist/utils/logger.js +127 -8
  70. package/dist/utils/theme.d.ts +55 -0
  71. package/dist/utils/theme.js +195 -0
  72. package/package.json +1 -1
@@ -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,123 @@ 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.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;
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
+ }
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);
32
73
  }
33
- async initializeRateLimiter() {
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) {
34
84
  const config = await (0, config_1.getConfig)();
35
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
+ }
36
143
  }
37
144
  async initialize(options = {}) {
38
145
  const headless = options.headless ?? true;
@@ -51,16 +158,20 @@ class Scanner {
51
158
  const startTime = Date.now();
52
159
  const depth = options.depth ?? 2;
53
160
  const timeout = options.timeout ?? 30000;
54
- await this.initializeRateLimiter();
161
+ this.resetScanState();
162
+ await this.initializeScanSettings(targetUrl, options);
55
163
  if (!this.browser) {
56
164
  await this.initialize(options);
57
165
  }
166
+ this.emit("scan:start", { target: targetUrl, options });
58
167
  logger_1.logger.info(`Starting scan of ${targetUrl} (depth: ${depth}, timeout: ${timeout}ms)`);
59
168
  try {
60
169
  await this.crawl(targetUrl, depth, timeout);
61
170
  }
62
171
  catch (error) {
63
- logger_1.logger.error(`Scan failed: ${error.message}`);
172
+ const err = error;
173
+ this.emit("scan:error", { error: err });
174
+ logger_1.logger.error(`Scan failed: ${err.message}`);
64
175
  throw error;
65
176
  }
66
177
  finally {
@@ -79,6 +190,7 @@ class Scanner {
79
190
  requestsMade: this.requestsMade,
80
191
  },
81
192
  };
193
+ this.emit("scan:complete", { result });
82
194
  return result;
83
195
  }
84
196
  async applyRateLimit() {
@@ -108,62 +220,198 @@ class Scanner {
108
220
  }
109
221
  throw new Error(`Failed after ${this.retryConfig.maxRetries + 1} attempts: ${lastError?.message}`);
110
222
  }
111
- sanitizePayload(payload) {
112
- // Prevent null bytes and extreme length payloads
113
- if (payload.includes('\0')) {
114
- throw new Error("Payload contains null bytes");
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);
115
242
  }
116
- if (payload.length > 10000) {
117
- throw new Error("Payload exceeds maximum length of 10000 characters");
243
+ finally {
244
+ await page.close().catch((err) => logger_1.logger.debug(`Error closing page: ${err.message}`));
118
245
  }
119
- return payload;
120
246
  }
121
247
  async crawl(url, depth, timeout) {
248
+ if (this.crawledUrls >= this.maxPages) {
249
+ return;
250
+ }
122
251
  if (depth === 0 || this.visitedUrls.has(url)) {
123
252
  return;
124
253
  }
125
254
  this.visitedUrls.add(url);
126
255
  this.crawledUrls++;
127
- const page = await this.browser.newPage();
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();
128
265
  try {
129
- await page.setRequestInterception(true);
130
- page.on("request", (request) => {
131
- this.requestsMade++;
132
- request.continue();
133
- });
134
266
  const response = await this.withRetry(() => page.goto(url, { waitUntil: "networkidle2", timeout }), `crawl ${url}`);
135
267
  if (!response) {
136
268
  logger_1.logger.warn(`No response from ${url}`);
269
+ this.scanErrors.push({ url, error: "No response" });
137
270
  return;
138
271
  }
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
272
  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);
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
+ }
149
282
  if (depth > 1) {
150
283
  const links = await this.extractLinks(page, url);
151
- for (const link of links.slice(0, 10)) {
284
+ for (const link of links) {
152
285
  await this.crawl(link, depth - 1, timeout);
153
286
  }
154
287
  }
288
+ this.emit("crawl:complete", { url });
155
289
  }
156
290
  catch (error) {
157
- logger_1.logger.error(`Error crawling ${url}: ${error.message}`);
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}`);
158
295
  }
159
296
  finally {
160
297
  await page.close().catch(err => logger_1.logger.debug(`Error closing page: ${err.message}`));
161
298
  }
162
299
  }
163
- async testForms(page, url, timeout) {
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);
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);
323
+ }
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) {
164
366
  const forms = await page.$$("form");
165
- const config = await (0, config_1.getConfig)();
166
- const maxConcurrency = config.scan.maxThreads || 5;
367
+ if (forms.length > 0) {
368
+ this.emit("form:test", { url, formCount: forms.length });
369
+ }
370
+ for (const form of forms) {
371
+ this.testedForms++;
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
+ }
167
415
  for (const form of forms) {
168
416
  this.testedForms++;
169
417
  try {
@@ -177,66 +425,60 @@ class Scanner {
177
425
  if (!name || type === "hidden" || type === "submit") {
178
426
  continue;
179
427
  }
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));
428
+ inputTests.push(() => this.runInIsolatedPage((testPage) => this.testXSS(testPage, url, name, timeout)));
429
+ inputTests.push(() => this.runInIsolatedPage((testPage) => this.testSQLi(testPage, url, name, timeout)));
184
430
  }
185
- // Execute tests with concurrency control
186
- await this.runWithConcurrency(inputTests, maxConcurrency);
431
+ await this.runWithConcurrency(inputTests, this.maxConcurrency);
187
432
  }
188
433
  catch (error) {
189
434
  logger_1.logger.debug(`Error testing form: ${error.message}`);
190
435
  }
191
436
  }
192
437
  }
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) {
438
+ async testUrlParametersLegacy(page, baseUrl, timeout) {
209
439
  try {
210
440
  const url = new URL(baseUrl);
211
441
  const params = Array.from(url.searchParams.keys());
212
442
  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);
443
+ await this.testXSS(page, baseUrl, param, timeout);
444
+ await this.testSQLi(page, baseUrl, param, timeout);
219
445
  }
220
446
  }
221
447
  catch (error) {
222
448
  logger_1.logger.debug(`Error testing URL parameters: ${error.message}`);
223
449
  }
224
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
+ }
225
470
  async testXSS(page, url, param, timeout) {
226
471
  const payloads = [
227
472
  "<script>alert('XSS')</script>",
228
473
  '"><script>alert(1)</script>',
229
474
  "<img src=x onerror=alert(1)>",
230
- "'-alert(1)-'",
231
- "<svg/onload=alert(1)>",
232
475
  ];
233
476
  for (const payload of payloads) {
234
477
  try {
235
- const sanitizedPayload = this.sanitizePayload(payload);
236
- const testUrl = this.buildTestUrl(url, param, sanitizedPayload);
478
+ const testUrl = this.buildTestUrl(url, param, payload);
237
479
  await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `XSS test for ${param}`);
238
480
  const content = await page.content();
239
- this.detector.detectXSS(url, param, sanitizedPayload, content);
481
+ this.detector.detectXSS(url, param, payload, content);
240
482
  }
241
483
  catch (error) {
242
484
  logger_1.logger.debug(`XSS test failed for ${param}: ${error.message}`);
@@ -244,31 +486,11 @@ class Scanner {
244
486
  }
245
487
  }
246
488
  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
- }
489
+ const errorBasedPayloads = ["'", "1' OR '1'='1", "' OR 1=1--"];
267
490
  for (const payload of errorBasedPayloads) {
268
491
  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}`);
492
+ const testUrl = this.buildTestUrl(url, param, payload);
493
+ await this.withRetry(() => page.goto(testUrl, { waitUntil: "networkidle2", timeout }), `SQLi test for ${param}`);
272
494
  const content = await page.content();
273
495
  this.detector.detectSQLi(url, param, content);
274
496
  }
@@ -276,162 +498,6 @@ class Scanner {
276
498
  logger_1.logger.debug(`SQLi test failed for ${param}: ${error.message}`);
277
499
  }
278
500
  }
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
501
  }
436
502
  buildTestUrl(baseUrl, param, value) {
437
503
  try {
@@ -446,20 +512,42 @@ class Scanner {
446
512
  async extractLinks(page, baseUrl) {
447
513
  const links = await page.$$eval("a[href]", (anchors) => anchors.map((a) => a.getAttribute("href")).filter(Boolean));
448
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
+ };
449
528
  for (const link of links) {
450
529
  if (!link)
451
530
  continue;
452
531
  try {
453
532
  const absolute = new URL(link, baseUrl);
454
- if (absolute.origin === new URL(baseUrl).origin) {
455
- absoluteLinks.push(absolute.toString());
533
+ if (!["http:", "https:"].includes(absolute.protocol)) {
534
+ continue;
535
+ }
536
+ if (this.strictScope && absolute.origin !== baseOrigin) {
537
+ continue;
538
+ }
539
+ if (!isAllowedByPatterns(absolute)) {
540
+ continue;
456
541
  }
542
+ absolute.hash = "";
543
+ absoluteLinks.push(absolute.toString());
457
544
  }
458
545
  catch {
459
546
  // Skip invalid URLs
460
547
  }
461
548
  }
462
- return [...new Set(absoluteLinks)];
549
+ const deduped = [...new Set(absoluteLinks)];
550
+ return deduped.slice(0, this.maxLinksPerPage);
463
551
  }
464
552
  async close() {
465
553
  if (this.browser) {