kramscan 0.1.1 → 0.2.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.
Files changed (72) hide show
  1. package/README.md +392 -236
  2. package/dist/agent/confirmation.d.ts +5 -1
  3. package/dist/agent/confirmation.js +29 -9
  4. package/dist/agent/context.js +2 -3
  5. package/dist/agent/orchestrator.d.ts +2 -0
  6. package/dist/agent/orchestrator.js +50 -8
  7. package/dist/agent/prompts/system.d.ts +1 -1
  8. package/dist/agent/prompts/system.js +5 -7
  9. package/dist/agent/skills/health-check.js +22 -2
  10. package/dist/agent/skills/index.d.ts +1 -0
  11. package/dist/agent/skills/index.js +3 -1
  12. package/dist/agent/skills/verify-finding.d.ts +17 -0
  13. package/dist/agent/skills/verify-finding.js +91 -0
  14. package/dist/agent/skills/web-scan.js +46 -0
  15. package/dist/cli.js +150 -149
  16. package/dist/commands/agent.js +38 -38
  17. package/dist/commands/ai.d.ts +2 -0
  18. package/dist/commands/ai.js +112 -0
  19. package/dist/commands/analyze.js +103 -54
  20. package/dist/commands/config.js +55 -29
  21. package/dist/commands/doctor.js +20 -15
  22. package/dist/commands/onboard.js +188 -141
  23. package/dist/commands/report.js +68 -76
  24. package/dist/commands/scan.js +261 -81
  25. package/dist/commands/scans.d.ts +2 -0
  26. package/dist/commands/scans.js +51 -0
  27. package/dist/core/ai-client.d.ts +6 -1
  28. package/dist/core/ai-client.js +80 -12
  29. package/dist/core/ai-payloads.d.ts +17 -0
  30. package/dist/core/ai-payloads.js +54 -0
  31. package/dist/core/config-schema.d.ts +197 -0
  32. package/dist/core/config-schema.js +68 -0
  33. package/dist/core/config-schema.test.d.ts +1 -0
  34. package/dist/core/config-schema.test.js +151 -0
  35. package/dist/core/config.d.ts +8 -31
  36. package/dist/core/config.js +68 -11
  37. package/dist/core/errors.d.ts +71 -0
  38. package/dist/core/errors.js +162 -0
  39. package/dist/core/scan-index.d.ts +19 -0
  40. package/dist/core/scan-index.js +52 -0
  41. package/dist/core/scan-storage.d.ts +11 -0
  42. package/dist/core/scan-storage.js +69 -0
  43. package/dist/core/scanner.d.ts +95 -13
  44. package/dist/core/scanner.js +336 -248
  45. package/dist/core/vulnerability-detector.d.ts +3 -0
  46. package/dist/core/vulnerability-detector.js +25 -15
  47. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  48. package/dist/core/vulnerability-detector.test.js +210 -0
  49. package/dist/index.js +3 -0
  50. package/dist/plugins/PluginManager.d.ts +27 -0
  51. package/dist/plugins/PluginManager.js +166 -0
  52. package/dist/plugins/index.d.ts +7 -0
  53. package/dist/plugins/index.js +19 -0
  54. package/dist/plugins/types.d.ts +55 -0
  55. package/dist/plugins/types.js +25 -0
  56. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  57. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  58. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  59. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  60. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  61. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  62. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  63. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  64. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  65. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  66. package/dist/reports/PdfGenerator.d.ts +36 -0
  67. package/dist/reports/PdfGenerator.js +379 -0
  68. package/dist/utils/logger.d.ts +33 -1
  69. package/dist/utils/logger.js +127 -8
  70. package/dist/utils/theme.d.ts +55 -0
  71. package/dist/utils/theme.js +195 -0
  72. package/package.json +1 -1
@@ -1,164 +1,211 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
35
5
  Object.defineProperty(exports, "__esModule", { value: true });
36
6
  exports.registerOnboardCommand = registerOnboardCommand;
