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
@@ -0,0 +1,20 @@
1
+ export interface ProbeResult {
2
+ reachable: boolean;
3
+ statusCode?: number;
4
+ server?: string;
5
+ framework?: string;
6
+ responseTime: number;
7
+ }
8
+ /**
9
+ * Probes a localhost URL for server readiness.
10
+ * Polls with exponential backoff until the server responds or timeout is reached.
11
+ */
12
+ export declare function probeServer(url: string, options?: {
13
+ timeout?: number;
14
+ interval?: number;
15
+ maxAttempts?: number;
16
+ }): Promise<ProbeResult>;
17
+ /**
18
+ * Checks if a URL points to localhost.
19
+ */
20
+ export declare function isLocalhost(url: string): boolean;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.probeServer = probeServer;
7
+ exports.isLocalhost = isLocalhost;
8
+ const http_1 = __importDefault(require("http"));
9
+ const https_1 = __importDefault(require("https"));
10
+ /**
11
+ * Probes a localhost URL for server readiness.
12
+ * Polls with exponential backoff until the server responds or timeout is reached.
13
+ */
14
+ async function probeServer(url, options = {}) {
15
+ const { timeout = 30000, interval = 1000, maxAttempts = 20 } = options;
16
+ const startTime = Date.now();
17
+ let attempts = 0;
18
+ let lastError = null;
19
+ while (attempts < maxAttempts && Date.now() - startTime < timeout) {
20
+ attempts++;
21
+ try {
22
+ const result = await pingUrl(url);
23
+ if (result.reachable) {
24
+ return result;
25
+ }
26
+ lastError = `HTTP ${result.statusCode}`;
27
+ }
28
+ catch (err) {
29
+ lastError = err.message;
30
+ }
31
+ // Exponential backoff with cap
32
+ const delay = Math.min(interval * Math.pow(1.5, attempts - 1), 5000);
33
+ await sleep(delay);
34
+ }
35
+ return {
36
+ reachable: false,
37
+ responseTime: Date.now() - startTime,
38
+ };
39
+ }
40
+ /**
41
+ * Single ping to a URL — returns immediately.
42
+ */
43
+ function pingUrl(url) {
44
+ return new Promise((resolve, reject) => {
45
+ const startTime = Date.now();
46
+ const parsedUrl = new URL(url);
47
+ const client = parsedUrl.protocol === "https:" ? https_1.default : http_1.default;
48
+ const req = client.get(url, {
49
+ timeout: 5000,
50
+ rejectUnauthorized: false, // Allow self-signed certs in dev
51
+ }, (res) => {
52
+ const responseTime = Date.now() - startTime;
53
+ const server = res.headers["server"] || undefined;
54
+ const poweredBy = res.headers["x-powered-by"] || "";
55
+ // Detect framework from headers
56
+ let framework;
57
+ if (poweredBy.includes("Express"))
58
+ framework = "Express.js";
59
+ else if (poweredBy.includes("Next.js"))
60
+ framework = "Next.js";
61
+ else if (poweredBy.includes("Nuxt"))
62
+ framework = "Nuxt.js";
63
+ else if (poweredBy.includes("PHP"))
64
+ framework = "PHP";
65
+ else if (poweredBy.includes("ASP.NET"))
66
+ framework = "ASP.NET";
67
+ else if (res.headers["x-django-request-id"])
68
+ framework = "Django";
69
+ else if (res.headers["x-request-id"] && server?.includes("nginx"))
70
+ framework = "Rails/nginx";
71
+ // Consume body to free socket
72
+ res.resume();
73
+ resolve({
74
+ reachable: (res.statusCode || 0) >= 100 && (res.statusCode || 0) < 600,
75
+ statusCode: res.statusCode,
76
+ server: server,
77
+ framework,
78
+ responseTime,
79
+ });
80
+ });
81
+ req.on("error", (err) => {
82
+ reject(err);
83
+ });
84
+ req.on("timeout", () => {
85
+ req.destroy();
86
+ reject(new Error("Connection timed out"));
87
+ });
88
+ });
89
+ }
90
+ function sleep(ms) {
91
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
+ }
93
+ /**
94
+ * Checks if a URL points to localhost.
95
+ */
96
+ function isLocalhost(url) {
97
+ try {
98
+ const parsed = new URL(url);
99
+ const host = parsed.hostname.toLowerCase();
100
+ return (host === "localhost" ||
101
+ host === "127.0.0.1" ||
102
+ host === "0.0.0.0" ||
103
+ host === "::1" ||
104
+ host.endsWith(".localhost"));
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
@@ -28,11 +28,15 @@ export interface ScanResult {
28
28
  testedForms: number;
29
29
  requestsMade: number;
30
30
  };
31
+ score: number;
31
32
  }
32
33
  export declare class VulnerabilityDetector {
33
34
  private vulnerabilities;
34
35
  private reportedHeaders;
35
36
  private reportedPaths;
37
+ private onVulnerabilityFound?;
38
+ setOnVulnerabilityFound(callback: (vuln: Vulnerability) => void): void;
39
+ addVulnerability(vuln: Vulnerability): void;
36
40
  detectXSS(url: string, param: string, payload: string, response: string): void;
37
41
  detectStoredXSS(url: string, payload: string, response: string): void;
38
42
  detectSQLi(url: string, param: string, errorResponse: string): void;
@@ -56,5 +60,10 @@ export declare class VulnerabilityDetector {
56
60
  low: number;
57
61
  info: number;
58
62
  };
63
+ /**
64
+ * Calculates a security score from 0-100
65
+ * 100 is perfect, 0 is very poor
66
+ */
67
+ calculateScore(): number;
59
68
  clear(): void;
60
69
  }
@@ -5,12 +5,22 @@ class VulnerabilityDetector {
5
5
  vulnerabilities = [];
6
6
  reportedHeaders = new Set();
7
7
  reportedPaths = new Set();
8
+ onVulnerabilityFound;
9
+ setOnVulnerabilityFound(callback) {
10
+ this.onVulnerabilityFound = callback;
11
+ }
12
+ addVulnerability(vuln) {
13
+ this.vulnerabilities.push(vuln);
14
+ if (this.onVulnerabilityFound) {
15
+ this.onVulnerabilityFound(vuln);
16
+ }
17
+ }
8
18
  detectXSS(url, param, payload, response) {
9
19
  if (response.includes(payload)) {
10
20
  const existing = this.vulnerabilities.find(v => v.type === "xss" && v.url === url && v.evidence?.includes(param));
11
21
  if (existing)
12
22
  return;
13
- this.vulnerabilities.push({
23
+ this.addVulnerability({
14
24
  type: "xss",
15
25
  severity: "high",
16
26
  title: "Reflected Cross-Site Scripting (XSS)",
@@ -27,7 +37,7 @@ class VulnerabilityDetector {
27
37
  const existing = this.vulnerabilities.find(v => v.type === "xss" && v.url === url && v.title.includes("Stored"));
28
38
  if (existing)
29
39
  return;
30
- this.vulnerabilities.push({
40
+ this.addVulnerability({
31
41
  type: "xss",
32
42
  severity: "critical",
33
43
  title: "Stored Cross-Site Scripting (XSS)",
@@ -65,7 +75,7 @@ class VulnerabilityDetector {
65
75
  return;
66
76
  for (const error of sqlErrors) {
67
77
  if (errorResponse.includes(error)) {
68
- this.vulnerabilities.push({
78
+ this.addVulnerability({
69
79
  type: "sqli",
70
80
  severity: "critical",
71
81
  title: "SQL Injection",
@@ -84,7 +94,7 @@ class VulnerabilityDetector {
84
94
  const existing = this.vulnerabilities.find(v => v.type === "sqli" && v.url === url && v.title.includes("Blind"));
85
95
  if (existing)
86
96
  return;
87
- this.vulnerabilities.push({
97
+ this.addVulnerability({
88
98
  type: "sqli",
89
99
  severity: "high",
90
100
  title: "Blind SQL Injection",
@@ -105,7 +115,7 @@ class VulnerabilityDetector {
105
115
  const existing = this.vulnerabilities.find(v => v.type === "csrf" && v.url === url);
106
116
  if (existing)
107
117
  return;
108
- this.vulnerabilities.push({
118
+ this.addVulnerability({
109
119
  type: "csrf",
110
120
  severity: "medium",
111
121
  title: "Missing CSRF Protection",
@@ -157,7 +167,7 @@ class VulnerabilityDetector {
157
167
  for (const [header, config] of Object.entries(requiredHeaders)) {
158
168
  if (!headers[header.toLowerCase()]) {
159
169
  missingCount++;
160
- this.vulnerabilities.push({
170
+ this.addVulnerability({
161
171
  type: "header",
162
172
  severity: config.severity,
163
173
  title: config.title,
@@ -190,7 +200,7 @@ class VulnerabilityDetector {
190
200
  const existing = this.vulnerabilities.find(v => v.type === "sensitive_data" && v.url === url && v.title.includes(pattern.name));
191
201
  if (existing)
192
202
  continue;
193
- this.vulnerabilities.push({
203
+ this.addVulnerability({
194
204
  type: "sensitive_data",
195
205
  severity: pattern.severity,
196
206
  title: `Exposed ${pattern.name}`,
@@ -210,7 +220,7 @@ class VulnerabilityDetector {
210
220
  const existing = this.vulnerabilities.find(v => v.type === "idor" && v.url === url && v.evidence?.includes(paramName));
211
221
  if (existing)
212
222
  return;
213
- this.vulnerabilities.push({
223
+ this.addVulnerability({
214
224
  type: "idor",
215
225
  severity: "high",
216
226
  title: "Insecure Direct Object Reference (IDOR)",
@@ -239,7 +249,7 @@ class VulnerabilityDetector {
239
249
  const existing = this.vulnerabilities.find(v => v.type === "lfi" && v.url === url);
240
250
  if (existing)
241
251
  return;
242
- this.vulnerabilities.push({
252
+ this.addVulnerability({
243
253
  type: "lfi",
244
254
  severity: "critical",
245
255
  title: "Local File Inclusion (LFI)",
@@ -270,7 +280,7 @@ class VulnerabilityDetector {
270
280
  if (this.reportedPaths.has(pathKey))
271
281
  return;
272
282
  this.reportedPaths.add(pathKey);
273
- this.vulnerabilities.push({
283
+ this.addVulnerability({
274
284
  type: "lfi",
275
285
  severity: "high",
276
286
  title: "Path Traversal",
@@ -305,7 +315,7 @@ class VulnerabilityDetector {
305
315
  const existing = this.vulnerabilities.find(v => v.type === "cmdi" && v.url === url);
306
316
  if (existing)
307
317
  return;
308
- this.vulnerabilities.push({
318
+ this.addVulnerability({
309
319
  type: "cmdi",
310
320
  severity: "critical",
311
321
  title: "OS Command Injection",
@@ -336,7 +346,7 @@ class VulnerabilityDetector {
336
346
  const existing = this.vulnerabilities.find(v => v.type === "ssrf" && v.url === url);
337
347
  if (existing)
338
348
  return;
339
- this.vulnerabilities.push({
349
+ this.addVulnerability({
340
350
  type: "ssrf",
341
351
  severity: "high",
342
352
  title: "Server-Side Request Forgery (SSRF)",
@@ -361,7 +371,7 @@ class VulnerabilityDetector {
361
371
  const existing = this.vulnerabilities.find(v => v.type === "redirect" && v.url === url);
362
372
  if (existing)
363
373
  return;
364
- this.vulnerabilities.push({
374
+ this.addVulnerability({
365
375
  type: "redirect",
366
376
  severity: "medium",
367
377
  title: "Open Redirect",
@@ -378,7 +388,7 @@ class VulnerabilityDetector {
378
388
  const existing = this.vulnerabilities.find(v => v.type === "redirect" && v.url === url);
379
389
  if (existing)
380
390
  return;
381
- this.vulnerabilities.push({
391
+ this.addVulnerability({
382
392
  type: "redirect",
383
393
  severity: "medium",
384
394
  title: "Open Redirect",
@@ -412,7 +422,7 @@ class VulnerabilityDetector {
412
422
  const existing = this.vulnerabilities.find(v => v.type === "info" && v.url === url && v.title === pattern.name);
413
423
  if (existing)
414
424
  continue;
415
- this.vulnerabilities.push({
425
+ this.addVulnerability({
416
426
  type: "info",
417
427
  severity: pattern.severity,
418
428
  title: pattern.name,
@@ -442,6 +452,27 @@ class VulnerabilityDetector {
442
452
  }
443
453
  return summary;
444
454
  }
455
+ /**
456
+ * Calculates a security score from 0-100
457
+ * 100 is perfect, 0 is very poor
458
+ */
459
+ calculateScore() {
460
+ if (this.vulnerabilities.length === 0)
461
+ return 100;
462
+ const weights = {
463
+ critical: 25,
464
+ high: 10,
465
+ medium: 5,
466
+ low: 2,
467
+ info: 0,
468
+ };
469
+ let totalDeduction = 0;
470
+ for (const vuln of this.vulnerabilities) {
471
+ totalDeduction += weights[vuln.severity] || 0;
472
+ }
473
+ const score = Math.max(0, 100 - totalDeduction);
474
+ return Math.round(score);
475
+ }
445
476
  clear() {
446
477
  this.vulnerabilities = [];
447
478
  this.reportedHeaders.clear();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vulnerability_detector_1 = require("./vulnerability-detector");
4
+ describe("VulnerabilityDetector", () => {
5
+ let detector;
6
+ beforeEach(() => {
7
+ detector = new vulnerability_detector_1.VulnerabilityDetector();
8
+ });
9
+ // ─── detectXSS ─────────────────────────────────────────────────
10
+ describe("detectXSS", () => {
11
+ it("should detect reflected XSS when payload is in response", () => {
12
+ const payload = "<script>alert('XSS')</script>";
13
+ detector.detectXSS("https://example.com/search?q=test", "q", payload, `<html><body>${payload}</body></html>`);
14
+ const vulns = detector.getVulnerabilities();
15
+ expect(vulns).toHaveLength(1);
16
+ expect(vulns[0].type).toBe("xss");
17
+ expect(vulns[0].severity).toBe("high");
18
+ expect(vulns[0].cwe).toBe("CWE-79");
19
+ });
20
+ it("should not report XSS when payload is not reflected", () => {
21
+ detector.detectXSS("https://example.com/search", "q", "<script>alert(1)</script>", "<html><body>Safe content</body></html>");
22
+ expect(detector.getVulnerabilities()).toHaveLength(0);
23
+ });
24
+ it("should report XSS for each detection call (no deduplication)", () => {
25
+ const payload = "<script>alert('XSS')</script>";
26
+ const response = `<html>${payload}</html>`;
27
+ detector.detectXSS("https://example.com", "q", payload, response);
28
+ detector.detectXSS("https://example.com", "q", payload, response);
29
+ // XSS detection does not deduplicate — each call adds a separate finding
30
+ expect(detector.getVulnerabilities()).toHaveLength(2);
31
+ });
32
+ });
33
+ // ─── detectSQLi ────────────────────────────────────────────────
34
+ describe("detectSQLi", () => {
35
+ it("should detect SQL injection from error messages", () => {
36
+ detector.detectSQLi("https://example.com/users?id=1", "id", "You have an error in your SQL syntax near '1'");
37
+ const vulns = detector.getVulnerabilities();
38
+ expect(vulns).toHaveLength(1);
39
+ expect(vulns[0].type).toBe("sqli");
40
+ expect(vulns[0].severity).toBe("critical");
41
+ expect(vulns[0].cwe).toBe("CWE-89");
42
+ });
43
+ it("should detect PostgreSQL errors", () => {
44
+ detector.detectSQLi("https://example.com/api", "q", "ERROR: PostgreSQL syntax error at position 42");
45
+ expect(detector.getVulnerabilities()).toHaveLength(1);
46
+ });
47
+ it("should not report when no SQL errors found", () => {
48
+ detector.detectSQLi("https://example.com", "q", "<html><body>Normal content</body></html>");
49
+ expect(detector.getVulnerabilities()).toHaveLength(0);
50
+ });
51
+ it("should deduplicate SQLi findings for the same URL", () => {
52
+ detector.detectSQLi("https://example.com", "id", "SQL syntax error");
53
+ detector.detectSQLi("https://example.com", "name", "ORA-00933 error");
54
+ expect(detector.getVulnerabilities()).toHaveLength(1);
55
+ });
56
+ });
57
+ // ─── detectBlindSQLi ───────────────────────────────────────────
58
+ describe("detectBlindSQLi", () => {
59
+ it("should detect time-based blind SQLi when delay exceeds threshold", () => {
60
+ detector.detectBlindSQLi("https://example.com", "id", 500, 4000);
61
+ const vulns = detector.getVulnerabilities();
62
+ expect(vulns).toHaveLength(1);
63
+ expect(vulns[0].type).toBe("sqli");
64
+ expect(vulns[0].title).toContain("Blind");
65
+ });
66
+ it("should not report when delay is within threshold", () => {
67
+ detector.detectBlindSQLi("https://example.com", "id", 500, 2000);
68
+ expect(detector.getVulnerabilities()).toHaveLength(0);
69
+ });
70
+ });
71
+ // ─── detectCSRF ────────────────────────────────────────────────
72
+ describe("detectCSRF", () => {
73
+ it("should detect missing CSRF token", () => {
74
+ detector.detectCSRF("https://example.com/form", '<form method="POST"><input name="email" /></form>');
75
+ const vulns = detector.getVulnerabilities();
76
+ expect(vulns).toHaveLength(1);
77
+ expect(vulns[0].type).toBe("csrf");
78
+ expect(vulns[0].severity).toBe("medium");
79
+ });
80
+ it("should not report when CSRF token is present", () => {
81
+ detector.detectCSRF("https://example.com/form", '<form method="POST"><input name="csrf_token" /><input name="email" /></form>');
82
+ expect(detector.getVulnerabilities()).toHaveLength(0);
83
+ });
84
+ it("should recognize _token as a valid CSRF token", () => {
85
+ detector.detectCSRF("https://example.com", '<form><input name="_token" value="abc123" /></form>');
86
+ expect(detector.getVulnerabilities()).toHaveLength(0);
87
+ });
88
+ });
89
+ // ─── analyzeSecurityHeaders ────────────────────────────────────
90
+ describe("analyzeSecurityHeaders", () => {
91
+ it("should detect missing security headers", () => {
92
+ detector.analyzeSecurityHeaders("https://example.com", {});
93
+ const vulns = detector.getVulnerabilities();
94
+ expect(vulns.length).toBeGreaterThanOrEqual(3);
95
+ expect(vulns.every((v) => v.type === "header")).toBe(true);
96
+ });
97
+ it("should not report when all headers are present", () => {
98
+ detector.analyzeSecurityHeaders("https://example.com", {
99
+ "content-security-policy": "default-src 'self'",
100
+ "x-frame-options": "DENY",
101
+ "strict-transport-security": "max-age=31536000",
102
+ "x-content-type-options": "nosniff",
103
+ "referrer-policy": "strict-origin-when-cross-origin",
104
+ "permissions-policy": "camera=()",
105
+ });
106
+ expect(detector.getVulnerabilities()).toHaveLength(0);
107
+ });
108
+ it("should only check headers once per host", () => {
109
+ detector.analyzeSecurityHeaders("https://example.com/page1", {});
110
+ const count1 = detector.getVulnerabilities().length;
111
+ detector.analyzeSecurityHeaders("https://example.com/page2", {});
112
+ const count2 = detector.getVulnerabilities().length;
113
+ expect(count2).toBe(count1);
114
+ });
115
+ });
116
+ // ─── detectSensitiveData ──────────────────────────────────────
117
+ describe("detectSensitiveData", () => {
118
+ it("should detect exposed AWS access keys", () => {
119
+ detector.detectSensitiveData("https://example.com", 'config = { key: "AKIAIOSFODNN7EXAMPLE" }');
120
+ const vulns = detector.getVulnerabilities();
121
+ expect(vulns).toHaveLength(1);
122
+ expect(vulns[0].type).toBe("sensitive_data");
123
+ expect(vulns[0].severity).toBe("critical");
124
+ });
125
+ it("should detect exposed JWT tokens", () => {
126
+ detector.detectSensitiveData("https://example.com", 'let token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0"');
127
+ const vulns = detector.getVulnerabilities();
128
+ expect(vulns).toHaveLength(1);
129
+ expect(vulns[0].title).toContain("JWT");
130
+ });
131
+ it("should detect private keys", () => {
132
+ detector.detectSensitiveData("https://example.com", "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAK...");
133
+ const vulns = detector.getVulnerabilities();
134
+ expect(vulns).toHaveLength(1);
135
+ expect(vulns[0].severity).toBe("critical");
136
+ });
137
+ it("should not report on clean responses", () => {
138
+ detector.detectSensitiveData("https://example.com", "<html><body>Hello World</body></html>");
139
+ expect(detector.getVulnerabilities()).toHaveLength(0);
140
+ });
141
+ });
142
+ // ─── getSummary ────────────────────────────────────────────────
143
+ describe("getSummary", () => {
144
+ it("should return correct severity counts", () => {
145
+ // Add mixed severity vulns
146
+ detector.addVulnerability({
147
+ type: "xss", severity: "high", title: "XSS",
148
+ description: "test", url: "https://example.com",
149
+ });
150
+ detector.addVulnerability({
151
+ type: "sqli", severity: "critical", title: "SQLi",
152
+ description: "test", url: "https://example.com",
153
+ });
154
+ detector.addVulnerability({
155
+ type: "header", severity: "low", title: "Header",
156
+ description: "test", url: "https://example.com",
157
+ });
158
+ detector.addVulnerability({
159
+ type: "info", severity: "info", title: "Info",
160
+ description: "test", url: "https://example.com",
161
+ });
162
+ const summary = detector.getSummary();
163
+ expect(summary.total).toBe(4);
164
+ expect(summary.critical).toBe(1);
165
+ expect(summary.high).toBe(1);
166
+ expect(summary.low).toBe(1);
167
+ expect(summary.info).toBe(1);
168
+ expect(summary.medium).toBe(0);
169
+ });
170
+ it("should return zeros when no vulnerabilities", () => {
171
+ const summary = detector.getSummary();
172
+ expect(summary.total).toBe(0);
173
+ expect(summary.critical).toBe(0);
174
+ });
175
+ });
176
+ // ─── clear ─────────────────────────────────────────────────────
177
+ describe("clear", () => {
178
+ it("should remove all vulnerabilities and reset state", () => {
179
+ detector.addVulnerability({
180
+ type: "xss", severity: "high", title: "XSS",
181
+ description: "test", url: "https://example.com",
182
+ });
183
+ expect(detector.getVulnerabilities()).toHaveLength(1);
184
+ detector.clear();
185
+ expect(detector.getVulnerabilities()).toHaveLength(0);
186
+ expect(detector.getSummary().total).toBe(0);
187
+ });
188
+ it("should allow re-detecting after clear", () => {
189
+ detector.analyzeSecurityHeaders("https://example.com", {});
190
+ const count1 = detector.getVulnerabilities().length;
191
+ detector.clear();
192
+ detector.analyzeSecurityHeaders("https://example.com", {});
193
+ const count2 = detector.getVulnerabilities().length;
194
+ expect(count2).toBe(count1);
195
+ });
196
+ });
197
+ // ─── Callback ──────────────────────────────────────────────────
198
+ describe("onVulnerabilityFound callback", () => {
199
+ it("should call callback when vulnerability is added", () => {
200
+ const callback = jest.fn();
201
+ detector.setOnVulnerabilityFound(callback);
202
+ detector.addVulnerability({
203
+ type: "xss", severity: "high", title: "XSS",
204
+ description: "test", url: "https://example.com",
205
+ });
206
+ expect(callback).toHaveBeenCalledTimes(1);
207
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({ type: "xss", severity: "high" }));
208
+ });
209
+ });
210
+ });
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const cli_1 = require("./cli");
4
+ const errors_1 = require("./core/errors");
5
+ // Ensure uncaught exceptions and unhandled rejections produce useful output
6
+ (0, errors_1.setupGlobalErrorHandlers)();
4
7
  (0, cli_1.run)().catch((err) => {
5
8
  console.error("Fatal error:", err);
6
9
  process.exit(1);
@@ -0,0 +1,27 @@
1
+ import { VulnerabilityPlugin, PluginContext, FormData } from "./types";
2
+ import { Vulnerability } from "../core/vulnerability-detector";
3
+ export interface PluginExecutionResult {
4
+ plugin: string;
5
+ vulnerabilities: Vulnerability[];
6
+ errors: Array<{
7
+ url: string;
8
+ error: string;
9
+ }>;
10
+ duration: number;
11
+ }
12
+ export declare class PluginManager {
13
+ private plugins;
14
+ private enabledPlugins;
15
+ register(plugin: VulnerabilityPlugin): void;
16
+ unregister(pluginName: string): boolean;
17
+ enable(pluginName: string): boolean;
18
+ disable(pluginName: string): boolean;
19
+ getPlugin(name: string): VulnerabilityPlugin | undefined;
20
+ getAllPlugins(): VulnerabilityPlugin[];
21
+ getEnabledPlugins(): VulnerabilityPlugin[];
22
+ testParameter(context: PluginContext, param: string, value: string): Promise<PluginExecutionResult[]>;
23
+ testFormInput(context: PluginContext, formData: FormData): Promise<PluginExecutionResult[]>;
24
+ analyzeContent(context: PluginContext, content: string): Promise<PluginExecutionResult[]>;
25
+ analyzeHeaders(context: PluginContext, headers: Record<string, string>): Promise<PluginExecutionResult[]>;
26
+ }
27
+ export declare const pluginManager: PluginManager;