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.
- package/README.md +215 -293
- package/dist/agent/skills/generate-report.js +1 -1
- package/dist/agent/skills/health-check.d.ts +2 -2
- package/dist/agent/skills/health-check.js +2 -2
- package/dist/agent/skills/verify-finding.js +1 -1
- package/dist/agent/skills/web-scan.d.ts +1 -1
- package/dist/agent/skills/web-scan.js +1 -1
- package/dist/cli.js +7 -2
- package/dist/commands/config.js +2 -2
- package/dist/commands/dev.js +4 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/gate.js +3 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +191 -0
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/report.js +89 -11
- package/dist/commands/scan.js +10 -5
- package/dist/core/config.js +8 -1
- package/dist/core/project-config.d.ts +59 -0
- package/dist/core/project-config.js +104 -0
- package/dist/core/server-probe.js +1 -3
- package/dist/index.js +14 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +0 -1
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +1 -1
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +1 -1
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +1 -1
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +1 -1
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +1 -1
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +1 -1
- package/dist/reports/PdfGenerator.js +1 -0
- package/dist/utils/logger.js +3 -3
- package/package.json +3 -1
|
@@ -64,11 +64,11 @@ class HealthCheckSkill {
|
|
|
64
64
|
},
|
|
65
65
|
],
|
|
66
66
|
};
|
|
67
|
-
validateParameters(
|
|
67
|
+
validateParameters(_params) {
|
|
68
68
|
// No required parameters
|
|
69
69
|
return { valid: true, errors: [] };
|
|
70
70
|
}
|
|
71
|
-
async execute(params,
|
|
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
|
-
|
|
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>,
|
|
20
|
+
execute(params: Record<string, unknown>, _context: AgentContext): Promise<SkillResult>;
|
|
21
21
|
run(): Promise<SkillResult>;
|
|
22
22
|
}
|
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
|
-
|
|
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
|
-
|
|
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")
|
package/dist/commands/config.js
CHANGED
|
@@ -107,8 +107,8 @@ function registerConfigCommand(program) {
|
|
|
107
107
|
type: "list",
|
|
108
108
|
name: "reportFormat",
|
|
109
109
|
message: "Default report format:",
|
|
110
|
-
choices: ["word", "json", "txt"],
|
|
111
|
-
default: config.report.defaultFormat,
|
|
110
|
+
choices: ["markdown", "word", "json", "txt"],
|
|
111
|
+
default: config.report.defaultFormat || "markdown",
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
type: "number",
|
package/dist/commands/dev.js
CHANGED
|
@@ -25,7 +25,7 @@ function registerDevCommand(program) {
|
|
|
25
25
|
.option("--no-watch", "Run a single scan without watching (useful for CI)")
|
|
26
26
|
.action(async (url, options) => {
|
|
27
27
|
// Resolve target URL
|
|
28
|
-
|
|
28
|
+
let targetUrl = url || (options.port ? `http://localhost:${options.port}` : null);
|
|
29
29
|
if (!targetUrl) {
|
|
30
30
|
console.log("");
|
|
31
31
|
console.log(theme_1.theme.error("✗ No target URL specified."));
|
|
@@ -34,6 +34,9 @@ function registerDevCommand(program) {
|
|
|
34
34
|
console.log("");
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
|
+
if (!/^https?:\/\//i.test(targetUrl)) {
|
|
38
|
+
targetUrl = `http://${targetUrl}`;
|
|
39
|
+
}
|
|
37
40
|
const isLocal = (0, server_probe_1.isLocalhost)(targetUrl);
|
|
38
41
|
console.log("");
|
|
39
42
|
console.log(theme_1.theme.brand.bold("🛠️ KramScan Dev Mode"));
|
package/dist/commands/doctor.js
CHANGED
package/dist/commands/gate.js
CHANGED
|
@@ -15,6 +15,9 @@ function registerGateCommand(program) {
|
|
|
15
15
|
.option("--timeout <ms>", "Maximum scan duration", "60000")
|
|
16
16
|
.option("--json", "Output results as JSON")
|
|
17
17
|
.action(async (url, options) => {
|
|
18
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
19
|
+
url = `http://${url}`;
|
|
20
|
+
}
|
|
18
21
|
const jsonMode = options.json === true;
|
|
19
22
|
if (!jsonMode) {
|
|
20
23
|
console.log("");
|
|
@@ -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
|
+
}
|
package/dist/commands/onboard.js
CHANGED
|
@@ -140,8 +140,8 @@ function registerOnboardCommand(program) {
|
|
|
140
140
|
type: "list",
|
|
141
141
|
name: "reportFormat",
|
|
142
142
|
message: "Default report format",
|
|
143
|
-
choices: ["word", "txt", "json"],
|
|
144
|
-
default: config.report.defaultFormat,
|
|
143
|
+
choices: ["markdown", "word", "txt", "json"],
|
|
144
|
+
default: config.report.defaultFormat || "markdown",
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
type: "confirm",
|
package/dist/commands/report.js
CHANGED
|
@@ -8,6 +8,8 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const docx_1 = require("docx");
|
|
9
9
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
13
|
const config_1 = require("../core/config");
|
|
12
14
|
const scan_storage_1 = require("../core/scan-storage");
|
|
13
15
|
const ai_client_1 = require("../core/ai-client");
|
|
@@ -16,7 +18,7 @@ function registerReportCommand(program) {
|
|
|
16
18
|
program
|
|
17
19
|
.command("report [scan-file]")
|
|
18
20
|
.description("Generate a professional security report")
|
|
19
|
-
.option("-f, --format <type>", "Report format: word|json|txt")
|
|
21
|
+
.option("-f, --format <type>", "Report format: word|json|txt|markdown")
|
|
20
22
|
.option("-o, --output <file>", "Output filename")
|
|
21
23
|
.option("--ai-summary", "Generate an AI-powered executive summary")
|
|
22
24
|
.action(async (scanFile, options) => {
|
|
@@ -34,7 +36,10 @@ function registerReportCommand(program) {
|
|
|
34
36
|
const content = await promises_1.default.readFile(filepath, "utf-8");
|
|
35
37
|
const scanResult = JSON.parse(content);
|
|
36
38
|
const config = await (0, config_1.getConfig)();
|
|
37
|
-
|
|
39
|
+
let format = (options.format || config.report.defaultFormat);
|
|
40
|
+
if (!options.format && !config.report.defaultFormat) {
|
|
41
|
+
format = "markdown";
|
|
42
|
+
}
|
|
38
43
|
let aiSummary;
|
|
39
44
|
if (options.aiSummary) {
|
|
40
45
|
spinner = logger_1.logger.spinner("Generating AI executive summary...");
|
|
@@ -47,17 +52,42 @@ function registerReportCommand(program) {
|
|
|
47
52
|
spinner.warn(`AI summary failed: ${err.message}`);
|
|
48
53
|
}
|
|
49
54
|
}
|
|
55
|
+
let outputDir = "";
|
|
56
|
+
if (!options.output) {
|
|
57
|
+
const { location } = await inquirer_1.default.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: "list",
|
|
60
|
+
name: "location",
|
|
61
|
+
message: "Where should the report be saved?",
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: "Current Project Directory (./)", value: "cwd" },
|
|
64
|
+
{ name: "Desktop", value: "desktop" },
|
|
65
|
+
{ name: "Default Reports Directory (~/.kramscan/reports/)", value: "default" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
if (location === "cwd") {
|
|
70
|
+
outputDir = process.cwd();
|
|
71
|
+
}
|
|
72
|
+
else if (location === "desktop") {
|
|
73
|
+
outputDir = path_1.default.join(os_1.default.homedir(), "Desktop");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
spinner = logger_1.logger.spinner(`Generating ${format.toUpperCase()} report...`);
|
|
51
77
|
let outputPath;
|
|
52
78
|
switch (format) {
|
|
53
79
|
case "word":
|
|
54
|
-
outputPath = await generateWordReport(scanResult, options.output, aiSummary);
|
|
80
|
+
outputPath = await generateWordReport(scanResult, options.output, aiSummary, outputDir);
|
|
55
81
|
break;
|
|
56
82
|
case "json":
|
|
57
|
-
outputPath = await generateJsonReport(scanResult, options.output, aiSummary);
|
|
83
|
+
outputPath = await generateJsonReport(scanResult, options.output, aiSummary, outputDir);
|
|
58
84
|
break;
|
|
59
85
|
case "txt":
|
|
60
|
-
outputPath = await generateTxtReport(scanResult, options.output, aiSummary);
|
|
86
|
+
outputPath = await generateTxtReport(scanResult, options.output, aiSummary, outputDir);
|
|
87
|
+
break;
|
|
88
|
+
case "markdown":
|
|
89
|
+
case "md":
|
|
90
|
+
outputPath = await generateMarkdownReport(scanResult, options.output, aiSummary, outputDir);
|
|
61
91
|
break;
|
|
62
92
|
default:
|
|
63
93
|
throw new Error(`Unsupported format: ${format}`);
|
|
@@ -76,7 +106,7 @@ function registerReportCommand(program) {
|
|
|
76
106
|
}
|
|
77
107
|
});
|
|
78
108
|
}
|
|
79
|
-
async function generateWordReport(scanResult, outputFile, aiSummary) {
|
|
109
|
+
async function generateWordReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
80
110
|
const doc = new docx_1.Document({
|
|
81
111
|
sections: [
|
|
82
112
|
{
|
|
@@ -145,15 +175,15 @@ async function generateWordReport(scanResult, outputFile, aiSummary) {
|
|
|
145
175
|
],
|
|
146
176
|
});
|
|
147
177
|
const buffer = await docx_1.Packer.toBuffer(doc);
|
|
148
|
-
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
178
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
149
179
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
150
180
|
const filename = outputFile || `report-${timestamp}.docx`;
|
|
151
181
|
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
152
182
|
await promises_1.default.writeFile(filepath, buffer);
|
|
153
183
|
return filepath;
|
|
154
184
|
}
|
|
155
|
-
async function generateJsonReport(scanResult, outputFile, aiSummary) {
|
|
156
|
-
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
185
|
+
async function generateJsonReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
186
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
157
187
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
158
188
|
const filename = outputFile || `report-${timestamp}.json`;
|
|
159
189
|
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
@@ -161,7 +191,7 @@ async function generateJsonReport(scanResult, outputFile, aiSummary) {
|
|
|
161
191
|
await promises_1.default.writeFile(filepath, JSON.stringify(finalResult, null, 2));
|
|
162
192
|
return filepath;
|
|
163
193
|
}
|
|
164
|
-
async function generateTxtReport(scanResult, outputFile, aiSummary) {
|
|
194
|
+
async function generateTxtReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
165
195
|
const lines = [];
|
|
166
196
|
lines.push("=".repeat(60));
|
|
167
197
|
lines.push("SECURITY ASSESSMENT REPORT");
|
|
@@ -208,10 +238,58 @@ async function generateTxtReport(scanResult, outputFile, aiSummary) {
|
|
|
208
238
|
lines.push("=".repeat(60));
|
|
209
239
|
lines.push("End of Report");
|
|
210
240
|
lines.push("=".repeat(60));
|
|
211
|
-
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
241
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
212
242
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
213
243
|
const filename = outputFile || `report-${timestamp}.txt`;
|
|
214
244
|
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
215
245
|
await promises_1.default.writeFile(filepath, lines.join("\n"));
|
|
216
246
|
return filepath;
|
|
217
247
|
}
|
|
248
|
+
async function generateMarkdownReport(scanResult, outputFile, aiSummary, outputDir) {
|
|
249
|
+
const lines = [];
|
|
250
|
+
lines.push("# Security Assessment Report");
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push(`**Target:** \`${scanResult.target}\``);
|
|
253
|
+
lines.push(`**Date:** ${new Date(scanResult.timestamp).toLocaleString()}`);
|
|
254
|
+
lines.push(`**Duration:** ${(scanResult.duration / 1000).toFixed(2)}s`);
|
|
255
|
+
lines.push("");
|
|
256
|
+
lines.push("## Executive Summary");
|
|
257
|
+
if (aiSummary) {
|
|
258
|
+
lines.push(aiSummary);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
lines.push(`Total Vulnerabilities: **${scanResult.summary.total}** (${scanResult.summary.critical} Critical, ${scanResult.summary.high} High, ${scanResult.summary.medium} Medium, ${scanResult.summary.low} Low, ${scanResult.summary.info} Info)`);
|
|
262
|
+
}
|
|
263
|
+
lines.push("");
|
|
264
|
+
lines.push("## Scan Statistics");
|
|
265
|
+
lines.push(`- **URLs Crawled:** ${scanResult.metadata.crawledUrls}`);
|
|
266
|
+
lines.push(`- **Forms Tested:** ${scanResult.metadata.testedForms}`);
|
|
267
|
+
lines.push(`- **Requests Made:** ${scanResult.metadata.requestsMade}`);
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push("## Detailed Findings");
|
|
270
|
+
lines.push("");
|
|
271
|
+
scanResult.vulnerabilities.forEach((vuln, index) => {
|
|
272
|
+
lines.push(`### ${index + 1}. ${vuln.title} [${vuln.severity.toUpperCase()}]`);
|
|
273
|
+
lines.push(`- **URL:** \`${vuln.url}\``);
|
|
274
|
+
lines.push(`- **Type:** ${vuln.type}`);
|
|
275
|
+
lines.push(`- **Description:** ${vuln.description}`);
|
|
276
|
+
if (vuln.evidence) {
|
|
277
|
+
lines.push(`- **Evidence:** \`${vuln.evidence}\``);
|
|
278
|
+
}
|
|
279
|
+
if (vuln.remediation) {
|
|
280
|
+
lines.push(`- **Remediation:** ${vuln.remediation}`);
|
|
281
|
+
}
|
|
282
|
+
if (vuln.cwe) {
|
|
283
|
+
lines.push(`- **CWE:** ${vuln.cwe}`);
|
|
284
|
+
}
|
|
285
|
+
lines.push("");
|
|
286
|
+
});
|
|
287
|
+
lines.push("---");
|
|
288
|
+
lines.push("*Generated by KramScan*");
|
|
289
|
+
const reportsDir = outputDir || await (0, scan_storage_1.ensureReportsDirectory)();
|
|
290
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
291
|
+
const filename = outputFile || `report-${timestamp}.md`;
|
|
292
|
+
const filepath = path_1.default.isAbsolute(filename) ? filename : path_1.default.join(reportsDir, filename);
|
|
293
|
+
await promises_1.default.writeFile(filepath, lines.join("\n"));
|
|
294
|
+
return filepath;
|
|
295
|
+
}
|
package/dist/commands/scan.js
CHANGED
|
@@ -71,6 +71,9 @@ function registerScanCommand(program) {
|
|
|
71
71
|
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
72
72
|
console.log("");
|
|
73
73
|
}
|
|
74
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
75
|
+
url = `http://${url}`;
|
|
76
|
+
}
|
|
74
77
|
// Validate URL
|
|
75
78
|
try {
|
|
76
79
|
new URL(url);
|
|
@@ -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) {
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
-
|
|
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;
|