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.
- package/LICENSE +1 -1
- package/README.md +419 -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 +156 -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/dev.d.ts +2 -0
- package/dist/commands/dev.js +236 -0
- package/dist/commands/doctor.js +20 -15
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +109 -0
- package/dist/commands/onboard.js +188 -141
- package/dist/commands/report.js +68 -76
- package/dist/commands/scan.js +262 -81
- package/dist/commands/scans.d.ts +2 -0
- package/dist/commands/scans.js +55 -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 +71 -14
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +162 -0
- package/dist/core/scan-index.d.ts +20 -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 +342 -248
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +9 -0
- package/dist/core/vulnerability-detector.js +46 -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 +12 -0
- package/dist/plugins/index.js +29 -0
- package/dist/plugins/types.d.ts +55 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -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 +404 -0
- package/dist/utils/logger.d.ts +33 -1
- package/dist/utils/logger.js +127 -8
- package/dist/utils/theme.d.ts +56 -0
- package/dist/utils/theme.js +201 -0
- 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.
|
|
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,
|
|
@@ -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;
|