kramscan 0.1.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.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/kramscan.js +4 -0
- package/bin/openscan.js +4 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +225 -0
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.js +115 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +139 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +234 -0
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.js +146 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +225 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +125 -0
- package/dist/core/ai-client.d.ts +12 -0
- package/dist/core/ai-client.js +89 -0
- package/dist/core/config.d.ts +45 -0
- package/dist/core/config.js +146 -0
- package/dist/core/executor.d.ts +2 -0
- package/dist/core/executor.js +74 -0
- package/dist/core/logger.d.ts +12 -0
- package/dist/core/logger.js +51 -0
- package/dist/core/registry.d.ts +3 -0
- package/dist/core/registry.js +35 -0
- package/dist/core/scanner.d.ts +24 -0
- package/dist/core/scanner.js +197 -0
- package/dist/core/storage.d.ts +4 -0
- package/dist/core/storage.js +39 -0
- package/dist/core/types.d.ts +24 -0
- package/dist/core/types.js +2 -0
- package/dist/core/vulnerability-detector.d.ts +47 -0
- package/dist/core/vulnerability-detector.js +150 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/skills/base.d.ts +8 -0
- package/dist/skills/base.js +6 -0
- package/dist/skills/builtin.d.ts +4 -0
- package/dist/skills/builtin.js +71 -0
- package/dist/skills/loader.d.ts +2 -0
- package/dist/skills/loader.js +27 -0
- package/dist/skills/types.d.ts +46 -0
- package/dist/skills/types.js +2 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +34 -0
- package/package.json +62 -0
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.registerDoctorCommand = registerDoctorCommand;
|
|
40
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
41
|
+
const config_1 = require("../core/config");
|
|
42
|
+
const logger_1 = require("../utils/logger");
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const util_1 = require("util");
|
|
45
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
46
|
+
const os_1 = __importDefault(require("os"));
|
|
47
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
48
|
+
function registerDoctorCommand(program) {
|
|
49
|
+
program
|
|
50
|
+
.command("doctor")
|
|
51
|
+
.description("Check environment health and configuration")
|
|
52
|
+
.action(async () => {
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(chalk_1.default.bold.cyan("🩺 KramScan Health Check"));
|
|
55
|
+
console.log(chalk_1.default.gray("─".repeat(50)));
|
|
56
|
+
console.log("");
|
|
57
|
+
const checks = [];
|
|
58
|
+
// Check Node.js version
|
|
59
|
+
checks.push(await checkNodeVersion());
|
|
60
|
+
// Check Puppeteer/Chrome
|
|
61
|
+
checks.push(await checkPuppeteer());
|
|
62
|
+
// Check config file
|
|
63
|
+
checks.push(await checkConfig());
|
|
64
|
+
// Check API keys
|
|
65
|
+
checks.push(await checkAPIKeys());
|
|
66
|
+
// Check disk space
|
|
67
|
+
checks.push(await checkDiskSpace());
|
|
68
|
+
// Check network connectivity
|
|
69
|
+
checks.push(await checkNetwork());
|
|
70
|
+
// Display results
|
|
71
|
+
let passCount = 0;
|
|
72
|
+
let failCount = 0;
|
|
73
|
+
let warnCount = 0;
|
|
74
|
+
for (const check of checks) {
|
|
75
|
+
const icon = check.status === "pass"
|
|
76
|
+
? chalk_1.default.green("✓")
|
|
77
|
+
: check.status === "fail"
|
|
78
|
+
? chalk_1.default.red("✗")
|
|
79
|
+
: chalk_1.default.yellow("⚠");
|
|
80
|
+
console.log(`${icon} ${chalk_1.default.bold(check.name)}`);
|
|
81
|
+
console.log(` ${chalk_1.default.gray(check.message)}`);
|
|
82
|
+
console.log("");
|
|
83
|
+
if (check.status === "pass")
|
|
84
|
+
passCount++;
|
|
85
|
+
else if (check.status === "fail")
|
|
86
|
+
failCount++;
|
|
87
|
+
else
|
|
88
|
+
warnCount++;
|
|
89
|
+
}
|
|
90
|
+
// Summary
|
|
91
|
+
console.log(chalk_1.default.gray("─".repeat(50)));
|
|
92
|
+
console.log(`${chalk_1.default.green(`${passCount} passed`)} | ${chalk_1.default.yellow(`${warnCount} warnings`)} | ${chalk_1.default.red(`${failCount} failed`)}`);
|
|
93
|
+
console.log("");
|
|
94
|
+
if (failCount > 0) {
|
|
95
|
+
logger_1.logger.error("Some checks failed. Please fix the issues above.");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
else if (warnCount > 0) {
|
|
99
|
+
logger_1.logger.warn("Some checks have warnings. KramScan should work but may have limitations.");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
logger_1.logger.success("All checks passed! KramScan is ready to use.");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function checkNodeVersion() {
|
|
107
|
+
const version = process.version;
|
|
108
|
+
const major = parseInt(version.slice(1).split(".")[0]);
|
|
109
|
+
if (major >= 18) {
|
|
110
|
+
return {
|
|
111
|
+
name: "Node.js Version",
|
|
112
|
+
status: "pass",
|
|
113
|
+
message: `${version} (>= 18 required)`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
return {
|
|
118
|
+
name: "Node.js Version",
|
|
119
|
+
status: "fail",
|
|
120
|
+
message: `${version} - Please upgrade to Node.js 18 or higher`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function checkPuppeteer() {
|
|
125
|
+
try {
|
|
126
|
+
const puppeteer = await Promise.resolve().then(() => __importStar(require("puppeteer")));
|
|
127
|
+
return {
|
|
128
|
+
name: "Puppeteer",
|
|
129
|
+
status: "pass",
|
|
130
|
+
message: "Installed and ready",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
name: "Puppeteer",
|
|
136
|
+
status: "fail",
|
|
137
|
+
message: "Not installed. Run: npm install puppeteer",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function checkConfig() {
|
|
142
|
+
try {
|
|
143
|
+
const config = (0, config_1.getConfig)();
|
|
144
|
+
return {
|
|
145
|
+
name: "Configuration",
|
|
146
|
+
status: "pass",
|
|
147
|
+
message: `Config loaded from ~/.kramscan/config.json`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
name: "Configuration",
|
|
153
|
+
status: "warn",
|
|
154
|
+
message: "Config file not found. Run 'kramscan onboard' to create it.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function checkAPIKeys() {
|
|
159
|
+
try {
|
|
160
|
+
const config = (0, config_1.getConfig)();
|
|
161
|
+
if (!config.ai.enabled) {
|
|
162
|
+
return {
|
|
163
|
+
name: "AI Configuration",
|
|
164
|
+
status: "warn",
|
|
165
|
+
message: "AI analysis is disabled. Run 'kramscan onboard' to enable it.",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (!config.ai.apiKey) {
|
|
169
|
+
return {
|
|
170
|
+
name: "AI Configuration",
|
|
171
|
+
status: "warn",
|
|
172
|
+
message: `${config.ai.provider} API key not configured. Run 'kramscan onboard' to set it.`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
name: "AI Configuration",
|
|
177
|
+
status: "pass",
|
|
178
|
+
message: `${config.ai.provider} configured with model ${config.ai.defaultModel}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
name: "AI Configuration",
|
|
184
|
+
status: "warn",
|
|
185
|
+
message: "Unable to check AI configuration",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function checkDiskSpace() {
|
|
190
|
+
try {
|
|
191
|
+
const homeDir = os_1.default.homedir();
|
|
192
|
+
const kramScanDir = `${homeDir}/.kramscan`;
|
|
193
|
+
// Create directory if it doesn't exist
|
|
194
|
+
await promises_1.default.mkdir(kramScanDir, { recursive: true });
|
|
195
|
+
return {
|
|
196
|
+
name: "Disk Space",
|
|
197
|
+
status: "pass",
|
|
198
|
+
message: `Scan directory: ${kramScanDir}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
name: "Disk Space",
|
|
204
|
+
status: "fail",
|
|
205
|
+
message: "Unable to access scan directory",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function checkNetwork() {
|
|
210
|
+
try {
|
|
211
|
+
// Simple DNS check
|
|
212
|
+
const { exec } = require("child_process");
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
exec("ping -n 1 8.8.8.8", (error) => {
|
|
215
|
+
if (error)
|
|
216
|
+
reject(error);
|
|
217
|
+
else
|
|
218
|
+
resolve(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
name: "Network Connectivity",
|
|
223
|
+
status: "pass",
|
|
224
|
+
message: "Internet connection available",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
name: "Network Connectivity",
|
|
230
|
+
status: "warn",
|
|
231
|
+
message: "Unable to verify internet connection",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerOnboardCommand = registerOnboardCommand;
|
|
37
|
+
const readline = __importStar(require("readline"));
|
|
38
|
+
const config_1 = require("../core/config");
|
|
39
|
+
const logger_1 = require("../core/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
|
+
});
|
|
72
|
+
}
|
|
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
|
+
});
|
|
93
|
+
}
|
|
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());
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// ─── Command Registration ─────────────────────────────────────────
|
|
102
|
+
function registerOnboardCommand(program) {
|
|
103
|
+
program
|
|
104
|
+
.command("onboard")
|
|
105
|
+
.description("First-time setup wizard")
|
|
106
|
+
.action(async () => {
|
|
107
|
+
const logger = (0, logger_1.createLogger)();
|
|
108
|
+
const store = (0, config_1.getConfigStore)();
|
|
109
|
+
const rl = readline.createInterface({
|
|
110
|
+
input: process.stdin,
|
|
111
|
+
output: process.stdout,
|
|
112
|
+
});
|
|
113
|
+
console.log("");
|
|
114
|
+
console.log(` ${c.bold}${c.brightCyan}━━━ KramScan Setup Wizard ━━━${c.reset}`);
|
|
115
|
+
console.log(` ${c.gray}Configure your scanning environment${c.reset}`);
|
|
116
|
+
console.log("");
|
|
117
|
+
// AI Configuration
|
|
118
|
+
const aiEnabled = await askConfirm(rl, "Enable AI analysis?", false);
|
|
119
|
+
store.set("ai.enabled", aiEnabled);
|
|
120
|
+
if (aiEnabled) {
|
|
121
|
+
const aiProvider = await askList(rl, "Select AI provider", ["openai", "anthropic"], "openai");
|
|
122
|
+
store.set("ai.provider", aiProvider);
|
|
123
|
+
const apiKey = await askPassword(rl, "API key (leave blank to skip)");
|
|
124
|
+
if (apiKey) {
|
|
125
|
+
store.set("ai.apiKey", apiKey);
|
|
126
|
+
}
|
|
127
|
+
const model = await ask(rl, "Default AI model", aiProvider === "openai" ? "gpt-4" : "claude-3-opus-20240229");
|
|
128
|
+
store.set("ai.model", model);
|
|
129
|
+
}
|
|
130
|
+
// Report Configuration
|
|
131
|
+
const reportFormat = await askList(rl, "Default report format", ["word", "txt", "json"], "word");
|
|
132
|
+
store.set("report.defaultFormat", reportFormat);
|
|
133
|
+
// Scan Configuration
|
|
134
|
+
const strictScope = await askConfirm(rl, "Enable strict scope enforcement?", true);
|
|
135
|
+
store.set("scan.strictScope", strictScope);
|
|
136
|
+
const rateLimitStr = await ask(rl, "Requests per second rate limit", "5");
|
|
137
|
+
const rateLimit = parseInt(rateLimitStr, 10) || 5;
|
|
138
|
+
store.set("scan.rateLimitPerSecond", rateLimit);
|
|
139
|
+
rl.close();
|
|
140
|
+
console.log("");
|
|
141
|
+
logger.success("Onboarding complete! Your configuration has been saved.");
|
|
142
|
+
console.log(` ${c.gray}Config location: ~/.kramscan/config.json${c.reset}`);
|
|
143
|
+
console.log(` ${c.gray}Run ${c.cyan}kramscan${c.gray} to get started.${c.reset}`);
|
|
144
|
+
console.log("");
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
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.registerReportCommand = registerReportCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
const config_1 = require("../core/config");
|
|
10
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const docx_1 = require("docx");
|
|
14
|
+
function registerReportCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command("report [scan-file]")
|
|
17
|
+
.description("Generate a professional security report")
|
|
18
|
+
.option("-f, --format <type>", "Report format: word|json|txt")
|
|
19
|
+
.option("-o, --output <file>", "Output filename")
|
|
20
|
+
.action(async (scanFile, options) => {
|
|
21
|
+
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("");
|
|
25
|
+
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]}`);
|
|
45
|
+
}
|
|
46
|
+
const content = await promises_1.default.readFile(filepath, "utf-8");
|
|
47
|
+
const scanResult = JSON.parse(content);
|
|
48
|
+
// Determine format
|
|
49
|
+
const config = (0, config_1.getConfig)();
|
|
50
|
+
const format = options.format || config.report.defaultFormat;
|
|
51
|
+
const spinner = logger_1.logger.spinner(`Generating ${format.toUpperCase()} report...`);
|
|
52
|
+
let outputPath;
|
|
53
|
+
switch (format) {
|
|
54
|
+
case "word":
|
|
55
|
+
outputPath = await generateWordReport(scanResult, options.output);
|
|
56
|
+
break;
|
|
57
|
+
case "json":
|
|
58
|
+
outputPath = await generateJsonReport(scanResult, options.output);
|
|
59
|
+
break;
|
|
60
|
+
case "txt":
|
|
61
|
+
outputPath = await generateTxtReport(scanResult, options.output);
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
65
|
+
}
|
|
66
|
+
spinner.succeed("Report generated!");
|
|
67
|
+
console.log("");
|
|
68
|
+
logger_1.logger.success(`Report saved to: ${outputPath}`);
|
|
69
|
+
console.log("");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
logger_1.logger.error(error.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function generateWordReport(scanResult, outputFile) {
|
|
78
|
+
const doc = new docx_1.Document({
|
|
79
|
+
sections: [
|
|
80
|
+
{
|
|
81
|
+
properties: {},
|
|
82
|
+
children: [
|
|
83
|
+
// Title
|
|
84
|
+
new docx_1.Paragraph({
|
|
85
|
+
text: "Security Assessment Report",
|
|
86
|
+
heading: docx_1.HeadingLevel.HEADING_1,
|
|
87
|
+
alignment: docx_1.AlignmentType.CENTER,
|
|
88
|
+
}),
|
|
89
|
+
new docx_1.Paragraph({
|
|
90
|
+
text: `Target: ${scanResult.target}`,
|
|
91
|
+
alignment: docx_1.AlignmentType.CENTER,
|
|
92
|
+
}),
|
|
93
|
+
new docx_1.Paragraph({
|
|
94
|
+
text: `Date: ${new Date(scanResult.timestamp).toLocaleString()}`,
|
|
95
|
+
alignment: docx_1.AlignmentType.CENTER,
|
|
96
|
+
}),
|
|
97
|
+
new docx_1.Paragraph({ text: "" }),
|
|
98
|
+
// Executive Summary
|
|
99
|
+
new docx_1.Paragraph({
|
|
100
|
+
text: "Executive Summary",
|
|
101
|
+
heading: docx_1.HeadingLevel.HEADING_2,
|
|
102
|
+
}),
|
|
103
|
+
new docx_1.Paragraph({
|
|
104
|
+
children: [
|
|
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.`,
|
|
113
|
+
}),
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
new docx_1.Paragraph({ text: "" }),
|
|
117
|
+
// Scan Statistics
|
|
118
|
+
new docx_1.Paragraph({
|
|
119
|
+
text: "Scan Statistics",
|
|
120
|
+
heading: docx_1.HeadingLevel.HEADING_2,
|
|
121
|
+
}),
|
|
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}` }),
|
|
125
|
+
new docx_1.Paragraph({
|
|
126
|
+
text: `• Duration: ${(scanResult.duration / 1000).toFixed(2)} seconds`,
|
|
127
|
+
}),
|
|
128
|
+
new docx_1.Paragraph({ text: "" }),
|
|
129
|
+
// Findings
|
|
130
|
+
new docx_1.Paragraph({
|
|
131
|
+
text: "Detailed Findings",
|
|
132
|
+
heading: docx_1.HeadingLevel.HEADING_2,
|
|
133
|
+
}),
|
|
134
|
+
...scanResult.vulnerabilities.flatMap((vuln, i) => [
|
|
135
|
+
new docx_1.Paragraph({
|
|
136
|
+
text: `${i + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`,
|
|
137
|
+
heading: docx_1.HeadingLevel.HEADING_3,
|
|
138
|
+
}),
|
|
139
|
+
new docx_1.Paragraph({ text: `URL: ${vuln.url}` }),
|
|
140
|
+
new docx_1.Paragraph({ text: `Type: ${vuln.type}` }),
|
|
141
|
+
new docx_1.Paragraph({ text: `Description: ${vuln.description}` }),
|
|
142
|
+
...(vuln.evidence
|
|
143
|
+
? [new docx_1.Paragraph({ text: `Evidence: ${vuln.evidence}` })]
|
|
144
|
+
: []),
|
|
145
|
+
...(vuln.remediation
|
|
146
|
+
? [new docx_1.Paragraph({ text: `Remediation: ${vuln.remediation}` })]
|
|
147
|
+
: []),
|
|
148
|
+
...(vuln.cwe ? [new docx_1.Paragraph({ text: `CWE: ${vuln.cwe}` })] : []),
|
|
149
|
+
new docx_1.Paragraph({ text: "" }),
|
|
150
|
+
]),
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
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 });
|
|
158
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
159
|
+
const filename = outputFile || `report-${timestamp}.docx`;
|
|
160
|
+
const filepath = path_1.default.isAbsolute(filename)
|
|
161
|
+
? filename
|
|
162
|
+
: path_1.default.join(reportsDir, filename);
|
|
163
|
+
await promises_1.default.writeFile(filepath, buffer);
|
|
164
|
+
return filepath;
|
|
165
|
+
}
|
|
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 });
|
|
169
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
170
|
+
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));
|
|
175
|
+
return filepath;
|
|
176
|
+
}
|
|
177
|
+
async function generateTxtReport(scanResult, outputFile) {
|
|
178
|
+
const lines = [];
|
|
179
|
+
lines.push("=".repeat(60));
|
|
180
|
+
lines.push("SECURITY ASSESSMENT REPORT");
|
|
181
|
+
lines.push("=".repeat(60));
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push(`Target: ${scanResult.target}`);
|
|
184
|
+
lines.push(`Date: ${new Date(scanResult.timestamp).toLocaleString()}`);
|
|
185
|
+
lines.push(`Duration: ${(scanResult.duration / 1000).toFixed(2)}s`);
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("EXECUTIVE SUMMARY");
|
|
188
|
+
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)`);
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push("SCAN STATISTICS");
|
|
192
|
+
lines.push("-".repeat(60));
|
|
193
|
+
lines.push(`URLs Crawled: ${scanResult.metadata.crawledUrls}`);
|
|
194
|
+
lines.push(`Forms Tested: ${scanResult.metadata.testedForms}`);
|
|
195
|
+
lines.push(`Requests Made: ${scanResult.metadata.requestsMade}`);
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("DETAILED FINDINGS");
|
|
198
|
+
lines.push("-".repeat(60));
|
|
199
|
+
lines.push("");
|
|
200
|
+
scanResult.vulnerabilities.forEach((vuln, i) => {
|
|
201
|
+
lines.push(`${i + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`);
|
|
202
|
+
lines.push(` URL: ${vuln.url}`);
|
|
203
|
+
lines.push(` Type: ${vuln.type}`);
|
|
204
|
+
lines.push(` Description: ${vuln.description}`);
|
|
205
|
+
if (vuln.evidence)
|
|
206
|
+
lines.push(` Evidence: ${vuln.evidence}`);
|
|
207
|
+
if (vuln.remediation)
|
|
208
|
+
lines.push(` Remediation: ${vuln.remediation}`);
|
|
209
|
+
if (vuln.cwe)
|
|
210
|
+
lines.push(` CWE: ${vuln.cwe}`);
|
|
211
|
+
lines.push("");
|
|
212
|
+
});
|
|
213
|
+
lines.push("=".repeat(60));
|
|
214
|
+
lines.push("End of Report");
|
|
215
|
+
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 });
|
|
218
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
219
|
+
const filename = outputFile || `report-${timestamp}.txt`;
|
|
220
|
+
const filepath = path_1.default.isAbsolute(filename)
|
|
221
|
+
? filename
|
|
222
|
+
: path_1.default.join(reportsDir, filename);
|
|
223
|
+
await promises_1.default.writeFile(filepath, lines.join("\n"));
|
|
224
|
+
return filepath;
|
|
225
|
+
}
|