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.
Files changed (105) hide show
  1. package/README.md +392 -87
  2. package/dist/agent/confirmation.d.ts +38 -0
  3. package/dist/agent/confirmation.js +210 -0
  4. package/dist/agent/context.d.ts +81 -0
  5. package/dist/agent/context.js +227 -0
  6. package/dist/agent/index.d.ts +10 -0
  7. package/dist/agent/index.js +32 -0
  8. package/dist/agent/orchestrator.d.ts +63 -0
  9. package/dist/agent/orchestrator.js +370 -0
  10. package/dist/agent/prompts/system.d.ts +6 -0
  11. package/dist/agent/prompts/system.js +116 -0
  12. package/dist/agent/skill-registry.d.ts +78 -0
  13. package/dist/agent/skill-registry.js +202 -0
  14. package/dist/agent/skills/analyze-findings.d.ts +22 -0
  15. package/dist/agent/skills/analyze-findings.js +191 -0
  16. package/dist/agent/skills/generate-report.d.ts +26 -0
  17. package/dist/agent/skills/generate-report.js +436 -0
  18. package/dist/agent/skills/health-check.d.ts +28 -0
  19. package/dist/agent/skills/health-check.js +344 -0
  20. package/dist/agent/skills/index.d.ts +9 -0
  21. package/dist/agent/skills/index.js +17 -0
  22. package/dist/agent/skills/verify-finding.d.ts +17 -0
  23. package/dist/agent/skills/verify-finding.js +91 -0
  24. package/dist/agent/skills/web-scan.d.ts +22 -0
  25. package/dist/agent/skills/web-scan.js +203 -0
  26. package/dist/agent/types.d.ts +141 -0
  27. package/dist/agent/types.js +16 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.js +176 -139
  30. package/dist/commands/agent.d.ts +6 -0
  31. package/dist/commands/agent.js +250 -0
  32. package/dist/commands/ai.d.ts +2 -0
  33. package/dist/commands/ai.js +112 -0
  34. package/dist/commands/analyze.js +104 -55
  35. package/dist/commands/config.js +63 -37
  36. package/dist/commands/doctor.js +22 -17
  37. package/dist/commands/onboard.js +190 -125
  38. package/dist/commands/report.js +69 -77
  39. package/dist/commands/scan.js +261 -81
  40. package/dist/commands/scans.d.ts +2 -0
  41. package/dist/commands/scans.js +51 -0
  42. package/dist/core/ai-client.d.ts +7 -2
  43. package/dist/core/ai-client.js +231 -20
  44. package/dist/core/ai-payloads.d.ts +17 -0
  45. package/dist/core/ai-payloads.js +54 -0
  46. package/dist/core/config-schema.d.ts +197 -0
  47. package/dist/core/config-schema.js +68 -0
  48. package/dist/core/config-schema.test.d.ts +1 -0
  49. package/dist/core/config-schema.test.js +151 -0
  50. package/dist/core/config.d.ts +17 -36
  51. package/dist/core/config.js +261 -20
  52. package/dist/core/errors.d.ts +71 -0
  53. package/dist/core/errors.js +162 -0
  54. package/dist/core/scan-index.d.ts +19 -0
  55. package/dist/core/scan-index.js +52 -0
  56. package/dist/core/scan-storage.d.ts +11 -0
  57. package/dist/core/scan-storage.js +69 -0
  58. package/dist/core/scanner.d.ts +101 -4
  59. package/dist/core/scanner.js +432 -63
  60. package/dist/core/vulnerability-detector.d.ts +18 -2
  61. package/dist/core/vulnerability-detector.js +349 -38
  62. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  63. package/dist/core/vulnerability-detector.test.js +210 -0
  64. package/dist/index.js +3 -0
  65. package/dist/plugins/PluginManager.d.ts +27 -0
  66. package/dist/plugins/PluginManager.js +166 -0
  67. package/dist/plugins/index.d.ts +7 -0
  68. package/dist/plugins/index.js +19 -0
  69. package/dist/plugins/types.d.ts +55 -0
  70. package/dist/plugins/types.js +25 -0
  71. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  72. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  73. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  74. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  75. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  76. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  77. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  78. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  79. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  80. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  81. package/dist/reports/PdfGenerator.d.ts +36 -0
  82. package/dist/reports/PdfGenerator.js +379 -0
  83. package/dist/utils/logger.d.ts +33 -1
  84. package/dist/utils/logger.js +127 -8
  85. package/dist/utils/theme.d.ts +55 -0
  86. package/dist/utils/theme.js +195 -0
  87. package/package.json +27 -6
  88. package/dist/core/executor.d.ts +0 -2
  89. package/dist/core/executor.js +0 -74
  90. package/dist/core/logger.d.ts +0 -12
  91. package/dist/core/logger.js +0 -51
  92. package/dist/core/registry.d.ts +0 -3
  93. package/dist/core/registry.js +0 -35
  94. package/dist/core/storage.d.ts +0 -4
  95. package/dist/core/storage.js +0 -39
  96. package/dist/core/types.d.ts +0 -24
  97. package/dist/core/types.js +0 -2
  98. package/dist/skills/base.d.ts +0 -8
  99. package/dist/skills/base.js +0 -6
  100. package/dist/skills/builtin.d.ts +0 -4
  101. package/dist/skills/builtin.js +0 -71
  102. package/dist/skills/loader.d.ts +0 -2
  103. package/dist/skills/loader.js +0 -27
  104. package/dist/skills/types.d.ts +0 -46
  105. package/dist/skills/types.js +0 -2
@@ -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
- 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();
15
20
  crawledUrls = 0;
16
21
  testedForms = 0;
17
22
  requestsMade = 0;
18
- constructor() {
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
- // Start crawling
44
- await this.crawl(targetUrl, depth, timeout);
45
- // Close browser
46
- await this.close();
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
- 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();
69
265
  try {
70
- // Set up request interception
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
- // Check for sensitive data
91
- this.detector.detectSensitiveData(url, content);
92
- // Find and test forms
93
- await this.testForms(page, url, timeout);
94
- // Find links and crawl deeper
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.slice(0, 10)) {
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
- 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}`);
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
- async testForms(page, url, timeout) {
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
- // Get form HTML for CSRF detection
115
- const formHtml = await form.evaluate((el) => el.outerHTML);
116
- this.detector.detectCSRF(url, formHtml);
117
- // Find input fields
118
- const inputs = await form.$$("input, textarea");
119
- for (const input of inputs) {
120
- const name = await input.evaluate((el) => el.getAttribute("name"));
121
- const type = await input.evaluate((el) => el.getAttribute("type"));
122
- if (!name || type === "hidden" || type === "submit") {
123
- continue;
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
- // Test for XSS
126
- await this.testXSS(page, url, name, timeout);
127
- // Test for SQLi
128
- await this.testSQLi(page, url, name, timeout);
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 payloads = ["'", "1' OR '1'='1", "1; DROP TABLE users--", "' OR 1=1--"];
153
- for (const payload of payloads) {
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
- const url = new URL(baseUrl);
167
- url.searchParams.set(param, value);
168
- return url.toString();
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
- // Only crawl same-origin links
180
- if (absolute.origin === base.origin) {
181
- absoluteLinks.push(absolute.toString());
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
- // Invalid URL, skip
546
+ // Skip invalid URLs
186
547
  }
187
548
  }
188
- return [...new Set(absoluteLinks)]; // Remove duplicates
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
- await this.browser.close();
193
- this.browser = null;
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: "xss" | "sqli" | "csrf" | "header" | "sensitive_data" | "other";
3
- severity: "critical" | "high" | "medium" | "low" | "info";
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;