37
- const readline = __importStar(require("readline"));
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const openai_1 = __importDefault(require("openai"));
38
10
  const config_1 = require("../core/config");
39
11
  const logger_1 = require("../utils/logger");
40
- // ─── ANSI Helpers ──────────────────────────────────────────────────
41
- const c = {
42
- reset: "\x1b[0m",
43
- bold: "\x1b[1m",
44
- dim: "\x1b[2m",
45
- cyan: "\x1b[36m",
46
- green: "\x1b[32m",
47
- yellow: "\x1b[33m",
48
- gray: "\x1b[90m",
49
- white: "\x1b[37m",
50
- brightCyan: "\x1b[96m",
51
- };
52
- // ─── Prompt Utilities ──────────────────────────────────────────────
53
- function ask(rl, question, defaultVal) {
54
- const defaultHint = defaultVal ? ` ${c.gray}(${defaultVal})${c.reset}` : "";
55
- return new Promise((resolve) => {
56
- rl.question(` ${c.cyan}?${c.reset} ${question}${defaultHint} `, (answer) => {
57
- resolve(answer.trim() || defaultVal || "");
58
- });
59
- });
60
- }
61
- function askConfirm(rl, question, defaultVal = true) {
62
- const hint = defaultVal ? `${c.gray}(Y/n)${c.reset}` : `${c.gray}(y/N)${c.reset}`;
63
- return new Promise((resolve) => {
64
- rl.question(` ${c.cyan}?${c.reset} ${question} ${hint} `, (answer) => {
65
- const a = answer.trim().toLowerCase();
66
- if (a === "")
67
- resolve(defaultVal);
68
- else
69
- resolve(a === "y" || a === "yes");
70
- });
71
- });
12
+ const theme_1 = require("../utils/theme");
13
+ function getDefaultModel(provider) {
14
+ switch (provider) {
15
+ case "anthropic":
16
+ return "claude-3-5-sonnet-20241022";
17
+ case "gemini":
18
+ return "gemini-2.0-flash";
19
+ case "openrouter":
20
+ return "anthropic/claude-3.5-sonnet";
21
+ case "mistral":
22
+ return "mistral-large-latest";
23
+ case "kimi":
24
+ return "moonshot-v1-8k";
25
+ case "groq":
26
+ return "llama-3.1-8b-instant";
27
+ default:
28
+ return "gpt-4";
29
+ }
72
30
  }
73
- function askList(rl, question, choices, defaultVal) {
74
- const choicesStr = choices
75
- .map((ch, i) => {
76
- const isDefault = ch === defaultVal;
77
- return ` ${isDefault ? c.brightCyan + "❯" : " "} ${ch}${c.reset}`;
78
- })
79
- .join("\n");
80
- return new Promise((resolve) => {
81
- console.log(` ${c.cyan}?${c.reset} ${question}`);
82
- console.log(choicesStr);
83
- rl.question(` ${c.gray}Enter choice:${c.reset} `, (answer) => {
84
- const trimmed = answer.trim();
85
- if (choices.includes(trimmed)) {
86
- resolve(trimmed);
87
- }
88
- else {
89
- resolve(defaultVal || choices[0]);
90
- }
91
- });
92
- });
31
+ function getEnvApiKey(provider) {
32
+ const envVars = {
33
+ openai: process.env.OPENAI_API_KEY,
34
+ anthropic: process.env.ANTHROPIC_API_KEY,
35
+ gemini: process.env.GEMINI_API_KEY,
36
+ mistral: process.env.MISTRAL_API_KEY,
37
+ openrouter: process.env.OPENROUTER_API_KEY,
38
+ kimi: process.env.KIMI_API_KEY,
39
+ groq: process.env.GROQ_API_KEY,
40
+ };
41
+ return envVars[provider] || "";
93
42
  }
94
- function askPassword(rl, question) {
95
- return new Promise((resolve) => {
96
- rl.question(` ${c.cyan}?${c.reset} ${question} ${c.gray}(hidden)${c.reset} `, (answer) => {
97
- resolve(answer.trim());
43
+ async function modelExists(provider, apiKey, model) {
44
+ if (!apiKey || !model) {
45
+ return true;
46
+ }
47
+ if (provider === "gemini") {
48
+ const resp = await axios_1.default.get("https://generativelanguage.googleapis.com/v1beta/models", { params: { key: apiKey } });
49
+ const models = resp.data?.models || [];
50
+ return models.some((m) => {
51
+ const id = m.name?.startsWith("models/")
52
+ ? m.name.slice("models/".length)
53
+ : m.name;
54
+ return (id === model &&
55
+ (m.supportedGenerationMethods || []).includes("generateContent"));
98
56
  });
99
- });
57
+ }
58
+ if (provider === "openai" ||
59
+ provider === "openrouter" ||
60
+ provider === "kimi" ||
61
+ provider === "groq") {
62
+ const baseURL = provider === "openrouter"
63
+ ? "https://openrouter.ai/api/v1"
64
+ : provider === "kimi"
65
+ ? "https://api.moonshot.cn/v1"
66
+ : provider === "groq"
67
+ ? "https://api.groq.com/openai/v1"
68
+ : undefined;
69
+ const client = new openai_1.default(baseURL ? { apiKey, baseURL } : { apiKey });
70
+ const resp = await client.models.list();
71
+ return (resp.data || []).some((m) => m.id === model);
72
+ }
73
+ return true;
100
74
  }
101
- // ─── Command Registration ─────────────────────────────────────────
102
75
  function registerOnboardCommand(program) {
103
76
  program
104
77
  .command("onboard")
105
78
  .description("First-time setup wizard")
106
79
  .action(async () => {
107
- const store = (0, config_1.getConfigStore)();
108
- const rl = readline.createInterface({
109
- input: process.stdin,
110
- output: process.stdout,
111
- });
80
+ const config = await (0, config_1.getConfig)();
112
81
  console.log("");
113
- console.log(` ${c.bold}${c.brightCyan}━━━ KramScan Setup Wizard ━━━${c.reset}`);
114
- console.log(` ${c.gray}Configure your scanning environment${c.reset}`);
82
+ console.log(theme_1.theme.brand.bold("🚀 KramScan Setup Wizard"));
83
+ console.log(theme_1.theme.gray("─".repeat(50)));
84
+ console.log(theme_1.theme.white(" Configure your scanning environment in a few easy steps."));
115
85
  console.log("");
116
- // AI Configuration
117
- const aiEnabled = await askConfirm(rl, "Enable AI analysis?", false);
118
- store.set("ai.enabled", aiEnabled);
119
- if (aiEnabled) {
120
- const aiProvider = await askList(rl, "Select AI provider", [
121
- "openai",
122
- "anthropic",
123
- "gemini",
124
- "openrouter",
125
- "mistral",
126
- "kimi"
127
- ], "openai");
128
- store.set("ai.provider", aiProvider);
129
- const apiKey = await askPassword(rl, "API key (leave blank to skip)");
130
- if (apiKey) {
131
- store.set("ai.apiKey", apiKey);
86
+ // Smart detection for API keys
87
+ const detectedProviders = Object.keys({
88
+ openai: process.env.OPENAI_API_KEY,
89
+ anthropic: process.env.ANTHROPIC_API_KEY,
90
+ gemini: process.env.GEMINI_API_KEY,
91
+ mistral: process.env.MISTRAL_API_KEY,
92
+ openrouter: process.env.OPENROUTER_API_KEY,
93
+ kimi: process.env.KIMI_API_KEY,
94
+ groq: process.env.GROQ_API_KEY,
95
+ }).filter(p => !!getEnvApiKey(p));
96
+ if (detectedProviders.length > 0) {
97
+ console.log(theme_1.theme.green(` ✨ Detected API keys in environment for: ${detectedProviders.join(", ")}`));
98
+ console.log("");
99
+ }
100
+ const answers = await inquirer_1.default.prompt([
101
+ {
102
+ type: "confirm",
103
+ name: "aiEnabled",
104
+ message: "Enable AI analysis?",
105
+ default: config.ai.enabled,
106
+ },
107
+ {
108
+ type: "list",
109
+ name: "aiProvider",
110
+ message: "Select AI provider",
111
+ choices: [
112
+ "openai",
113
+ "anthropic",
114
+ "gemini",
115
+ "openrouter",
116
+ "mistral",
117
+ "kimi",
118
+ "groq",
119
+ ],
120
+ default: config.ai.provider,
121
+ when: (a) => a.aiEnabled,
122
+ },
123
+ {
124
+ type: "password",
125
+ name: "apiKey",
126
+ message: "API key (leave blank to keep existing)",
127
+ default: "",
128
+ mask: "*",
129
+ when: (a) => a.aiEnabled,
130
+ },
131
+ {
132
+ type: "input",
133
+ name: "model",
134
+ message: "Default AI model",
135
+ default: (a) => config.ai.defaultModel ||
136
+ getDefaultModel((a.aiProvider || config.ai.provider)),
137
+ when: (a) => a.aiEnabled,
138
+ },
139
+ {
140
+ type: "list",
141
+ name: "reportFormat",
142
+ message: "Default report format",
143
+ choices: ["word", "txt", "json"],
144
+ default: config.report.defaultFormat,
145
+ },
146
+ {
147
+ type: "confirm",
148
+ name: "strictScope",
149
+ message: "Enable strict scope enforcement?",
150
+ default: config.scan.strictScope,
151
+ },
152
+ {
153
+ type: "number",
154
+ name: "rateLimit",
155
+ message: "Requests per second rate limit",
156
+ default: config.scan.rateLimitPerSecond,
157
+ validate: (v) => Number.isFinite(v) && v > 0 ? true : "Enter a positive number",
158
+ },
159
+ ]);
160
+ config.ai.enabled = !!answers.aiEnabled;
161
+ if (config.ai.enabled) {
162
+ config.ai.provider = answers.aiProvider;
163
+ if (answers.apiKey) {
164
+ config.ai.apiKey = answers.apiKey;
165
+ }
166
+ const keyForCheck = config.ai.apiKey || getEnvApiKey(config.ai.provider);
167
+ let chosenModel = String(answers.model || getDefaultModel(config.ai.provider));
168
+ if (keyForCheck) {
169
+ try {
170
+ const ok = await modelExists(config.ai.provider, keyForCheck, chosenModel);
171
+ if (!ok) {
172
+ logger_1.logger.warn(`Model '${chosenModel}' is not available for provider '${config.ai.provider}'.`);
173
+ logger_1.logger.warn("Tip: run 'kramscan ai models' to see valid models.");
174
+ const retry = await inquirer_1.default.prompt([
175
+ {
176
+ type: "confirm",
177
+ name: "retry",
178
+ message: "Enter a different model now?",
179
+ default: true,
180
+ },
181
+ ]);
182
+ if (retry.retry) {
183
+ const modelAns = await inquirer_1.default.prompt([
184
+ {
185
+ type: "input",
186
+ name: "model",
187
+ message: "Default AI model",
188
+ default: getDefaultModel(config.ai.provider),
189
+ },
190
+ ]);
191
+ chosenModel = String(modelAns.model || chosenModel);
192
+ }
193
+ }
194
+ }
195
+ catch (error) {
196
+ logger_1.logger.warn(`Model preflight check failed: ${error.message}`);
197
+ }
132
198
  }
133
- // Provider-specific default models
134
- let defaultModel = "gpt-4";
135
- if (aiProvider === "anthropic")
136
- defaultModel = "claude-3-5-sonnet-20241022";
137
- else if (aiProvider === "gemini")
138
- defaultModel = "gemini-2.0-flash-exp";
139
- else if (aiProvider === "openrouter")
140
- defaultModel = "anthropic/claude-3.5-sonnet";
141
- else if (aiProvider === "mistral")
142
- defaultModel = "mistral-large-latest";
143
- else if (aiProvider === "kimi")
144
- defaultModel = "moonshot-v1-8k";
145
- const model = await ask(rl, "Default AI model", defaultModel);
146
- store.set("ai.defaultModel", model);
199
+ config.ai.defaultModel = chosenModel;
147
200
  }
148
- // Report Configuration
149
- const reportFormat = await askList(rl, "Default report format", ["word", "txt", "json"], "word");
150
- store.set("report.defaultFormat", reportFormat);
151
- // Scan Configuration
152
- const strictScope = await askConfirm(rl, "Enable strict scope enforcement?", true);
153
- store.set("scan.strictScope", strictScope);
154
- const rateLimitStr = await ask(rl, "Requests per second rate limit", "5");
155
- const rateLimit = parseInt(rateLimitStr, 10) || 5;
156
- store.set("scan.rateLimitPerSecond", rateLimit);
157
- rl.close();
201
+ config.report.defaultFormat = answers.reportFormat;
202
+ config.scan.strictScope = !!answers.strictScope;
203
+ config.scan.rateLimitPerSecond = answers.rateLimit;
204
+ await (0, config_1.setConfig)(config);
158
205
  console.log("");
159
206
  logger_1.logger.success("Onboarding complete! Your configuration has been saved.");
160
- console.log(` ${c.gray}Config location: ~/.kramscan/config.json${c.reset}`);
161
- console.log(` ${c.gray}Run ${c.cyan}kramscan${c.gray} to get started.${c.reset}`);
207
+ console.log("Config location: ~/.kramscan/config.json");
208
+ console.log("Run 'kramscan' to get started.");
162
209
  console.log("");
163
210
  });
164
211
  }
@@ -5,60 +5,59 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerReportCommand = registerReportCommand;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const logger_1 = require("../utils/logger");
9
- const config_1 = require("../core/config");
8
+ const docx_1 = require("docx");
10
9
  const promises_1 = __importDefault(require("fs/promises"));
11
10
  const path_1 = __importDefault(require("path"));
12
- const os_1 = __importDefault(require("os"));
13
- const docx_1 = require("docx");
11
+ const config_1 = require("../core/config");
12
+ const scan_storage_1 = require("../core/scan-storage");
13
+ const ai_client_1 = require("../core/ai-client");
14
+ const logger_1 = require("../utils/logger");
14
15
  function registerReportCommand(program) {
15
16
  program
16
17
  .command("report [scan-file]")
17
18
  .description("Generate a professional security report")
18
19
  .option("-f, --format <type>", "Report format: word|json|txt")
19
20
  .option("-o, --output <file>", "Output filename")
21
+ .option("--ai-summary", "Generate an AI-powered executive summary")
20
22
  .action(async (scanFile, options) => {
21
23
  console.log("");
22
- console.log(chalk_1.default.bold.cyan("📄 Generating Security Report"));
23
- console.log(chalk_1.default.gray("".repeat(50)));
24
+ console.log(chalk_1.default.bold.cyan("Generating Security Report"));
25
+ console.log(chalk_1.default.gray("-".repeat(50)));
24
26
  console.log("");
27
+ let spinner = null;
25
28
  try {
26
- // Load scan results
27
- let filepath;
28
- if (scanFile) {
29
- filepath = path_1.default.isAbsolute(scanFile)
30
- ? scanFile
31
- : path_1.default.join(process.cwd(), scanFile);
32
- }
33
- else {
34
- // Find latest scan
35
- const scanDir = path_1.default.join(os_1.default.homedir(), ".kramscan", "scans");
36
- const files = await promises_1.default.readdir(scanDir);
37
- const scanFiles = files.filter((f) => f.endsWith(".json"));
38
- if (scanFiles.length === 0) {
39
- logger_1.logger.error("No scan results found. Run 'kramscan scan <url>' first.");
40
- process.exit(1);
41
- }
42
- scanFiles.sort().reverse();
43
- filepath = path_1.default.join(scanDir, scanFiles[0]);
44
- logger_1.logger.info(`Using latest scan: ${scanFiles[0]}`);
29
+ const resolved = await (0, scan_storage_1.resolveScanFile)(scanFile);
30
+ const filepath = resolved.filepath;
31
+ if (resolved.isLatest) {
32
+ logger_1.logger.info(`Using latest scan: ${resolved.filename}`);
45
33
  }
46
34
  const content = await promises_1.default.readFile(filepath, "utf-8");
47
35
  const scanResult = JSON.parse(content);
48
- // Determine format
49
36
  const config = await (0, config_1.getConfig)();
50
- const format = options.format || config.report.defaultFormat;
51
- const spinner = logger_1.logger.spinner(`Generating ${format.toUpperCase()} report...`);
37
+ const format = (options.format || config.report.defaultFormat);
38
+ let aiSummary;
39
+ if (options.aiSummary) {
40
+ spinner = logger_1.logger.spinner("Generating AI executive summary...");
41
+ try {
42
+ const aiClient = await (0, ai_client_1.createAIClient)();
43
+ aiSummary = await (0, ai_client_1.generateExecutiveSummary)(aiClient, scanResult);
44
+ spinner.succeed("AI summary generated!");
45
+ }
46
+ catch (err) {
47
+ spinner.warn(`AI summary failed: ${err.message}`);
48
+ }
49
+ }
50
+ spinner = logger_1.logger.spinner(`Generating ${format.toUpperCase()} report...`);
52
51
  let outputPath;
53
52
  switch (format) {
54
53
  case "word":
55
- outputPath = await generateWordReport(scanResult, options.output);
54
+ outputPath = await generateWordReport(scanResult, options.output, aiSummary);
56
55
  break;
57
56
  case "json":
58
- outputPath = await generateJsonReport(scanResult, options.output);
57
+ outputPath = await generateJsonReport(scanResult, options.output, aiSummary);
59
58
  break;
60
59
  case "txt":
61
- outputPath = await generateTxtReport(scanResult, options.output);
60
+ outputPath = await generateTxtReport(scanResult, options.output, aiSummary);
62
61
  break;
63
62
  default:
64
63
  throw new Error(`Unsupported format: ${format}`);
@@ -69,18 +68,20 @@ function registerReportCommand(program) {
69
68
  console.log("");
70
69
  }
71
70
  catch (error) {
71
+ if (spinner) {
72
+ spinner.fail("Report generation failed");
73
+ }
72
74
  logger_1.logger.error(error.message);
73
75
  process.exit(1);
74
76
  }
75
77
  });
76
78
  }
77
- async function generateWordReport(scanResult, outputFile) {
79
+ async function generateWordReport(scanResult, outputFile, aiSummary) {
78
80
  const doc = new docx_1.Document({
79
81
  sections: [
80
82
  {
81
83
  properties: {},
82
84
  children: [
83
- // Title
84
85
  new docx_1.Paragraph({
85
86
  text: "Security Assessment Report",
86
87
  heading: docx_1.HeadingLevel.HEADING_1,
@@ -95,7 +96,6 @@ async function generateWordReport(scanResult, outputFile) {
95
96
  alignment: docx_1.AlignmentType.CENTER,
96
97
  }),
97
98
  new docx_1.Paragraph({ text: "" }),
98
- // Executive Summary
99
99
  new docx_1.Paragraph({
100
100
  text: "Executive Summary",
101
101
  heading: docx_1.HeadingLevel.HEADING_2,
@@ -103,45 +103,37 @@ async function generateWordReport(scanResult, outputFile) {
103
103
  new docx_1.Paragraph({
104
104
  children: [
105
105
  new docx_1.TextRun({
106
- text: `This report contains the results of an automated security assessment performed on ${scanResult.target}. `,
107
- }),
108
- new docx_1.TextRun({
109
- text: `A total of ${scanResult.summary.total} vulnerabilities were identified, `,
110
- }),
111
- new docx_1.TextRun({
112
- text: `including ${scanResult.summary.critical} critical and ${scanResult.summary.high} high severity issues.`,
106
+ text: aiSummary || `This report contains the results of an automated security assessment performed on ${scanResult.target}. ` +
107
+ `A total of ${scanResult.summary.total} vulnerabilities were identified, ` +
108
+ `including ${scanResult.summary.critical} critical and ${scanResult.summary.high} high severity issues.`,
113
109
  }),
114
110
  ],
115
111
  }),
116
112
  new docx_1.Paragraph({ text: "" }),
117
- // Scan Statistics
118
113
  new docx_1.Paragraph({
119
114
  text: "Scan Statistics",
120
115
  heading: docx_1.HeadingLevel.HEADING_2,
121
116
  }),
122
- new docx_1.Paragraph({ text: `• URLs Crawled: ${scanResult.metadata.crawledUrls}` }),
123
- new docx_1.Paragraph({ text: `• Forms Tested: ${scanResult.metadata.testedForms}` }),
124
- new docx_1.Paragraph({ text: `• Requests Made: ${scanResult.metadata.requestsMade}` }),
117
+ new docx_1.Paragraph({ text: `- URLs Crawled: ${scanResult.metadata.crawledUrls}` }),
118
+ new docx_1.Paragraph({ text: `- Forms Tested: ${scanResult.metadata.testedForms}` }),
119
+ new docx_1.Paragraph({ text: `- Requests Made: ${scanResult.metadata.requestsMade}` }),
125
120
  new docx_1.Paragraph({
126
- text: `• Duration: ${(scanResult.duration / 1000).toFixed(2)} seconds`,
121
+ text: `- Duration: ${(scanResult.duration / 1000).toFixed(2)} seconds`,
127
122
  }),
128
123
  new docx_1.Paragraph({ text: "" }),
129
- // Findings
130
124
  new docx_1.Paragraph({
131
125
  text: "Detailed Findings",
132
126
  heading: docx_1.HeadingLevel.HEADING_2,
133
127
  }),
134
- ...scanResult.vulnerabilities.flatMap((vuln, i) => [
128
+ ...scanResult.vulnerabilities.flatMap((vuln, index) => [
135
129
  new docx_1.Paragraph({
136
- text: `${i + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`,
130
+ text: `${index + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`,
137
131
  heading: docx_1.HeadingLevel.HEADING_3,
138
132
  }),
139
133
  new docx_1.Paragraph({ text: `URL: ${vuln.url}` }),
140
134
  new docx_1.Paragraph({ text: `Type: ${vuln.type}` }),
141
135
  new docx_1.Paragraph({ text: `Description: ${vuln.description}` }),
142
- ...(vuln.evidence
143
- ? [new docx_1.Paragraph({ text: `Evidence: ${vuln.evidence}` })]
144
- : []),
136
+ ...(vuln.evidence ? [new docx_1.Paragraph({ text: `Evidence: ${vuln.evidence}` })] : []),
145
137
  ...(vuln.remediation
146
138
  ? [new docx_1.Paragraph({ text: `Remediation: ${vuln.remediation}` })]
147
139
  : []),
@@ -153,28 +145,23 @@ async function generateWordReport(scanResult, outputFile) {
153
145
  ],
154
146
  });
155
147
  const buffer = await docx_1.Packer.toBuffer(doc);
156
- const reportsDir = path_1.default.join(os_1.default.homedir(), ".kramscan", "reports");
157
- await promises_1.default.mkdir(reportsDir, { recursive: true });
148
+ const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
158
149
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
159
150
  const filename = outputFile || `report-${timestamp}.docx`;
160
- const filepath = path_1.default.isAbsolute(filename)
161
- ? filename
162
- : path_1.default.join(reportsDir, filename);
151
+ const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
163
152
  await promises_1.default.writeFile(filepath, buffer);
164
153
  return filepath;
165
154
  }
166
- async function generateJsonReport(scanResult, outputFile) {
167
- const reportsDir = path_1.default.join(os_1.default.homedir(), ".kramscan", "reports");
168
- await promises_1.default.mkdir(reportsDir, { recursive: true });
155
+ async function generateJsonReport(scanResult, outputFile, aiSummary) {
156
+ const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
169
157
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
170
158
  const filename = outputFile || `report-${timestamp}.json`;
171
- const filepath = path_1.default.isAbsolute(filename)
172
- ? filename
173
- : path_1.default.join(reportsDir, filename);
174
- await promises_1.default.writeFile(filepath, JSON.stringify(scanResult, null, 2));
159
+ const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
160
+ const finalResult = aiSummary ? { ...scanResult, aiSummary } : scanResult;
161
+ await promises_1.default.writeFile(filepath, JSON.stringify(finalResult, null, 2));
175
162
  return filepath;
176
163
  }
177
- async function generateTxtReport(scanResult, outputFile) {
164
+ async function generateTxtReport(scanResult, outputFile, aiSummary) {
178
165
  const lines = [];
179
166
  lines.push("=".repeat(60));
180
167
  lines.push("SECURITY ASSESSMENT REPORT");
@@ -186,7 +173,12 @@ async function generateTxtReport(scanResult, outputFile) {
186
173
  lines.push("");
187
174
  lines.push("EXECUTIVE SUMMARY");
188
175
  lines.push("-".repeat(60));
189
- 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)`);
176
+ if (aiSummary) {
177
+ lines.push(aiSummary);
178
+ }
179
+ else {
180
+ 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)`);
181
+ }
190
182
  lines.push("");
191
183
  lines.push("SCAN STATISTICS");
192
184
  lines.push("-".repeat(60));
@@ -197,29 +189,29 @@ async function generateTxtReport(scanResult, outputFile) {
197
189
  lines.push("DETAILED FINDINGS");
198
190
  lines.push("-".repeat(60));
199
191
  lines.push("");
200
- scanResult.vulnerabilities.forEach((vuln, i) => {
201
- lines.push(`${i + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`);
192
+ scanResult.vulnerabilities.forEach((vuln, index) => {
193
+ lines.push(`${index + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`);
202
194
  lines.push(` URL: ${vuln.url}`);
203
195
  lines.push(` Type: ${vuln.type}`);
204
196
  lines.push(` Description: ${vuln.description}`);
205
- if (vuln.evidence)
197
+ if (vuln.evidence) {
206
198
  lines.push(` Evidence: ${vuln.evidence}`);
207
- if (vuln.remediation)
199
+ }
200
+ if (vuln.remediation) {
208
201
  lines.push(` Remediation: ${vuln.remediation}`);
209
- if (vuln.cwe)
202
+ }
203
+ if (vuln.cwe) {
210
204
  lines.push(` CWE: ${vuln.cwe}`);
205
+ }
211
206
  lines.push("");
212
207
  });
213
208
  lines.push("=".repeat(60));
214
209
  lines.push("End of Report");
215
210
  lines.push("=".repeat(60));
216
- const reportsDir = path_1.default.join(os_1.default.homedir(), ".kramscan", "reports");
217
- await promises_1.default.mkdir(reportsDir, { recursive: true });
211
+ const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
218
212
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
219
213
  const filename = outputFile || `report-${timestamp}.txt`;
220
- const filepath = path_1.default.isAbsolute(filename)
221
- ? filename
222
- : path_1.default.join(reportsDir, filename);
214
+ const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
223
215
  await promises_1.default.writeFile(filepath, lines.join("\n"));
224
216
  return filepath;
225
217
  }