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 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
- new URL(input);
120
+ const urlToTest = /^https?:\/\//i.test(input) ? input : `http://${input}`;
121
+ new URL(urlToTest);
121
122
  return true;
122
123
  }
123
124
  catch {
@@ -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",
@@ -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
- const targetUrl = url || (options.port ? `http://localhost:${options.port}` : null);
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"));
@@ -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("");
@@ -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",
@@ -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
- const format = (options.format || config.report.defaultFormat);
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
+ }
@@ -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.0",
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": {