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.
- package/README.md +392 -236
- package/dist/agent/confirmation.d.ts +5 -1
- package/dist/agent/confirmation.js +29 -9
- package/dist/agent/context.js +2 -3
- package/dist/agent/orchestrator.d.ts +2 -0
- package/dist/agent/orchestrator.js +50 -8
- package/dist/agent/prompts/system.d.ts +1 -1
- package/dist/agent/prompts/system.js +5 -7
- package/dist/agent/skills/health-check.js +22 -2
- package/dist/agent/skills/index.d.ts +1 -0
- package/dist/agent/skills/index.js +3 -1
- package/dist/agent/skills/verify-finding.d.ts +17 -0
- package/dist/agent/skills/verify-finding.js +91 -0
- package/dist/agent/skills/web-scan.js +46 -0
- package/dist/cli.js +150 -149
- package/dist/commands/agent.js +38 -38
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +112 -0
- package/dist/commands/analyze.js +103 -54
- package/dist/commands/config.js +55 -29
- package/dist/commands/doctor.js +20 -15
- package/dist/commands/onboard.js +188 -141
- package/dist/commands/report.js +68 -76
- package/dist/commands/scan.js +261 -81
- package/dist/commands/scans.d.ts +2 -0
- package/dist/commands/scans.js +51 -0
- package/dist/core/ai-client.d.ts +6 -1
- package/dist/core/ai-client.js +80 -12
- package/dist/core/ai-payloads.d.ts +17 -0
- package/dist/core/ai-payloads.js +54 -0
- package/dist/core/config-schema.d.ts +197 -0
- package/dist/core/config-schema.js +68 -0
- package/dist/core/config-schema.test.d.ts +1 -0
- package/dist/core/config-schema.test.js +151 -0
- package/dist/core/config.d.ts +8 -31
- package/dist/core/config.js +68 -11
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +162 -0
- package/dist/core/scan-index.d.ts +19 -0
- package/dist/core/scan-index.js +52 -0
- package/dist/core/scan-storage.d.ts +11 -0
- package/dist/core/scan-storage.js +69 -0
- package/dist/core/scanner.d.ts +95 -13
- package/dist/core/scanner.js +336 -248
- package/dist/core/vulnerability-detector.d.ts +3 -0
- package/dist/core/vulnerability-detector.js +25 -15
- package/dist/core/vulnerability-detector.test.d.ts +1 -0
- package/dist/core/vulnerability-detector.test.js +210 -0
- package/dist/index.js +3 -0
- package/dist/plugins/PluginManager.d.ts +27 -0
- package/dist/plugins/PluginManager.js +166 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.js +19 -0
- package/dist/plugins/types.d.ts +55 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
- package/dist/reports/PdfGenerator.d.ts +36 -0
- package/dist/reports/PdfGenerator.js +379 -0
- package/dist/utils/logger.d.ts +33 -1
- package/dist/utils/logger.js +127 -8
- package/dist/utils/theme.d.ts +55 -0
- package/dist/utils/theme.js +195 -0
- package/package.json +1 -1
|
@@ -33,6 +33,9 @@ export declare class VulnerabilityDetector {
|
|
|
33
33
|
private vulnerabilities;
|
|
34
34
|
private reportedHeaders;
|
|
35
35
|
private reportedPaths;
|
|
36
|
+
private onVulnerabilityFound?;
|
|
37
|
+
setOnVulnerabilityFound(callback: (vuln: Vulnerability) => void): void;
|
|
38
|
+
addVulnerability(vuln: Vulnerability): void;
|
|
36
39
|
detectXSS(url: string, param: string, payload: string, response: string): void;
|
|
37
40
|
detectStoredXSS(url: string, payload: string, response: string): void;
|
|
38
41
|
detectSQLi(url: string, param: string, errorResponse: string): void;
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
425
|
+
this.addVulnerability({
|
|
416
426
|
type: "info",
|
|
417
427
|
severity: pattern.severity,
|
|
418
428
|
title: pattern.name,
|
|
@@ -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;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pluginManager = exports.PluginManager = void 0;
|
|
4
|
+
class PluginManager {
|
|
5
|
+
plugins = new Map();
|
|
6
|
+
enabledPlugins = new Set();
|
|
7
|
+
register(plugin) {
|
|
8
|
+
this.plugins.set(plugin.name, plugin);
|
|
9
|
+
if (plugin.enabled) {
|
|
10
|
+
this.enabledPlugins.add(plugin.name);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
unregister(pluginName) {
|
|
14
|
+
this.enabledPlugins.delete(pluginName);
|
|
15
|
+
return this.plugins.delete(pluginName);
|
|
16
|
+
}
|
|
17
|
+
enable(pluginName) {
|
|
18
|
+
if (this.plugins.has(pluginName)) {
|
|
19
|
+
this.enabledPlugins.add(pluginName);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
disable(pluginName) {
|
|
25
|
+
return this.enabledPlugins.delete(pluginName);
|
|
26
|
+
}
|
|
27
|
+
getPlugin(name) {
|
|
28
|
+
return this.plugins.get(name);
|
|
29
|
+
}
|
|
30
|
+
getAllPlugins() {
|
|
31
|
+
return Array.from(this.plugins.values());
|
|
32
|
+
}
|
|
33
|
+
getEnabledPlugins() {
|
|
34
|
+
return Array.from(this.enabledPlugins)
|
|
35
|
+
.map(name => this.plugins.get(name))
|
|
36
|
+
.filter((p) => p !== undefined);
|
|
37
|
+
}
|
|
38
|
+
async testParameter(context, param, value) {
|
|
39
|
+
const results = [];
|
|
40
|
+
for (const plugin of this.getEnabledPlugins()) {
|
|
41
|
+
if (!plugin.testParameter)
|
|
42
|
+
continue;
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
const vulnerabilities = [];
|
|
45
|
+
const errors = [];
|
|
46
|
+
try {
|
|
47
|
+
const result = await plugin.testParameter(context, param, value);
|
|
48
|
+
if (result.found && result.vulnerability) {
|
|
49
|
+
vulnerabilities.push(result.vulnerability);
|
|
50
|
+
}
|
|
51
|
+
if (result.error) {
|
|
52
|
+
errors.push({ url: context.url, error: result.error });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
errors.push({
|
|
57
|
+
url: context.url,
|
|
58
|
+
error: error.message
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
results.push({
|
|
62
|
+
plugin: plugin.name,
|
|
63
|
+
vulnerabilities,
|
|
64
|
+
errors,
|
|
65
|
+
duration: Date.now() - startTime,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
async testFormInput(context, formData) {
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const plugin of this.getEnabledPlugins()) {
|
|
73
|
+
if (!plugin.testFormInput)
|
|
74
|
+
continue;
|
|
75
|
+
const startTime = Date.now();
|
|
76
|
+
const vulnerabilities = [];
|
|
77
|
+
const errors = [];
|
|
78
|
+
try {
|
|
79
|
+
const result = await plugin.testFormInput(context, formData);
|
|
80
|
+
if (result.found && result.vulnerability) {
|
|
81
|
+
vulnerabilities.push(result.vulnerability);
|
|
82
|
+
}
|
|
83
|
+
if (result.error) {
|
|
84
|
+
errors.push({ url: context.url, error: result.error });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
errors.push({
|
|
89
|
+
url: context.url,
|
|
90
|
+
error: error.message
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
results.push({
|
|
94
|
+
plugin: plugin.name,
|
|
95
|
+
vulnerabilities,
|
|
96
|
+
errors,
|
|
97
|
+
duration: Date.now() - startTime,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
async analyzeContent(context, content) {
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const plugin of this.getEnabledPlugins()) {
|
|
105
|
+
if (!plugin.analyzeContent)
|
|
106
|
+
continue;
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
const errors = [];
|
|
109
|
+
try {
|
|
110
|
+
const vulnerabilities = await plugin.analyzeContent(context, content);
|
|
111
|
+
results.push({
|
|
112
|
+
plugin: plugin.name,
|
|
113
|
+
vulnerabilities,
|
|
114
|
+
errors,
|
|
115
|
+
duration: Date.now() - startTime,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
errors.push({
|
|
120
|
+
url: context.url,
|
|
121
|
+
error: error.message
|
|
122
|
+
});
|
|
123
|
+
results.push({
|
|
124
|
+
plugin: plugin.name,
|
|
125
|
+
vulnerabilities: [],
|
|
126
|
+
errors,
|
|
127
|
+
duration: Date.now() - startTime,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
async analyzeHeaders(context, headers) {
|
|
134
|
+
const results = [];
|
|
135
|
+
for (const plugin of this.getEnabledPlugins()) {
|
|
136
|
+
if (!plugin.analyzeHeaders)
|
|
137
|
+
continue;
|
|
138
|
+
const startTime = Date.now();
|
|
139
|
+
const errors = [];
|
|
140
|
+
try {
|
|
141
|
+
const vulnerabilities = await plugin.analyzeHeaders(context, headers);
|
|
142
|
+
results.push({
|
|
143
|
+
plugin: plugin.name,
|
|
144
|
+
vulnerabilities,
|
|
145
|
+
errors,
|
|
146
|
+
duration: Date.now() - startTime,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
errors.push({
|
|
151
|
+
url: context.url,
|
|
152
|
+
error: error.message
|
|
153
|
+
});
|
|
154
|
+
results.push({
|
|
155
|
+
plugin: plugin.name,
|
|
156
|
+
vulnerabilities: [],
|
|
157
|
+
errors,
|
|
158
|
+
duration: Date.now() - startTime,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
exports.PluginManager = PluginManager;
|
|
166
|
+
exports.pluginManager = new PluginManager();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { VulnerabilityPlugin, PluginContext, BaseVulnerabilityPlugin, VulnerabilityTestResult, FormData } from "./types";
|
|
2
|
+
export { PluginManager, PluginExecutionResult, pluginManager } from "./PluginManager";
|
|
3
|
+
export { XSSPlugin } from "./vulnerabilities/XSSPlugin";
|
|
4
|
+
export { SQLInjectionPlugin } from "./vulnerabilities/SQLInjectionPlugin";
|
|
5
|
+
export { SecurityHeadersPlugin } from "./vulnerabilities/SecurityHeadersPlugin";
|
|
6
|
+
export { SensitiveDataPlugin } from "./vulnerabilities/SensitiveDataPlugin";
|
|
7
|
+
export { CSRFPlugin } from "./vulnerabilities/CSRFPlugin";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CSRFPlugin = exports.SensitiveDataPlugin = exports.SecurityHeadersPlugin = exports.SQLInjectionPlugin = exports.XSSPlugin = exports.pluginManager = exports.PluginManager = exports.BaseVulnerabilityPlugin = void 0;
|
|
4
|
+
var types_1 = require("./types");
|
|
5
|
+
Object.defineProperty(exports, "BaseVulnerabilityPlugin", { enumerable: true, get: function () { return types_1.BaseVulnerabilityPlugin; } });
|
|
6
|
+
var PluginManager_1 = require("./PluginManager");
|
|
7
|
+
Object.defineProperty(exports, "PluginManager", { enumerable: true, get: function () { return PluginManager_1.PluginManager; } });
|
|
8
|
+
Object.defineProperty(exports, "pluginManager", { enumerable: true, get: function () { return PluginManager_1.pluginManager; } });
|
|
9
|
+
// Vulnerability plugins
|
|
10
|
+
var XSSPlugin_1 = require("./vulnerabilities/XSSPlugin");
|
|
11
|
+
Object.defineProperty(exports, "XSSPlugin", { enumerable: true, get: function () { return XSSPlugin_1.XSSPlugin; } });
|
|
12
|
+
var SQLInjectionPlugin_1 = require("./vulnerabilities/SQLInjectionPlugin");
|
|
13
|
+
Object.defineProperty(exports, "SQLInjectionPlugin", { enumerable: true, get: function () { return SQLInjectionPlugin_1.SQLInjectionPlugin; } });
|
|
14
|
+
var SecurityHeadersPlugin_1 = require("./vulnerabilities/SecurityHeadersPlugin");
|
|
15
|
+
Object.defineProperty(exports, "SecurityHeadersPlugin", { enumerable: true, get: function () { return SecurityHeadersPlugin_1.SecurityHeadersPlugin; } });
|
|
16
|
+
var SensitiveDataPlugin_1 = require("./vulnerabilities/SensitiveDataPlugin");
|
|
17
|
+
Object.defineProperty(exports, "SensitiveDataPlugin", { enumerable: true, get: function () { return SensitiveDataPlugin_1.SensitiveDataPlugin; } });
|
|
18
|
+
var CSRFPlugin_1 = require("./vulnerabilities/CSRFPlugin");
|
|
19
|
+
Object.defineProperty(exports, "CSRFPlugin", { enumerable: true, get: function () { return CSRFPlugin_1.CSRFPlugin; } });
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Page } from "puppeteer";
|
|
2
|
+
import { Vulnerability, VulnerabilityType, Severity } from "../core/vulnerability-detector";
|
|
3
|
+
export interface PluginContext {
|
|
4
|
+
page: Page;
|
|
5
|
+
url: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
timeout: number;
|
|
8
|
+
userAgent: string;
|
|
9
|
+
payloadGenerator?: any;
|
|
10
|
+
}
|
|
11
|
+
export interface VulnerabilityTestResult {
|
|
12
|
+
found: boolean;
|
|
13
|
+
vulnerability?: Vulnerability;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface VulnerabilityPlugin {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly type: VulnerabilityType;
|
|
19
|
+
readonly description: string;
|
|
20
|
+
readonly enabled: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Test a URL parameter for this vulnerability
|
|
23
|
+
*/
|
|
24
|
+
testParameter?(context: PluginContext, param: string, value: string): Promise<VulnerabilityTestResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Test a form input for this vulnerability
|
|
27
|
+
*/
|
|
28
|
+
testFormInput?(context: PluginContext, formData: FormData): Promise<VulnerabilityTestResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Analyze page content for this vulnerability
|
|
31
|
+
*/
|
|
32
|
+
analyzeContent?(context: PluginContext, content: string): Promise<Vulnerability[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Analyze HTTP headers for this vulnerability
|
|
35
|
+
*/
|
|
36
|
+
analyzeHeaders?(context: PluginContext, headers: Record<string, string>): Promise<Vulnerability[]>;
|
|
37
|
+
}
|
|
38
|
+
export interface FormData {
|
|
39
|
+
action: string;
|
|
40
|
+
method: string;
|
|
41
|
+
inputs: Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
type: string;
|
|
44
|
+
value?: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export declare abstract class BaseVulnerabilityPlugin implements VulnerabilityPlugin {
|
|
48
|
+
abstract readonly name: string;
|
|
49
|
+
abstract readonly type: VulnerabilityType;
|
|
50
|
+
abstract readonly description: string;
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
protected createVulnerability(title: string, description: string, url: string, severity: Severity, evidence?: string, remediation?: string, cwe?: string): Vulnerability;
|
|
53
|
+
protected success(vulnerability: Vulnerability): VulnerabilityTestResult;
|
|
54
|
+
protected failure(error?: string): VulnerabilityTestResult;
|
|
55
|
+
}
|