kramscan 0.3.0 → 0.4.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.
@@ -64,11 +64,11 @@ class HealthCheckSkill {
64
64
  },
65
65
  ],
66
66
  };
67
- validateParameters(params) {
67
+ validateParameters(_params) {
68
68
  // No required parameters
69
69
  return { valid: true, errors: [] };
70
70
  }
71
- async execute(params, context) {
71
+ async execute(params, _context) {
72
72
  const verbose = params.verbose ?? false;
73
73
  logger_1.logger.info("Running health check...");
74
74
  const checks = [];
@@ -48,7 +48,7 @@ class VerifyFindingSkill {
48
48
  // Get type from finding metadata or title
49
49
  const vulnType = finding.metadata?.type || (finding.title.toLowerCase().includes("sql") ? "sqli" : "xss");
50
50
  // Generate non-destructive verification payloads
51
- const payloads = await payloadGenerator.generatePayloads(vulnType, {
51
+ await payloadGenerator.generatePayloads(vulnType, {
52
52
  parameterName: finding.metadata?.parameter || "verify",
53
53
  url: finding.metadata?.url || context.currentTarget || "",
54
54
  });
@@ -17,6 +17,6 @@ export declare class WebScanSkill implements AgentSkill {
17
17
  valid: boolean;
18
18
  errors: string[];
19
19
  };
20
- execute(params: Record<string, unknown>, context: AgentContext): Promise<SkillResult>;
20
+ execute(params: Record<string, unknown>, _context: AgentContext): Promise<SkillResult>;
21
21
  run(): Promise<SkillResult>;
22
22
  }
@@ -126,7 +126,7 @@ class WebScanSkill {
126
126
  errors,
127
127
  };
128
128
  }
129
- async execute(params, context) {
129
+ async execute(params, _context) {
130
130
  const targetUrl = params.targetUrl;
131
131
  const depth = params.depth ?? 2;
132
132
  const timeout = params.timeout ?? 30000;
package/dist/cli.js CHANGED
@@ -53,6 +53,7 @@ const scans_1 = require("./commands/scans");
53
53
  const ai_1 = require("./commands/ai");
54
54
  const dev_1 = require("./commands/dev");
55
55
  const gate_1 = require("./commands/gate");
56
+ const init_1 = require("./commands/init");
56
57
  const config_2 = require("./core/config");
57
58
  const theme_1 = require("./utils/theme");
58
59
  let verboseMode = false;
@@ -71,6 +72,7 @@ function debugLog(...args) {
71
72
  const menuChoices = [
72
73
  { label: "Agent", value: "agent", description: "AI-powered interactive security assistant", icon: "🤖", status: "active" },
73
74
  { label: "Onboard", value: "onboard", description: "First-time setup wizard", icon: "⚡", status: "active" },
75
+ { label: "Init", value: "init", description: "Generate .kramscanrc for this project", icon: "📁", status: "active" },
74
76
  { label: "Scan", value: "scan", description: "Scan a target URL for vulnerabilities", icon: "🔍", status: "active" },
75
77
  { label: "Dev", value: "dev", description: "Watch-mode scanning for localhost dev servers", icon: "🛠️", status: "active" },
76
78
  { label: "Gate", value: "gate", description: "CI/CD security quality gate", icon: "🚧", status: "active" },
@@ -107,7 +109,7 @@ async function showInteractiveMenu() {
107
109
  console.log(theme_1.theme.gray(` Run ${theme_1.theme.cyan("kramscan --help")} for available commands.\n`));
108
110
  return;
109
111
  }
110
- let args = [action];
112
+ const args = [action];
111
113
  // Specific handling for commands that need input
112
114
  if (action === "scan") {
113
115
  const { url } = await inquirer_1.default.prompt([
@@ -117,7 +119,8 @@ async function showInteractiveMenu() {
117
119
  message: theme_1.theme.cyan("Enter the URL to scan:"),
118
120
  validate: (input) => {
119
121
  try {
120
- new URL(input);
122
+ const urlToTest = /^https?:\/\//i.test(input) ? input : `http://${input}`;
123
+ new URL(urlToTest);
121
124
  return true;
122
125
  }
123
126
  catch {
@@ -178,6 +181,7 @@ async function showInteractiveMenu() {
178
181
  // Error handling is managed by the commands themselves or global handlers
179
182
  }
180
183
  }
184
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
181
185
  async function showDirectCommandInput() {
182
186
  (0, theme_1.printBanner)();
183
187
  (0, theme_1.printInfo)();
@@ -226,6 +230,7 @@ function createProgram() {
226
230
  (0, ai_1.registerAiCommand)(program);
227
231
  (0, dev_1.registerDevCommand)(program);
228
232
  (0, gate_1.registerGateCommand)(program);
233
+ (0, init_1.registerInitCommand)(program);
229
234
  // Version subcommand with detailed environment info
230
235
  program
231
236
  .command("version")
@@ -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"));
@@ -138,7 +138,7 @@ async function checkPuppeteer() {
138
138
  }
139
139
  async function checkConfig() {
140
140
  try {
141
- const config = await (0, config_1.getConfig)();
141
+ await (0, config_1.getConfig)();
142
142
  return {
143
143
  name: "Configuration",
144
144
  status: "pass",
@@ -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("");
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Init Command
3
+ * Generates a .kramscanrc project configuration file in the current directory.
4
+ */
5
+ import { Command } from "commander";
6
+ export declare function registerInitCommand(program: Command): void;
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ /**
3
+ * Init Command
4
+ * Generates a .kramscanrc project configuration file in the current directory.
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.registerInitCommand = registerInitCommand;
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ const inquirer_1 = __importDefault(require("inquirer"));
13
+ const promises_1 = __importDefault(require("fs/promises"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const project_config_1 = require("../core/project-config");
16
+ const logger_1 = require("../utils/logger");
17
+ function registerInitCommand(program) {
18
+ program
19
+ .command("init")
20
+ .description("Generate a .kramscanrc project configuration file")
21
+ .option("-y, --yes", "Skip prompts and generate with defaults")
22
+ .option("--force", "Overwrite existing .kramscanrc file")
23
+ .action(async (options) => {
24
+ const targetPath = path_1.default.join(process.cwd(), project_config_1.PROJECT_CONFIG_FILENAME);
25
+ console.log("");
26
+ console.log(chalk_1.default.bold.cyan("KramScan Project Setup"));
27
+ console.log(chalk_1.default.gray("─".repeat(50)));
28
+ console.log("");
29
+ // Check if file already exists
30
+ try {
31
+ await promises_1.default.access(targetPath);
32
+ if (!options.force) {
33
+ console.log(chalk_1.default.yellow(` A ${project_config_1.PROJECT_CONFIG_FILENAME} file already exists in this directory.`));
34
+ console.log(chalk_1.default.gray(` Use ${chalk_1.default.white("--force")} to overwrite it.`));
35
+ console.log("");
36
+ return;
37
+ }
38
+ }
39
+ catch {
40
+ // File doesn't exist — good
41
+ }
42
+ let config;
43
+ if (options.yes) {
44
+ config = getDefaultProjectConfig();
45
+ }
46
+ else {
47
+ config = await runInteractiveSetup();
48
+ }
49
+ // Write the file
50
+ const content = JSON.stringify(config, null, 2) + "\n";
51
+ await promises_1.default.writeFile(targetPath, content, "utf-8");
52
+ console.log("");
53
+ logger_1.logger.success(`Created ${project_config_1.PROJECT_CONFIG_FILENAME} in ${process.cwd()}`);
54
+ console.log("");
55
+ console.log(chalk_1.default.gray(" This file configures KramScan for this project."));
56
+ console.log(chalk_1.default.gray(" Commit it to version control so your team shares the same settings."));
57
+ console.log(chalk_1.default.gray(` API keys are never stored here — use ${chalk_1.default.white("kramscan onboard")} or env vars.`));
58
+ console.log("");
59
+ });
60
+ }
61
+ function getDefaultProjectConfig() {
62
+ return {
63
+ scan: {
64
+ defaultProfile: "balanced",
65
+ defaultTimeout: 30000,
66
+ strictScope: true,
67
+ exclude: [
68
+ "logout",
69
+ "signout",
70
+ "delete",
71
+ ],
72
+ },
73
+ report: {
74
+ defaultFormat: "markdown",
75
+ companyName: "Your Company",
76
+ },
77
+ gate: {
78
+ failOn: "high",
79
+ maxVulns: 0,
80
+ },
81
+ plugins: {
82
+ disabled: [],
83
+ },
84
+ };
85
+ }
86
+ async function runInteractiveSetup() {
87
+ const answers = await inquirer_1.default.prompt([
88
+ {
89
+ type: "list",
90
+ name: "profile",
91
+ message: "Default scan profile:",
92
+ choices: [
93
+ { name: "quick — fast surface-level scan", value: "quick" },
94
+ { name: "balanced — good coverage, moderate speed", value: "balanced" },
95
+ { name: "deep — thorough crawl, slower", value: "deep" },
96
+ ],
97
+ default: "balanced",
98
+ },
99
+ {
100
+ type: "input",
101
+ name: "timeout",
102
+ message: "Default request timeout (ms):",
103
+ default: "30000",
104
+ validate: (input) => {
105
+ const n = parseInt(input, 10);
106
+ if (isNaN(n) || n < 1000)
107
+ return "Must be a number >= 1000";
108
+ return true;
109
+ },
110
+ filter: (input) => parseInt(input, 10),
111
+ },
112
+ {
113
+ type: "confirm",
114
+ name: "strictScope",
115
+ message: "Stay within the target domain? (strict scope)",
116
+ default: true,
117
+ },
118
+ {
119
+ type: "input",
120
+ name: "exclude",
121
+ message: "URL patterns to exclude (comma-separated, e.g. logout,signout):",
122
+ default: "logout,signout,delete",
123
+ filter: (input) => input
124
+ .split(",")
125
+ .map((s) => s.trim())
126
+ .filter(Boolean),
127
+ },
128
+ {
129
+ type: "list",
130
+ name: "reportFormat",
131
+ message: "Default report format:",
132
+ choices: [
133
+ { name: "markdown", value: "markdown" },
134
+ { name: "word (.docx)", value: "word" },
135
+ { name: "json", value: "json" },
136
+ { name: "txt", value: "txt" },
137
+ ],
138
+ default: "markdown",
139
+ },
140
+ {
141
+ type: "input",
142
+ name: "companyName",
143
+ message: "Company or project name (for report headers):",
144
+ default: path_1.default.basename(process.cwd()),
145
+ },
146
+ {
147
+ type: "list",
148
+ name: "gateFailOn",
149
+ message: "CI/CD gate — fail on severity at or above:",
150
+ choices: [
151
+ { name: "critical — only block on critical issues", value: "critical" },
152
+ { name: "high — block on high and critical", value: "high" },
153
+ { name: "medium — block on medium and above", value: "medium" },
154
+ { name: "low — block on everything except info", value: "low" },
155
+ ],
156
+ default: "high",
157
+ },
158
+ {
159
+ type: "input",
160
+ name: "gateMaxVulns",
161
+ message: "CI/CD gate — max allowed vulnerabilities before failing:",
162
+ default: "0",
163
+ validate: (input) => {
164
+ const n = parseInt(input, 10);
165
+ if (isNaN(n) || n < 0)
166
+ return "Must be a non-negative number";
167
+ return true;
168
+ },
169
+ filter: (input) => parseInt(input, 10),
170
+ },
171
+ ]);
172
+ return {
173
+ scan: {
174
+ defaultProfile: answers.profile,
175
+ defaultTimeout: answers.timeout,
176
+ strictScope: answers.strictScope,
177
+ exclude: answers.exclude,
178
+ },
179
+ report: {
180
+ defaultFormat: answers.reportFormat,
181
+ companyName: answers.companyName,
182
+ },
183
+ gate: {
184
+ failOn: answers.gateFailOn,
185
+ maxVulns: answers.gateMaxVulns,
186
+ },
187
+ plugins: {
188
+ disabled: [],
189
+ },
190
+ };
191
+ }
@@ -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);
@@ -116,23 +119,18 @@ function registerScanCommand(program) {
116
119
  console.log("");
117
120
  }
118
121
  const scanner = new scanner_1.Scanner(options.plugins !== false);
119
- // Set up event listeners for progress feedback
120
- let currentStage = "initializing";
121
122
  let vulnerabilitiesFound = 0;
122
123
  scanner.on("scan:start", () => {
123
124
  if (spinner)
124
125
  spinner.text = `Starting scan of ${url}...`;
125
- currentStage = "scanning";
126
126
  });
127
127
  scanner.on("crawl:page", (data) => {
128
128
  if (spinner)
129
129
  spinner.text = `Crawling: ${data.url} (${data.crawledCount}/${data.maxPages})`;
130
- currentStage = "crawling";
131
130
  });
132
131
  scanner.on("form:test", (data) => {
133
132
  if (spinner)
134
133
  spinner.text = `Testing forms on ${data.url} (${data.formCount} forms)...`;
135
- currentStage = "testing forms";
136
134
  });
137
135
  scanner.on("vuln:found", (data) => {
138
136
  vulnerabilitiesFound++;
@@ -273,6 +271,7 @@ function registerScanCommand(program) {
273
271
  choices: [
274
272
  { name: "🧠 Analyze findings with AI", value: "analyze" },
275
273
  { name: "📄 Generate a professional report", value: "report" },
274
+ { name: "🤖 Generate AI-ready Markdown report for fixing issues", value: "markdown" },
276
275
  { name: "👋 Exit to main menu", value: "exit" }
277
276
  ]
278
277
  }
@@ -289,6 +288,12 @@ function registerScanCommand(program) {
289
288
  registerReportCommand(reportProgram);
290
289
  await reportProgram.parseAsync(["node", "kramscan", "report", filepath]);
291
290
  }
291
+ else if (nextAction === "markdown") {
292
+ const { registerReportCommand } = await Promise.resolve().then(() => __importStar(require("./report")));
293
+ const reportProgram = new commander_1.Command();
294
+ registerReportCommand(reportProgram);
295
+ await reportProgram.parseAsync(["node", "kramscan", "report", filepath, "-f", "markdown"]);
296
+ }
292
297
  }
293
298
  }
294
299
  catch (error) {
@@ -49,6 +49,7 @@ const fs = __importStar(require("fs"));
49
49
  const path = __importStar(require("path"));
50
50
  const os = __importStar(require("os"));
51
51
  const config_schema_1 = require("./config-schema");
52
+ const project_config_1 = require("./project-config");
52
53
  const theme_1 = require("../utils/theme");
53
54
  var config_schema_2 = require("./config-schema");
54
55
  Object.defineProperty(exports, "scanProfiles", { enumerable: true, get: function () { return config_schema_2.defaultScanProfiles; } });
@@ -363,7 +364,13 @@ function getConfigStore() {
363
364
  }
364
365
  async function getConfig() {
365
366
  await ensureInitialized();
366
- return store.store;
367
+ const globalConfig = store.store;
368
+ // Merge project-level .kramscanrc if present
369
+ const project = (0, project_config_1.findProjectConfig)();
370
+ if (project) {
371
+ return (0, project_config_1.deepMerge)(globalConfig, project.config);
372
+ }
373
+ return globalConfig;
367
374
  }
368
375
  async function getConfigValue(key) {
369
376
  await ensureInitialized();
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Project-level configuration (.kramscanrc)
3
+ *
4
+ * Discovers and loads a .kramscanrc file from the current working directory
5
+ * (or any parent directory) and merges it with the global config. Project
6
+ * settings override global settings, but sensitive fields (ai.apiKey) are
7
+ * never read from the project file.
8
+ */
9
+ export declare const PROJECT_CONFIG_FILENAME = ".kramscanrc";
10
+ /**
11
+ * Represents the subset of config fields that can be set at the project level.
12
+ * Intentionally excludes ai.apiKey for security.
13
+ */
14
+ export interface ProjectConfig {
15
+ scan?: {
16
+ defaultProfile?: string;
17
+ defaultTimeout?: number;
18
+ maxThreads?: number;
19
+ followRedirects?: boolean;
20
+ verifySSL?: boolean;
21
+ rateLimitPerSecond?: number;
22
+ strictScope?: boolean;
23
+ include?: string[];
24
+ exclude?: string[];
25
+ profiles?: Record<string, {
26
+ depth?: number;
27
+ timeout?: number;
28
+ maxPages?: number;
29
+ maxLinksPerPage?: number;
30
+ }>;
31
+ };
32
+ report?: {
33
+ defaultFormat?: string;
34
+ companyName?: string;
35
+ includeScreenshots?: boolean;
36
+ severityThreshold?: string;
37
+ };
38
+ gate?: {
39
+ failOn?: string;
40
+ maxVulns?: number;
41
+ };
42
+ plugins?: {
43
+ disabled?: string[];
44
+ };
45
+ }
46
+ /**
47
+ * Search for a .kramscanrc file starting from `startDir` and walking up
48
+ * to the filesystem root. Returns the parsed contents and file path,
49
+ * or null if no file is found.
50
+ */
51
+ export declare function findProjectConfig(startDir?: string): {
52
+ config: ProjectConfig;
53
+ filepath: string;
54
+ } | null;
55
+ /**
56
+ * Deep merge `source` into `target`, returning a new object. Arrays are
57
+ * replaced, not concatenated. Undefined values in source are skipped.
58
+ */
59
+ export declare function deepMerge<T extends Record<string, unknown>>(target: T, source: Record<string, unknown>): T;