kramscan 0.2.0 → 0.3.1
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 +81 -54
- package/dist/cli.js +8 -1
- package/dist/commands/config.js +2 -2
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +239 -0
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +112 -0
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/report.js +89 -11
- package/dist/commands/scan.js +11 -0
- package/dist/commands/scans.js +4 -0
- package/dist/core/config-schema.js +1 -1
- package/dist/core/config.js +3 -3
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/scan-index.d.ts +1 -0
- package/dist/core/scanner.js +7 -1
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +6 -0
- package/dist/core/vulnerability-detector.js +21 -0
- package/dist/index.js +14 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.js +11 -1
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -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/reports/PdfGenerator.js +26 -1
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.js +7 -1
- package/package.json +8 -3
package/dist/commands/report.js
CHANGED
|
@@ -8,6 +8,8 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const docx_1 = require("docx");
|
|
9
9
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
13
|
const config_1 = require("../core/config");
|
|
12
14
|
const scan_storage_1 = require("../core/scan-storage");
|
|
13
15
|
const ai_client_1 = require("../core/ai-client");
|
|
@@ -16,7 +18,7 @@ function registerReportCommand(program) {
|
|
|
16
18
|
program
|
|
17
19
|
.command("report [scan-file]")
|
|
18
20
|
.description("Generate a professional security report")
|
|
19
|
-
.option("-f, --format <type>", "Report format: word|json|txt")
|
|
21
|
+
.option("-f, --format <type>", "Report format: word|json|txt|markdown")
|
|
20
22
|
.option("-o, --output <file>", "Output filename")
|
|
21
23
|
.option("--ai-summary", "Generate an AI-powered executive summary")
|
|
22
24
|
.action(async (scanFile, options) => {
|
|
@@ -34,7 +36,10 @@ function registerReportCommand(program) {
|
|
|
34
36
|
const content = await promises_1.default.readFile(filepath, "utf-8");
|
|
35
37
|
const scanResult = JSON.parse(content);
|
|
36
38
|
const config = await (0, config_1.getConfig)();
|
|
37
|
-
|
|
39
|
+
let format = (options.format || config.report.defaultFormat);
|
|
40
|
+
if (!options.format && !config.report.defaultFormat) {
|
|
41
|
+
format = "markdown";
|
|
42
|
+
}
|
|
38
43
|
let aiSummary;
|
|
39
44
|
if (options.aiSummary) {
|
|
40
45
|
spinner = logger_1.logger.spinner("Generating AI executive summary...");
|
|
@@ -47,17 +52,42 @@ function registerReportCommand(program) {
|
|
|
47
52
|
spinner.warn(`AI summary failed: ${err.message}`);
|
|
48
53
|
}
|
|
49
54
|
}
|
|
55
|
+
let outputDir = "";
|
|
56
|
+
if (!options.output) {
|
|
57
|
+
const { location } = await inquirer_1.default.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: "list",
|
|
60
|
+
name: "location",
|
|
61
|
+
message: "Where should the report be saved?",
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: "Current Project Directory (./)", value: "cwd" },
|
|
64
|
+
{ name: "Desktop", value: "desktop" },
|
|
65
|
+
{ name: "Default Reports Directory (~/.kramscan/reports/)", value: "default" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
if (location === "cwd") {
|
|
70
|
+
outputDir = process.cwd();
|
|
71
|
+
}
|
|
72
|
+
else if (location === "desktop") {
|
|
73
|
+
outputDir = path_1.default.join(os_1.default.homedir(), "Desktop");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
spinner = logger_1.logger.spinner(`Generating ${format.toUpperCase()} report...`);
|
|
51
77
|
let outputPath;
|
|
52
78
|
switch (format) {
|
|
53
79
|
case "word":
|
|
54
|
-
outputPath = await generateWordReport(scanResult, options.output, aiSummary);
|
|
80
|
+
outputPath = await generateWordReport(scanResult, options.output, aiSummary, outputDir);
|
|
55
81
|
break;
|
|
56
82
|
case "json":
|
|
57
|
-
outputPath = await generateJsonReport(scanResult, options.output, aiSummary);
|
|
83
|
+
outputPath = await generateJsonReport(scanResult, options.output, aiSummary, outputDir);
|
|
58
84
|
break;
|
|
59
85
|
case "txt":
|
|
60
|
-
outputPath = await generateTxtReport(scanResult, options.output, aiSummary);
|
|
86
|
+
outputPath = await generateTxtReport(scanResult, options.output, aiSummary, outputDir);
|
|
87
|
+
break;
|
|
88
|
+
case "markdown":
|
|
89
|
+
case "md":
|
|
90
|
+
outputPath = await generateMarkdownReport(scanResult, options.output, aiSummary, outputDir);
|
|
61
91
|
break;
|
|
62
92
|
default:
|
|
63
93
|
throw new Error(`Unsupported format: ${format}`);
|
|
@@ -76,7 +106,7 @@ function registerReportCommand(program) {
|
|
|
76
106
|
}
|
|
77
107
|
});
|
|
78
108
|
}
|
|
79
|
-
async function generateWordReport(scanResult, outputFile, aiSummary) {
|
|
109
|
+
async function generateWordReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
80
110
|
const doc = new docx_1.Document({
|
|
81
111
|
sections: [
|
|
82
112
|
{
|
|
@@ -145,15 +175,15 @@ async function generateWordReport(scanResult, outputFile, aiSummary) {
|
|
|
145
175
|
],
|
|
146
176
|
});
|
|
147
177
|
const buffer = await docx_1.Packer.toBuffer(doc);
|
|
148
|
-
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
178
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
149
179
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
150
180
|
const filename = outputFile || `report-${timestamp}.docx`;
|
|
151
181
|
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
152
182
|
await promises_1.default.writeFile(filepath, buffer);
|
|
153
183
|
return filepath;
|
|
154
184
|
}
|
|
155
|
-
async function generateJsonReport(scanResult, outputFile, aiSummary) {
|
|
156
|
-
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
185
|
+
async function generateJsonReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
186
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
157
187
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
158
188
|
const filename = outputFile || `report-${timestamp}.json`;
|
|
159
189
|
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
@@ -161,7 +191,7 @@ async function generateJsonReport(scanResult, outputFile, aiSummary) {
|
|
|
161
191
|
await promises_1.default.writeFile(filepath, JSON.stringify(finalResult, null, 2));
|
|
162
192
|
return filepath;
|
|
163
193
|
}
|
|
164
|
-
async function generateTxtReport(scanResult, outputFile, aiSummary) {
|
|
194
|
+
async function generateTxtReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
165
195
|
const lines = [];
|
|
166
196
|
lines.push("=".repeat(60));
|
|
167
197
|
lines.push("SECURITY ASSESSMENT REPORT");
|
|
@@ -208,10 +238,58 @@ async function generateTxtReport(scanResult, outputFile, aiSummary) {
|
|
|
208
238
|
lines.push("=".repeat(60));
|
|
209
239
|
lines.push("End of Report");
|
|
210
240
|
lines.push("=".repeat(60));
|
|
211
|
-
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
241
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
212
242
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
213
243
|
const filename = outputFile || `report-${timestamp}.txt`;
|
|
214
244
|
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
215
245
|
await promises_1.default.writeFile(filepath, lines.join("\n"));
|
|
216
246
|
return filepath;
|
|
217
247
|
}
|
|
248
|
+
async function generateMarkdownReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
249
|
+
const lines = [];
|
|
250
|
+
lines.push("# Security Assessment Report");
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push(`**Target:** \`${scanResult.target}\``);
|
|
253
|
+
lines.push(`**Date:** ${new Date(scanResult.timestamp).toLocaleString()}`);
|
|
254
|
+
lines.push(`**Duration:** ${(scanResult.duration / 1000).toFixed(2)}s`);
|
|
255
|
+
lines.push("");
|
|
256
|
+
lines.push("## Executive Summary");
|
|
257
|
+
if (aiSummary) {
|
|
258
|
+
lines.push(aiSummary);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
lines.push(`Total Vulnerabilities: **${scanResult.summary.total}** (${scanResult.summary.critical} Critical, ${scanResult.summary.high} High, ${scanResult.summary.medium} Medium, ${scanResult.summary.low} Low, ${scanResult.summary.info} Info)`);
|
|
262
|
+
}
|
|
263
|
+
lines.push("");
|
|
264
|
+
lines.push("## Scan Statistics");
|
|
265
|
+
lines.push(`- **URLs Crawled:** ${scanResult.metadata.crawledUrls}`);
|
|
266
|
+
lines.push(`- **Forms Tested:** ${scanResult.metadata.testedForms}`);
|
|
267
|
+
lines.push(`- **Requests Made:** ${scanResult.metadata.requestsMade}`);
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push("## Detailed Findings");
|
|
270
|
+
lines.push("");
|
|
271
|
+
scanResult.vulnerabilities.forEach((vuln, index) => {
|
|
272
|
+
lines.push(`### ${index + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`);
|
|
273
|
+
lines.push(`- **URL:** \`${vuln.url}\``);
|
|
274
|
+
lines.push(`- **Type:** ${vuln.type}`);
|
|
275
|
+
lines.push(`- **Description:** ${vuln.description}`);
|
|
276
|
+
if (vuln.evidence) {
|
|
277
|
+
lines.push(`- **Evidence:** \`${vuln.evidence}\``);
|
|
278
|
+
}
|
|
279
|
+
if (vuln.remediation) {
|
|
280
|
+
lines.push(`- **Remediation:** ${vuln.remediation}`);
|
|
281
|
+
}
|
|
282
|
+
if (vuln.cwe) {
|
|
283
|
+
lines.push(`- **CWE:** ${vuln.cwe}`);
|
|
284
|
+
}
|
|
285
|
+
lines.push("");
|
|
286
|
+
});
|
|
287
|
+
lines.push("---");
|
|
288
|
+
lines.push("*Generated by KramScan*");
|
|
289
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
290
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
291
|
+
const filename = outputFile || `report-${timestamp}.md`;
|
|
292
|
+
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
293
|
+
await promises_1.default.writeFile(filepath, lines.join("\n"));
|
|
294
|
+
return filepath;
|
|
295
|
+
}
|
package/dist/commands/scan.js
CHANGED
|
@@ -71,6 +71,9 @@ function registerScanCommand(program) {
|
|
|
71
71
|
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
72
72
|
console.log("");
|
|
73
73
|
}
|
|
74
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
75
|
+
url = `http://${url}`;
|
|
76
|
+
}
|
|
74
77
|
// Validate URL
|
|
75
78
|
try {
|
|
76
79
|
new URL(url);
|
|
@@ -226,6 +229,7 @@ function registerScanCommand(program) {
|
|
|
226
229
|
metadata: result.metadata,
|
|
227
230
|
summary: result.summary,
|
|
228
231
|
vulnerabilities: result.vulnerabilities,
|
|
232
|
+
score: result.score,
|
|
229
233
|
filepath,
|
|
230
234
|
pdfPath,
|
|
231
235
|
});
|
|
@@ -272,6 +276,7 @@ function registerScanCommand(program) {
|
|
|
272
276
|
choices: [
|
|
273
277
|
{ name: "🧠 Analyze findings with AI", value: "analyze" },
|
|
274
278
|
{ name: "📄 Generate a professional report", value: "report" },
|
|
279
|
+
{ name: "🤖 Generate AI-ready Markdown report for fixing issues", value: "markdown" },
|
|
275
280
|
{ name: "👋 Exit to main menu", value: "exit" }
|
|
276
281
|
]
|
|
277
282
|
}
|
|
@@ -288,6 +293,12 @@ function registerScanCommand(program) {
|
|
|
288
293
|
registerReportCommand(reportProgram);
|
|
289
294
|
await reportProgram.parseAsync(["node", "kramscan", "report", filepath]);
|
|
290
295
|
}
|
|
296
|
+
else if (nextAction === "markdown") {
|
|
297
|
+
const { registerReportCommand } = await Promise.resolve().then(() => __importStar(require("./report")));
|
|
298
|
+
const reportProgram = new commander_1.Command();
|
|
299
|
+
registerReportCommand(reportProgram);
|
|
300
|
+
await reportProgram.parseAsync(["node", "kramscan", "report", filepath, "-f", "markdown"]);
|
|
301
|
+
}
|
|
291
302
|
}
|
|
292
303
|
}
|
|
293
304
|
catch (error) {
|
package/dist/commands/scans.js
CHANGED
|
@@ -30,6 +30,10 @@ function registerScansCommand(program) {
|
|
|
30
30
|
console.log(chalk_1.default.gray(" PDF :"), chalk_1.default.white(entry.pdfPath));
|
|
31
31
|
}
|
|
32
32
|
console.log(chalk_1.default.gray(" Findings:"), chalk_1.default.white(`${entry.summary.total} total (${entry.summary.critical}C ${entry.summary.high}H ${entry.summary.medium}M ${entry.summary.low}L ${entry.summary.info}I)`));
|
|
33
|
+
if (entry.score !== undefined) {
|
|
34
|
+
const scoreColor = entry.score > 80 ? chalk_1.default.green : (entry.score > 50 ? chalk_1.default.yellow : chalk_1.default.red);
|
|
35
|
+
console.log(chalk_1.default.gray(" Score :"), scoreColor(`${entry.score}/100`));
|
|
36
|
+
}
|
|
33
37
|
console.log("");
|
|
34
38
|
}
|
|
35
39
|
});
|
|
@@ -39,7 +39,7 @@ exports.ConfigSchema = zod_1.z.object({
|
|
|
39
39
|
scan: zod_1.z.object({
|
|
40
40
|
defaultTimeout: zod_1.z.number().int().min(1000).default(30000),
|
|
41
41
|
maxThreads: zod_1.z.number().int().min(1).max(20).default(5),
|
|
42
|
-
userAgent: zod_1.z.string().default("KramScan/0.
|
|
42
|
+
userAgent: zod_1.z.string().default("KramScan/0.2.0"),
|
|
43
43
|
followRedirects: zod_1.z.boolean().default(true),
|
|
44
44
|
verifySSL: zod_1.z.boolean().default(true),
|
|
45
45
|
rateLimitPerSecond: zod_1.z.number().int().min(1).max(100).default(5),
|
package/dist/core/config.js
CHANGED
|
@@ -64,7 +64,7 @@ const defaults = {
|
|
|
64
64
|
enabled: false,
|
|
65
65
|
},
|
|
66
66
|
scan: {
|
|
67
|
-
defaultTimeout:
|
|
67
|
+
defaultTimeout: 30000,
|
|
68
68
|
maxThreads: 5,
|
|
69
69
|
userAgent: `KramScan/${theme_1.CLI_VERSION}`,
|
|
70
70
|
followRedirects: true,
|
|
@@ -81,8 +81,8 @@ const defaults = {
|
|
|
81
81
|
severityThreshold: "low",
|
|
82
82
|
},
|
|
83
83
|
skills: {
|
|
84
|
-
sqli: { enabled: true, timeout:
|
|
85
|
-
xss: { enabled: true, timeout:
|
|
84
|
+
sqli: { enabled: true, timeout: 120000 },
|
|
85
|
+
xss: { enabled: true, timeout: 90000 },
|
|
86
86
|
headers: { enabled: true },
|
|
87
87
|
csrf: { enabled: true },
|
|
88
88
|
idor: { enabled: true },
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Vulnerability, ScanResult } from "./vulnerability-detector";
|
|
2
|
+
export interface ScanDiff {
|
|
3
|
+
newVulnerabilities: Vulnerability[];
|
|
4
|
+
resolvedVulnerabilities: Vulnerability[];
|
|
5
|
+
unchangedCount: number;
|
|
6
|
+
previousTotal: number;
|
|
7
|
+
currentTotal: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compares two scan results and produces a diff of new and resolved vulnerabilities.
|
|
11
|
+
*/
|
|
12
|
+
export declare function diffScanResults(previous: ScanResult, current: ScanResult): ScanDiff;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diffScanResults = diffScanResults;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a unique fingerprint for a vulnerability to enable comparison.
|
|
6
|
+
*/
|
|
7
|
+
function vulnFingerprint(v) {
|
|
8
|
+
return `${v.type}:${v.severity}:${v.title}:${new URL(v.url).pathname}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compares two scan results and produces a diff of new and resolved vulnerabilities.
|
|
12
|
+
*/
|
|
13
|
+
function diffScanResults(previous, current) {
|
|
14
|
+
const prevFingerprints = new Map();
|
|
15
|
+
const currFingerprints = new Map();
|
|
16
|
+
for (const v of previous.vulnerabilities) {
|
|
17
|
+
prevFingerprints.set(vulnFingerprint(v), v);
|
|
18
|
+
}
|
|
19
|
+
for (const v of current.vulnerabilities) {
|
|
20
|
+
currFingerprints.set(vulnFingerprint(v), v);
|
|
21
|
+
}
|
|
22
|
+
const newVulnerabilities = [];
|
|
23
|
+
const resolvedVulnerabilities = [];
|
|
24
|
+
let unchangedCount = 0;
|
|
25
|
+
// Find new vulnerabilities (in current but not in previous)
|
|
26
|
+
for (const [fp, vuln] of currFingerprints) {
|
|
27
|
+
if (!prevFingerprints.has(fp)) {
|
|
28
|
+
newVulnerabilities.push(vuln);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
unchangedCount++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Find resolved vulnerabilities (in previous but not in current)
|
|
35
|
+
for (const [fp, vuln] of prevFingerprints) {
|
|
36
|
+
if (!currFingerprints.has(fp)) {
|
|
37
|
+
resolvedVulnerabilities.push(vuln);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
newVulnerabilities,
|
|
42
|
+
resolvedVulnerabilities,
|
|
43
|
+
unchangedCount,
|
|
44
|
+
previousTotal: previous.vulnerabilities.length,
|
|
45
|
+
currentTotal: current.vulnerabilities.length,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -13,6 +13,7 @@ export interface ScanIndexEntry {
|
|
|
13
13
|
low: number;
|
|
14
14
|
info: number;
|
|
15
15
|
};
|
|
16
|
+
score?: number;
|
|
16
17
|
}
|
|
17
18
|
export declare function addScanToIndex(entry: Omit<ScanIndexEntry, "id">): Promise<ScanIndexEntry>;
|
|
18
19
|
export declare function listScans(limit?: number): Promise<ScanIndexEntry[]>;
|
package/dist/core/scanner.js
CHANGED
|
@@ -30,7 +30,7 @@ class Scanner extends events_1.EventEmitter {
|
|
|
30
30
|
maxLinksPerPage = 50;
|
|
31
31
|
includePatterns = [];
|
|
32
32
|
excludePatterns = [];
|
|
33
|
-
userAgent = "KramScan/0.
|
|
33
|
+
userAgent = "KramScan/0.2.0";
|
|
34
34
|
scanErrors = [];
|
|
35
35
|
pluginErrors = new Map();
|
|
36
36
|
usePlugins = true;
|
|
@@ -63,6 +63,11 @@ class Scanner extends events_1.EventEmitter {
|
|
|
63
63
|
plugins_1.pluginManager.register(new plugins_2.SecurityHeadersPlugin());
|
|
64
64
|
plugins_1.pluginManager.register(new plugins_2.SensitiveDataPlugin());
|
|
65
65
|
plugins_1.pluginManager.register(new plugins_2.CSRFPlugin());
|
|
66
|
+
plugins_1.pluginManager.register(new plugins_2.CORSAnalyzerPlugin());
|
|
67
|
+
plugins_1.pluginManager.register(new plugins_2.DebugEndpointPlugin());
|
|
68
|
+
plugins_1.pluginManager.register(new plugins_2.DirectoryTraversalPlugin());
|
|
69
|
+
plugins_1.pluginManager.register(new plugins_2.CookieSecurityPlugin());
|
|
70
|
+
plugins_1.pluginManager.register(new plugins_2.OpenRedirectPlugin());
|
|
66
71
|
}
|
|
67
72
|
// Type-safe event emitter methods
|
|
68
73
|
emit(event, data) {
|
|
@@ -189,6 +194,7 @@ class Scanner extends events_1.EventEmitter {
|
|
|
189
194
|
testedForms: this.testedForms,
|
|
190
195
|
requestsMade: this.requestsMade,
|
|
191
196
|
},
|
|
197
|
+
score: this.detector.calculateScore(),
|
|
192
198
|
};
|
|
193
199
|
this.emit("scan:complete", { result });
|
|
194
200
|
return result;
|
|
@@ -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,6 +28,7 @@ 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;
|
|
@@ -59,5 +60,10 @@ export declare class VulnerabilityDetector {
|
|
|
59
60
|
low: number;
|
|
60
61
|
info: number;
|
|
61
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Calculates a security score from 0-100
|
|
65
|
+
* 100 is perfect, 0 is very poor
|
|
66
|
+
*/
|
|
67
|
+
calculateScore(): number;
|
|
62
68
|
clear(): void;
|
|
63
69
|
}
|
|
@@ -452,6 +452,27 @@ class VulnerabilityDetector {
|
|
|
452
452
|
}
|
|
453
453
|
return summary;
|
|
454
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
|
+
}
|
|
455
476
|
clear() {
|
|
456
477
|
this.vulnerabilities = [];
|
|
457
478
|
this.reportedHeaders.clear();
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const cli_1 = require("./cli");
|
|
4
7
|
const errors_1 = require("./core/errors");
|
|
8
|
+
const update_notifier_1 = __importDefault(require("update-notifier"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
5
11
|
// Ensure uncaught exceptions and unhandled rejections produce useful output
|
|
6
12
|
(0, errors_1.setupGlobalErrorHandlers)();
|
|
13
|
+
try {
|
|
14
|
+
const pkgPath = path_1.default.join(__dirname, "../package.json");
|
|
15
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
|
|
16
|
+
(0, update_notifier_1.default)({ pkg }).notify({ isGlobal: true });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// Silently ignore if update notification fails
|
|
20
|
+
}
|
|
7
21
|
(0, cli_1.run)().catch((err) => {
|
|
8
22
|
console.error("Fatal error:", err);
|
|
9
23
|
process.exit(1);
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -5,3 +5,8 @@ export { SQLInjectionPlugin } from "./vulnerabilities/SQLInjectionPlugin";
|
|
|
5
5
|
export { SecurityHeadersPlugin } from "./vulnerabilities/SecurityHeadersPlugin";
|
|
6
6
|
export { SensitiveDataPlugin } from "./vulnerabilities/SensitiveDataPlugin";
|
|
7
7
|
export { CSRFPlugin } from "./vulnerabilities/CSRFPlugin";
|
|
8
|
+
export { CORSAnalyzerPlugin } from "./vulnerabilities/CORSAnalyzerPlugin";
|
|
9
|
+
export { DebugEndpointPlugin } from "./vulnerabilities/DebugEndpointPlugin";
|
|
10
|
+
export { DirectoryTraversalPlugin } from "./vulnerabilities/DirectoryTraversalPlugin";
|
|
11
|
+
export { CookieSecurityPlugin } from "./vulnerabilities/CookieSecurityPlugin";
|
|
12
|
+
export { OpenRedirectPlugin } from "./vulnerabilities/OpenRedirectPlugin";
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
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;
|
|
3
|
+
exports.OpenRedirectPlugin = exports.CookieSecurityPlugin = exports.DirectoryTraversalPlugin = exports.DebugEndpointPlugin = exports.CORSAnalyzerPlugin = exports.CSRFPlugin = exports.SensitiveDataPlugin = exports.SecurityHeadersPlugin = exports.SQLInjectionPlugin = exports.XSSPlugin = exports.pluginManager = exports.PluginManager = exports.BaseVulnerabilityPlugin = void 0;
|
|
4
4
|
var types_1 = require("./types");
|
|
5
5
|
Object.defineProperty(exports, "BaseVulnerabilityPlugin", { enumerable: true, get: function () { return types_1.BaseVulnerabilityPlugin; } });
|
|
6
6
|
var PluginManager_1 = require("./PluginManager");
|
|
@@ -17,3 +17,13 @@ var SensitiveDataPlugin_1 = require("./vulnerabilities/SensitiveDataPlugin");
|
|
|
17
17
|
Object.defineProperty(exports, "SensitiveDataPlugin", { enumerable: true, get: function () { return SensitiveDataPlugin_1.SensitiveDataPlugin; } });
|
|
18
18
|
var CSRFPlugin_1 = require("./vulnerabilities/CSRFPlugin");
|
|
19
19
|
Object.defineProperty(exports, "CSRFPlugin", { enumerable: true, get: function () { return CSRFPlugin_1.CSRFPlugin; } });
|
|
20
|
+
var CORSAnalyzerPlugin_1 = require("./vulnerabilities/CORSAnalyzerPlugin");
|
|
21
|
+
Object.defineProperty(exports, "CORSAnalyzerPlugin", { enumerable: true, get: function () { return CORSAnalyzerPlugin_1.CORSAnalyzerPlugin; } });
|
|
22
|
+
var DebugEndpointPlugin_1 = require("./vulnerabilities/DebugEndpointPlugin");
|
|
23
|
+
Object.defineProperty(exports, "DebugEndpointPlugin", { enumerable: true, get: function () { return DebugEndpointPlugin_1.DebugEndpointPlugin; } });
|
|
24
|
+
var DirectoryTraversalPlugin_1 = require("./vulnerabilities/DirectoryTraversalPlugin");
|
|
25
|
+
Object.defineProperty(exports, "DirectoryTraversalPlugin", { enumerable: true, get: function () { return DirectoryTraversalPlugin_1.DirectoryTraversalPlugin; } });
|
|
26
|
+
var CookieSecurityPlugin_1 = require("./vulnerabilities/CookieSecurityPlugin");
|
|
27
|
+
Object.defineProperty(exports, "CookieSecurityPlugin", { enumerable: true, get: function () { return CookieSecurityPlugin_1.CookieSecurityPlugin; } });
|
|
28
|
+
var OpenRedirectPlugin_1 = require("./vulnerabilities/OpenRedirectPlugin");
|
|
29
|
+
Object.defineProperty(exports, "OpenRedirectPlugin", { enumerable: true, get: function () { return OpenRedirectPlugin_1.OpenRedirectPlugin; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
import { Vulnerability } from "../../core/vulnerability-detector";
|
|
3
|
+
export declare class CORSAnalyzerPlugin extends BaseVulnerabilityPlugin {
|
|
4
|
+
readonly name = "CORS Analyzer";
|
|
5
|
+
readonly type: "header";
|
|
6
|
+
readonly description = "Detects overly permissive Cross-Origin Resource Sharing configurations";
|
|
7
|
+
private readonly reportedHosts;
|
|
8
|
+
analyzeHeaders(context: PluginContext, headers: Record<string, string>): Promise<Vulnerability[]>;
|
|
9
|
+
reset(): void;
|
|
10
|
+
}
|