kramscan 0.3.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/dist/cli.js +2 -1
- package/dist/commands/config.js +2 -2
- package/dist/commands/dev.js +4 -1
- package/dist/commands/gate.js +3 -0
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/report.js +89 -11
- package/dist/commands/scan.js +10 -0
- package/dist/index.js +14 -0
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -117,7 +117,8 @@ async function showInteractiveMenu() {
|
|
|
117
117
|
message: theme_1.theme.cyan("Enter the URL to scan:"),
|
|
118
118
|
validate: (input) => {
|
|
119
119
|
try {
|
|
120
|
-
|
|
120
|
+
const urlToTest = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
|
121
|
+
new URL(urlToTest);
|
|
121
122
|
return true;
|
|
122
123
|
}
|
|
123
124
|
catch {
|
package/dist/commands/config.js
CHANGED
|
@@ -107,8 +107,8 @@ function registerConfigCommand(program) {
|
|
|
107
107
|
type: "list",
|
|
108
108
|
name: "reportFormat",
|
|
109
109
|
message: "Default report format:",
|
|
110
|
-
choices: ["word", "json", "txt"],
|
|
111
|
-
default: config.report.defaultFormat,
|
|
110
|
+
choices: ["markdown", "word", "json", "txt"],
|
|
111
|
+
default: config.report.defaultFormat || "markdown",
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
type: "number",
|
package/dist/commands/dev.js
CHANGED
|
@@ -25,7 +25,7 @@ function registerDevCommand(program) {
|
|
|
25
25
|
.option("--no-watch", "Run a single scan without watching (useful for CI)")
|
|
26
26
|
.action(async (url, options) => {
|
|
27
27
|
// Resolve target URL
|
|
28
|
-
|
|
28
|
+
let targetUrl = url || (options.port ? `http://localhost:${options.port}` : null);
|
|
29
29
|
if (!targetUrl) {
|
|
30
30
|
console.log("");
|
|
31
31
|
console.log(theme_1.theme.error("✗ No target URL specified."));
|
|
@@ -34,6 +34,9 @@ function registerDevCommand(program) {
|
|
|
34
34
|
console.log("");
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
|
+
if (!/^https?:\/\//i.test(targetUrl)) {
|
|
38
|
+
targetUrl = `http://${targetUrl}`;
|
|
39
|
+
}
|
|
37
40
|
const isLocal = (0, server_probe_1.isLocalhost)(targetUrl);
|
|
38
41
|
console.log("");
|
|
39
42
|
console.log(theme_1.theme.brand.bold("🛠️ KramScan Dev Mode"));
|
package/dist/commands/gate.js
CHANGED
|
@@ -15,6 +15,9 @@ function registerGateCommand(program) {
|
|
|
15
15
|
.option("--timeout <ms>", "Maximum scan duration", "60000")
|
|
16
16
|
.option("--json", "Output results as JSON")
|
|
17
17
|
.action(async (url, options) => {
|
|
18
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
19
|
+
url = `http://${url}`;
|
|
20
|
+
}
|
|
18
21
|
const jsonMode = options.json === true;
|
|
19
22
|
if (!jsonMode) {
|
|
20
23
|
console.log("");
|
package/dist/commands/onboard.js
CHANGED
|
@@ -140,8 +140,8 @@ function registerOnboardCommand(program) {
|
|
|
140
140
|
type: "list",
|
|
141
141
|
name: "reportFormat",
|
|
142
142
|
message: "Default report format",
|
|
143
|
-
choices: ["word", "txt", "json"],
|
|
144
|
-
default: config.report.defaultFormat,
|
|
143
|
+
choices: ["markdown", "word", "txt", "json"],
|
|
144
|
+
default: config.report.defaultFormat || "markdown",
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
type: "confirm",
|
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);
|
|
@@ -273,6 +276,7 @@ function registerScanCommand(program) {
|
|
|
273
276
|
choices: [
|
|
274
277
|
{ name: "🧠 Analyze findings with AI", value: "analyze" },
|
|
275
278
|
{ name: "📄 Generate a professional report", value: "report" },
|
|
279
|
+
{ name: "🤖 Generate AI-ready Markdown report for fixing issues", value: "markdown" },
|
|
276
280
|
{ name: "👋 Exit to main menu", value: "exit" }
|
|
277
281
|
]
|
|
278
282
|
}
|
|
@@ -289,6 +293,12 @@ function registerScanCommand(program) {
|
|
|
289
293
|
registerReportCommand(reportProgram);
|
|
290
294
|
await reportProgram.parseAsync(["node", "kramscan", "report", filepath]);
|
|
291
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
|
+
}
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
catch (error) {
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kramscan",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "KramScan CLI — AI-powered web app security testing",
|
|
5
5
|
"author": "Akram Shaikh (https://akramshaikh.me)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.31.0",
|
|
55
55
|
"@google/generative-ai": "^0.24.1",
|
|
56
56
|
"@mistralai/mistralai": "^1.14.0",
|
|
57
|
+
"@types/update-notifier": "^5.1.0",
|
|
57
58
|
"axios": "^1.6.8",
|
|
58
59
|
"chalk": "^5.6.2",
|
|
59
60
|
"commander": "^12.1.0",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"openai": "^4.104.0",
|
|
66
67
|
"ora": "^8.2.0",
|
|
67
68
|
"puppeteer": "^22.15.0",
|
|
69
|
+
"update-notifier": "^5.1.0",
|
|
68
70
|
"uuid": "^9.0.1"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|