kramscan 0.1.1 → 0.3.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 +1 -1
- package/README.md +419 -236
- package/dist/agent/confirmation.d.ts +5 -1
- package/dist/agent/confirmation.js +29 -9
- package/dist/agent/context.js +2 -3
- package/dist/agent/orchestrator.d.ts +2 -0
- package/dist/agent/orchestrator.js +50 -8
- package/dist/agent/prompts/system.d.ts +1 -1
- package/dist/agent/prompts/system.js +5 -7
- package/dist/agent/skills/health-check.js +22 -2
- package/dist/agent/skills/index.d.ts +1 -0
- package/dist/agent/skills/index.js +3 -1
- package/dist/agent/skills/verify-finding.d.ts +17 -0
- package/dist/agent/skills/verify-finding.js +91 -0
- package/dist/agent/skills/web-scan.js +46 -0
- package/dist/cli.js +156 -149
- package/dist/commands/agent.js +38 -38
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +112 -0
- package/dist/commands/analyze.js +103 -54
- package/dist/commands/config.js +55 -29
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +236 -0
- package/dist/commands/doctor.js +20 -15
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +109 -0
- package/dist/commands/onboard.js +188 -141
- package/dist/commands/report.js +68 -76
- package/dist/commands/scan.js +262 -81
- package/dist/commands/scans.d.ts +2 -0
- package/dist/commands/scans.js +55 -0
- package/dist/core/ai-client.d.ts +6 -1
- package/dist/core/ai-client.js +80 -12
- package/dist/core/ai-payloads.d.ts +17 -0
- package/dist/core/ai-payloads.js +54 -0
- package/dist/core/config-schema.d.ts +197 -0
- package/dist/core/config-schema.js +68 -0
- package/dist/core/config-schema.test.d.ts +1 -0
- package/dist/core/config-schema.test.js +151 -0
- package/dist/core/config.d.ts +8 -31
- package/dist/core/config.js +71 -14
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +162 -0
- package/dist/core/scan-index.d.ts +20 -0
- package/dist/core/scan-index.js +52 -0
- package/dist/core/scan-storage.d.ts +11 -0
- package/dist/core/scan-storage.js +69 -0
- package/dist/core/scanner.d.ts +95 -13
- package/dist/core/scanner.js +342 -248
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +9 -0
- package/dist/core/vulnerability-detector.js +46 -15
- package/dist/core/vulnerability-detector.test.d.ts +1 -0
- package/dist/core/vulnerability-detector.test.js +210 -0
- package/dist/index.js +3 -0
- package/dist/plugins/PluginManager.d.ts +27 -0
- package/dist/plugins/PluginManager.js +166 -0
- package/dist/plugins/index.d.ts +12 -0
- package/dist/plugins/index.js +29 -0
- package/dist/plugins/types.d.ts +55 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
- package/dist/reports/PdfGenerator.d.ts +36 -0
- package/dist/reports/PdfGenerator.js +404 -0
- package/dist/utils/logger.d.ts +33 -1
- package/dist/utils/logger.js +127 -8
- package/dist/utils/theme.d.ts +56 -0
- package/dist/utils/theme.js +201 -0
- package/package.json +6 -3
package/dist/commands/analyze.js
CHANGED
|
@@ -1,47 +1,66 @@
|
|
|
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
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.registerAnalyzeCommand = registerAnalyzeCommand;
|
|
40
|
+
const commander_1 = require("commander");
|
|
7
41
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
42
|
const ai_client_1 = require("../core/ai-client");
|
|
43
|
+
const scan_storage_1 = require("../core/scan-storage");
|
|
9
44
|
const logger_1 = require("../utils/logger");
|
|
45
|
+
const theme_1 = require("../utils/theme");
|
|
10
46
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const os_1 = __importDefault(require("os"));
|
|
13
47
|
function registerAnalyzeCommand(program) {
|
|
14
48
|
program
|
|
15
49
|
.command("analyze [scan-file]")
|
|
16
50
|
.description("AI-powered analysis of scan results")
|
|
17
51
|
.option("-m, --model <name>", "Override default AI model")
|
|
18
52
|
.option("-v, --verbose", "Show detailed analysis")
|
|
19
|
-
.action(async (scanFile
|
|
53
|
+
.action(async (scanFile) => {
|
|
20
54
|
console.log("");
|
|
21
|
-
console.log(chalk_1.default.bold.cyan("
|
|
22
|
-
console.log(chalk_1.default.gray("
|
|
55
|
+
console.log(chalk_1.default.bold.cyan("AI Security Analysis"));
|
|
56
|
+
console.log(chalk_1.default.gray("-".repeat(50)));
|
|
23
57
|
console.log("");
|
|
58
|
+
let spinner = null;
|
|
24
59
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
? scanFile
|
|
30
|
-
: path_1.default.join(process.cwd(), scanFile);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
// Find latest scan
|
|
34
|
-
const scanDir = path_1.default.join(os_1.default.homedir(), ".kramscan", "scans");
|
|
35
|
-
const files = await promises_1.default.readdir(scanDir);
|
|
36
|
-
const scanFiles = files.filter((f) => f.endsWith(".json"));
|
|
37
|
-
if (scanFiles.length === 0) {
|
|
38
|
-
logger_1.logger.error("No scan results found. Run 'kramscan scan <url>' first.");
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
// Get most recent
|
|
42
|
-
scanFiles.sort().reverse();
|
|
43
|
-
filepath = path_1.default.join(scanDir, scanFiles[0]);
|
|
44
|
-
logger_1.logger.info(`Using latest scan: ${scanFiles[0]}`);
|
|
60
|
+
const resolved = await (0, scan_storage_1.resolveScanFile)(scanFile);
|
|
61
|
+
const filepath = resolved.filepath;
|
|
62
|
+
if (resolved.isLatest) {
|
|
63
|
+
logger_1.logger.info(`Using latest scan: ${resolved.filename}`);
|
|
45
64
|
}
|
|
46
65
|
const content = await promises_1.default.readFile(filepath, "utf-8");
|
|
47
66
|
const scanResult = JSON.parse(content);
|
|
@@ -49,26 +68,53 @@ function registerAnalyzeCommand(program) {
|
|
|
49
68
|
logger_1.logger.success("No vulnerabilities to analyze!");
|
|
50
69
|
return;
|
|
51
70
|
}
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
spinner = logger_1.logger.spinner("Analyzing vulnerabilities with AI...");
|
|
72
|
+
const { getConfig } = await Promise.resolve().then(() => __importStar(require("../core/config")));
|
|
73
|
+
const config = await getConfig();
|
|
74
|
+
if (!config.ai.enabled) {
|
|
75
|
+
spinner.stop();
|
|
76
|
+
const { default: inquirer } = await Promise.resolve().then(() => __importStar(require("inquirer")));
|
|
77
|
+
const { setup } = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: "confirm",
|
|
80
|
+
name: "setup",
|
|
81
|
+
message: theme_1.theme.yellow("AI analysis is not enabled. Would you like to set it up now?"),
|
|
82
|
+
default: true,
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
if (setup) {
|
|
86
|
+
const { registerOnboardCommand } = await Promise.resolve().then(() => __importStar(require("./onboard")));
|
|
87
|
+
const onboardProgram = new commander_1.Command();
|
|
88
|
+
registerOnboardCommand(onboardProgram);
|
|
89
|
+
await onboardProgram.parseAsync(["node", "kramscan", "onboard"]);
|
|
90
|
+
// Re-check after onboarding
|
|
91
|
+
const updatedConfig = await getConfig();
|
|
92
|
+
if (!updatedConfig.ai.enabled) {
|
|
93
|
+
logger_1.logger.error("AI analysis still not enabled. Exiting.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
spinner.start("Analyzing vulnerabilities with AI...");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
logger_1.logger.warn("AI analysis skipped.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
54
103
|
const aiClient = await (0, ai_client_1.createAIClient)();
|
|
55
|
-
// Build analysis prompt
|
|
56
104
|
const prompt = buildAnalysisPrompt(scanResult);
|
|
57
|
-
// Get AI analysis
|
|
58
105
|
const response = await aiClient.analyze(prompt);
|
|
59
106
|
spinner.succeed("Analysis complete!");
|
|
60
107
|
console.log("");
|
|
61
|
-
console.log(chalk_1.default.bold("
|
|
62
|
-
console.log(chalk_1.default.gray("
|
|
108
|
+
console.log(chalk_1.default.bold("AI Analysis"));
|
|
109
|
+
console.log(chalk_1.default.gray("-".repeat(50)));
|
|
63
110
|
console.log("");
|
|
64
111
|
console.log(response.content);
|
|
65
112
|
console.log("");
|
|
66
113
|
if (response.usage) {
|
|
67
|
-
console.log(chalk_1.default.gray("
|
|
114
|
+
console.log(chalk_1.default.gray("-".repeat(50)));
|
|
68
115
|
console.log(chalk_1.default.gray(`Tokens used: ${response.usage.totalTokens} (${response.usage.promptTokens} prompt + ${response.usage.completionTokens} completion)`));
|
|
69
116
|
console.log("");
|
|
70
117
|
}
|
|
71
|
-
// Save enhanced results
|
|
72
118
|
const enhancedResult = {
|
|
73
119
|
...scanResult,
|
|
74
120
|
aiAnalysis: {
|
|
@@ -82,6 +128,9 @@ function registerAnalyzeCommand(program) {
|
|
|
82
128
|
console.log("");
|
|
83
129
|
}
|
|
84
130
|
catch (error) {
|
|
131
|
+
if (spinner) {
|
|
132
|
+
spinner.fail("Analysis failed");
|
|
133
|
+
}
|
|
85
134
|
logger_1.logger.error(error.message);
|
|
86
135
|
process.exit(1);
|
|
87
136
|
}
|
|
@@ -89,27 +138,27 @@ function registerAnalyzeCommand(program) {
|
|
|
89
138
|
}
|
|
90
139
|
function buildAnalysisPrompt(scanResult) {
|
|
91
140
|
const vulnList = scanResult.vulnerabilities
|
|
92
|
-
.map((v, i) => `${i + 1}. [${v.severity.toUpperCase()}] ${v.title}
|
|
93
|
-
URL: ${v.url}
|
|
94
|
-
Description: ${v.description}
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
.map((v, i) => `${i + 1}. [${v.severity.toUpperCase()}] ${v.title}\n` +
|
|
142
|
+
` URL: ${v.url}\n` +
|
|
143
|
+
` Description: ${v.description}\n` +
|
|
144
|
+
`${v.evidence ? ` Evidence: ${v.evidence}\n` : ""}` +
|
|
145
|
+
`${v.cwe ? ` CWE: ${v.cwe}` : ""}`)
|
|
97
146
|
.join("\n\n");
|
|
98
|
-
return `You are a security expert analyzing web application vulnerabilities.
|
|
99
|
-
|
|
100
|
-
Target: ${scanResult.target}
|
|
101
|
-
Scan Date: ${scanResult.timestamp}
|
|
102
|
-
Total Vulnerabilities: ${scanResult.summary.total}
|
|
103
|
-
|
|
104
|
-
Vulnerabilities Found:
|
|
105
|
-
${vulnList}
|
|
106
|
-
|
|
107
|
-
Please provide:
|
|
108
|
-
1.
|
|
109
|
-
2.
|
|
110
|
-
3.
|
|
111
|
-
4.
|
|
112
|
-
5.
|
|
113
|
-
|
|
147
|
+
return `You are a security expert analyzing web application vulnerabilities.
|
|
148
|
+
|
|
149
|
+
Target: ${scanResult.target}
|
|
150
|
+
Scan Date: ${scanResult.timestamp}
|
|
151
|
+
Total Vulnerabilities: ${scanResult.summary.total}
|
|
152
|
+
|
|
153
|
+
Vulnerabilities Found:
|
|
154
|
+
${vulnList}
|
|
155
|
+
|
|
156
|
+
Please provide:
|
|
157
|
+
1. Executive Summary: Brief overview of the security posture
|
|
158
|
+
2. Risk Assessment: Overall risk level and business impact
|
|
159
|
+
3. Priority Recommendations: Top 3-5 vulnerabilities to fix first, with specific remediation steps
|
|
160
|
+
4. Attack Scenarios: How an attacker could chain these vulnerabilities
|
|
161
|
+
5. Remediation Roadmap: Step-by-step plan to address all findings
|
|
162
|
+
|
|
114
163
|
Format your response in clear markdown with headers and bullet points.`;
|
|
115
164
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -22,21 +22,26 @@ function registerConfigCommand(program) {
|
|
|
22
22
|
logger_1.logger.error(`Configuration key '${key}' not found`);
|
|
23
23
|
process.exit(1);
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
const displayValue = key === "ai.apiKey" && typeof value === "string" && value
|
|
26
|
+
? "***configured***"
|
|
27
|
+
: value;
|
|
28
|
+
console.log(chalk_1.default.cyan(key), "=", chalk_1.default.white(JSON.stringify(displayValue, null, 2)));
|
|
26
29
|
});
|
|
27
30
|
configCmd
|
|
28
31
|
.command("set <key> <value>")
|
|
29
32
|
.description("Set a configuration value")
|
|
30
33
|
.action(async (key, value) => {
|
|
31
34
|
const config = await (0, config_1.getConfig)();
|
|
32
|
-
// Parse value
|
|
33
35
|
let parsedValue = value;
|
|
34
|
-
if (value === "true")
|
|
36
|
+
if (value === "true") {
|
|
35
37
|
parsedValue = true;
|
|
36
|
-
|
|
38
|
+
}
|
|
39
|
+
else if (value === "false") {
|
|
37
40
|
parsedValue = false;
|
|
38
|
-
|
|
41
|
+
}
|
|
42
|
+
else if (!Number.isNaN(Number(value))) {
|
|
39
43
|
parsedValue = Number(value);
|
|
44
|
+
}
|
|
40
45
|
setNestedValue(config, key, parsedValue);
|
|
41
46
|
await (0, config_1.setConfig)(config);
|
|
42
47
|
logger_1.logger.success(`Set ${chalk_1.default.cyan(key)} = ${chalk_1.default.white(JSON.stringify(parsedValue))}`);
|
|
@@ -46,11 +51,18 @@ function registerConfigCommand(program) {
|
|
|
46
51
|
.description("List all configuration")
|
|
47
52
|
.action(async () => {
|
|
48
53
|
const config = await (0, config_1.getConfig)();
|
|
54
|
+
const sanitizedConfig = {
|
|
55
|
+
...config,
|
|
56
|
+
ai: {
|
|
57
|
+
...config.ai,
|
|
58
|
+
apiKey: config.ai.apiKey ? "***configured***" : "",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
49
61
|
console.log("");
|
|
50
|
-
console.log(chalk_1.default.bold.cyan("
|
|
51
|
-
console.log(chalk_1.default.gray("
|
|
62
|
+
console.log(chalk_1.default.bold.cyan("Current Configuration"));
|
|
63
|
+
console.log(chalk_1.default.gray("-".repeat(50)));
|
|
52
64
|
console.log("");
|
|
53
|
-
console.log(JSON.stringify(
|
|
65
|
+
console.log(JSON.stringify(sanitizedConfig, null, 2));
|
|
54
66
|
console.log("");
|
|
55
67
|
});
|
|
56
68
|
configCmd
|
|
@@ -59,8 +71,8 @@ function registerConfigCommand(program) {
|
|
|
59
71
|
.action(async () => {
|
|
60
72
|
const config = await (0, config_1.getConfig)();
|
|
61
73
|
console.log("");
|
|
62
|
-
console.log(chalk_1.default.bold.cyan("
|
|
63
|
-
console.log(chalk_1.default.gray("
|
|
74
|
+
console.log(chalk_1.default.bold.cyan("Configuration Editor"));
|
|
75
|
+
console.log(chalk_1.default.gray("-".repeat(50)));
|
|
64
76
|
console.log("");
|
|
65
77
|
const answers = await inquirer_1.default.prompt([
|
|
66
78
|
{
|
|
@@ -73,23 +85,23 @@ function registerConfigCommand(program) {
|
|
|
73
85
|
type: "list",
|
|
74
86
|
name: "aiProvider",
|
|
75
87
|
message: "AI provider:",
|
|
76
|
-
choices: ["openai", "anthropic"],
|
|
88
|
+
choices: ["openai", "anthropic", "gemini", "openrouter", "mistral", "kimi", "groq"],
|
|
77
89
|
default: config.ai.provider,
|
|
78
|
-
when: (
|
|
90
|
+
when: (ans) => ans.aiEnabled,
|
|
79
91
|
},
|
|
80
92
|
{
|
|
81
93
|
type: "input",
|
|
82
94
|
name: "apiKey",
|
|
83
|
-
message: "API key:",
|
|
84
|
-
default:
|
|
85
|
-
when: (
|
|
95
|
+
message: "API key (leave blank to keep current):",
|
|
96
|
+
default: "",
|
|
97
|
+
when: (ans) => ans.aiEnabled,
|
|
86
98
|
},
|
|
87
99
|
{
|
|
88
100
|
type: "input",
|
|
89
101
|
name: "model",
|
|
90
102
|
message: "Default AI model:",
|
|
91
103
|
default: config.ai.defaultModel,
|
|
92
|
-
when: (
|
|
104
|
+
when: (ans) => ans.aiEnabled,
|
|
93
105
|
},
|
|
94
106
|
{
|
|
95
107
|
type: "list",
|
|
@@ -103,36 +115,50 @@ function registerConfigCommand(program) {
|
|
|
103
115
|
name: "rateLimit",
|
|
104
116
|
message: "Requests per second rate limit:",
|
|
105
117
|
default: config.scan.rateLimitPerSecond,
|
|
118
|
+
validate: (value) => Number.isFinite(value) && value > 0
|
|
119
|
+
? true
|
|
120
|
+
: "Rate limit must be a positive number",
|
|
106
121
|
},
|
|
107
122
|
]);
|
|
108
|
-
|
|
109
|
-
if (answers.aiEnabled !== undefined)
|
|
123
|
+
if (answers.aiEnabled !== undefined) {
|
|
110
124
|
config.ai.enabled = answers.aiEnabled;
|
|
111
|
-
|
|
125
|
+
}
|
|
126
|
+
if (answers.aiProvider) {
|
|
112
127
|
config.ai.provider = answers.aiProvider;
|
|
113
|
-
|
|
128
|
+
}
|
|
129
|
+
if (answers.apiKey) {
|
|
114
130
|
config.ai.apiKey = answers.apiKey;
|
|
115
|
-
|
|
131
|
+
}
|
|
132
|
+
if (answers.model) {
|
|
116
133
|
config.ai.defaultModel = answers.model;
|
|
117
|
-
|
|
134
|
+
}
|
|
135
|
+
if (answers.reportFormat) {
|
|
118
136
|
config.report.defaultFormat = answers.reportFormat;
|
|
119
|
-
|
|
137
|
+
}
|
|
138
|
+
if (Number.isFinite(answers.rateLimit) && answers.rateLimit > 0) {
|
|
120
139
|
config.scan.rateLimitPerSecond = answers.rateLimit;
|
|
121
|
-
|
|
140
|
+
}
|
|
141
|
+
await (0, config_1.setConfig)(config);
|
|
122
142
|
console.log("");
|
|
123
143
|
logger_1.logger.success("Configuration updated successfully!");
|
|
124
144
|
console.log("");
|
|
125
145
|
});
|
|
126
146
|
}
|
|
127
|
-
function getNestedValue(obj,
|
|
128
|
-
return
|
|
147
|
+
function getNestedValue(obj, keyPath) {
|
|
148
|
+
return keyPath
|
|
149
|
+
.split(".")
|
|
150
|
+
.reduce((current, key) => current?.[key], obj);
|
|
129
151
|
}
|
|
130
|
-
function setNestedValue(obj,
|
|
131
|
-
const keys =
|
|
152
|
+
function setNestedValue(obj, keyPath, value) {
|
|
153
|
+
const keys = keyPath.split(".");
|
|
132
154
|
const lastKey = keys.pop();
|
|
155
|
+
if (!lastKey) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
133
158
|
const target = keys.reduce((current, key) => {
|
|
134
|
-
if (!current[key])
|
|
159
|
+
if (!current[key] || typeof current[key] !== "object") {
|
|
135
160
|
current[key] = {};
|
|
161
|
+
}
|
|
136
162
|
return current[key];
|
|
137
163
|
}, obj);
|
|
138
164
|
target[lastKey] = value;
|
|
@@ -0,0 +1,236 @@
|
|
|
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.registerDevCommand = registerDevCommand;
|
|
7
|
+
const scanner_1 = require("../core/scanner");
|
|
8
|
+
const server_probe_1 = require("../core/server-probe");
|
|
9
|
+
const diff_engine_1 = require("../core/diff-engine");
|
|
10
|
+
const theme_1 = require("../utils/theme");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
const theme_2 = require("../utils/theme");
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
15
|
+
function registerDevCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command("dev [url]")
|
|
18
|
+
.description("Watch-mode security scanning for localhost development servers")
|
|
19
|
+
.option("--port <number>", "Shorthand for http://localhost:<port>")
|
|
20
|
+
.option("--watch-dir <path>", "Directory to watch for file changes", "./src")
|
|
21
|
+
.option("--debounce <ms>", "Debounce time before re-scanning (ms)", "2000")
|
|
22
|
+
.option("--profile <name>", "Scan profile: quick|balanced", "quick")
|
|
23
|
+
.option("--notify", "Enable desktop notifications for critical findings")
|
|
24
|
+
.option("--fail-on <severity>", "Exit with code 1 if severity threshold met")
|
|
25
|
+
.option("--no-watch", "Run a single scan without watching (useful for CI)")
|
|
26
|
+
.action(async (url, options) => {
|
|
27
|
+
// Resolve target URL
|
|
28
|
+
const targetUrl = url || (options.port ? `http://localhost:${options.port}` : null);
|
|
29
|
+
if (!targetUrl) {
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log(theme_1.theme.error("✗ No target URL specified."));
|
|
32
|
+
console.log(theme_1.theme.gray(" Usage: kramscan dev http://localhost:3000"));
|
|
33
|
+
console.log(theme_1.theme.gray(" or: kramscan dev --port 3000"));
|
|
34
|
+
console.log("");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const isLocal = (0, server_probe_1.isLocalhost)(targetUrl);
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(theme_1.theme.brand.bold("🛠️ KramScan Dev Mode"));
|
|
40
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(theme_1.theme.white("Target:"), theme_1.theme.cyan(targetUrl));
|
|
43
|
+
console.log(theme_1.theme.white("Profile:"), theme_1.theme.cyan(options.profile));
|
|
44
|
+
if (isLocal) {
|
|
45
|
+
console.log(theme_1.theme.white("Environment:"), theme_1.theme.green("localhost (dev mode)"));
|
|
46
|
+
}
|
|
47
|
+
console.log("");
|
|
48
|
+
// Probe server readiness
|
|
49
|
+
const probeSpinner = logger_1.logger.spinner(`Waiting for ${targetUrl} to be ready...`);
|
|
50
|
+
const probeResult = await (0, server_probe_1.probeServer)(targetUrl, { timeout: 30000 });
|
|
51
|
+
if (!probeResult.reachable) {
|
|
52
|
+
probeSpinner.fail(`Server at ${targetUrl} is not responding`);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(theme_1.theme.warning("⚠️ Make sure your dev server is running:"));
|
|
55
|
+
console.log(theme_1.theme.gray(" • npm run dev"));
|
|
56
|
+
console.log(theme_1.theme.gray(" • yarn dev"));
|
|
57
|
+
console.log(theme_1.theme.gray(" • python manage.py runserver"));
|
|
58
|
+
console.log("");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
probeSpinner.succeed(`Server ready! (${probeResult.responseTime}ms` +
|
|
62
|
+
`${probeResult.framework ? `, ${probeResult.framework}` : ""}` +
|
|
63
|
+
`${probeResult.server ? `, ${probeResult.server}` : ""})`);
|
|
64
|
+
// Run initial scan
|
|
65
|
+
let previousResult = null;
|
|
66
|
+
const runScan = async (isRescan = false) => {
|
|
67
|
+
const scanSpinner = logger_1.logger.spinner(isRescan ? "Re-scanning after code change..." : "Running initial security scan...");
|
|
68
|
+
try {
|
|
69
|
+
const scanner = new scanner_1.Scanner(true);
|
|
70
|
+
const scanOptions = {
|
|
71
|
+
depth: 2,
|
|
72
|
+
timeout: 10000,
|
|
73
|
+
headless: true,
|
|
74
|
+
maxPages: 15,
|
|
75
|
+
maxLinksPerPage: 30,
|
|
76
|
+
profile: options.profile,
|
|
77
|
+
};
|
|
78
|
+
const result = await scanner.scan(targetUrl, scanOptions);
|
|
79
|
+
scanSpinner.succeed(isRescan
|
|
80
|
+
? `Re-scan complete: ${result.summary.total} vulnerabilities`
|
|
81
|
+
: `Initial scan complete: ${result.summary.total} vulnerabilities`);
|
|
82
|
+
await scanner.close();
|
|
83
|
+
if (isRescan && previousResult) {
|
|
84
|
+
// Show diff
|
|
85
|
+
const diff = (0, diff_engine_1.diffScanResults)(previousResult, result);
|
|
86
|
+
displayDiff(diff);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Show full summary for initial scan
|
|
90
|
+
(0, theme_1.displayScanSummary)({
|
|
91
|
+
target: result.target,
|
|
92
|
+
duration: result.duration,
|
|
93
|
+
metadata: result.metadata,
|
|
94
|
+
summary: result.summary,
|
|
95
|
+
vulnerabilities: result.vulnerabilities,
|
|
96
|
+
score: result.score,
|
|
97
|
+
filepath: "(dev mode — results in memory)",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Desktop notification for critical/high findings
|
|
101
|
+
if (options.notify && result.summary.critical + result.summary.high > 0) {
|
|
102
|
+
try {
|
|
103
|
+
// node-notifier is optional — skip if not installed
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
105
|
+
const notifier = require("node-notifier");
|
|
106
|
+
notifier.notify({
|
|
107
|
+
title: "⚠️ KramScan Security Alert",
|
|
108
|
+
message: `Found ${result.summary.critical} critical, ${result.summary.high} high vulnerabilities on ${targetUrl}`,
|
|
109
|
+
sound: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// node-notifier not installed, skip silently
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Check fail threshold
|
|
117
|
+
if (options.failOn) {
|
|
118
|
+
const shouldFail = checkThreshold(result, options.failOn);
|
|
119
|
+
if (shouldFail && !options.watch) {
|
|
120
|
+
console.log(theme_1.theme.error(`\n✗ Security gate failed: found vulnerabilities at or above '${options.failOn}' severity.\n`));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
scanSpinner.fail(`Scan failed: ${err.message}`);
|
|
128
|
+
return previousResult;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// Initial scan
|
|
132
|
+
previousResult = await runScan(false);
|
|
133
|
+
// Watch mode
|
|
134
|
+
if (options.watch !== false) {
|
|
135
|
+
const watchDir = path_1.default.resolve(options.watchDir);
|
|
136
|
+
try {
|
|
137
|
+
await promises_1.default.access(watchDir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
console.log(theme_1.theme.warning(`⚠️ Watch directory not found: ${watchDir}`));
|
|
141
|
+
console.log(theme_1.theme.gray(" Falling back to current directory."));
|
|
142
|
+
}
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(theme_1.theme.brand("👁️ Watching for changes..."));
|
|
145
|
+
console.log(theme_1.theme.gray(` Directory: ${watchDir}`));
|
|
146
|
+
console.log(theme_1.theme.gray(` Debounce: ${options.debounce}ms`));
|
|
147
|
+
console.log(theme_1.theme.gray(" Press Ctrl+C to stop."));
|
|
148
|
+
console.log("");
|
|
149
|
+
// Use fs.watch with recursive option (Node.js 19+)
|
|
150
|
+
let debounceTimer = null;
|
|
151
|
+
let scanning = false;
|
|
152
|
+
try {
|
|
153
|
+
const watcher = promises_1.default.watch(watchDir, { recursive: true });
|
|
154
|
+
for await (const event of watcher) {
|
|
155
|
+
if (scanning)
|
|
156
|
+
continue;
|
|
157
|
+
// Ignore node_modules, dist, .git, etc.
|
|
158
|
+
const filename = event.filename || "";
|
|
159
|
+
if (filename.includes("node_modules") ||
|
|
160
|
+
filename.includes("dist") ||
|
|
161
|
+
filename.includes(".git") ||
|
|
162
|
+
filename.includes(".next") ||
|
|
163
|
+
filename.includes("__pycache__")) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (debounceTimer)
|
|
167
|
+
clearTimeout(debounceTimer);
|
|
168
|
+
debounceTimer = setTimeout(async () => {
|
|
169
|
+
scanning = true;
|
|
170
|
+
console.log("");
|
|
171
|
+
console.log(theme_1.theme.dim(`📝 Change detected: ${filename}`));
|
|
172
|
+
previousResult = await runScan(true);
|
|
173
|
+
scanning = false;
|
|
174
|
+
}, parseInt(options.debounce, 10));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.log(theme_1.theme.warning(`⚠️ File watching failed: ${err.message}`));
|
|
179
|
+
console.log(theme_1.theme.gray(" Make sure the watch directory exists and is readable."));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function displayDiff(diff) {
|
|
185
|
+
console.log("");
|
|
186
|
+
console.log(theme_1.theme.brightWhite.bold("🔄 Scan Diff Report"));
|
|
187
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
188
|
+
if (diff.newVulnerabilities.length === 0 && diff.resolvedVulnerabilities.length === 0) {
|
|
189
|
+
console.log(theme_1.theme.gray(" No changes since last scan."));
|
|
190
|
+
console.log(theme_1.theme.gray(` ${diff.unchangedCount} vulnerabilities unchanged.`));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// New vulnerabilities
|
|
194
|
+
if (diff.newVulnerabilities.length > 0) {
|
|
195
|
+
console.log("");
|
|
196
|
+
console.log(theme_1.theme.error(` 🆕 ${diff.newVulnerabilities.length} New Vulnerabilities`));
|
|
197
|
+
for (const v of diff.newVulnerabilities) {
|
|
198
|
+
const color = (0, theme_2.getSeverityColor)(v.severity);
|
|
199
|
+
console.log(color(` [${v.severity.toUpperCase()}] ${v.title}`));
|
|
200
|
+
console.log(theme_1.theme.gray(` ${v.url}`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Resolved vulnerabilities
|
|
204
|
+
if (diff.resolvedVulnerabilities.length > 0) {
|
|
205
|
+
console.log("");
|
|
206
|
+
console.log(theme_1.theme.success(` ✅ ${diff.resolvedVulnerabilities.length} Resolved`));
|
|
207
|
+
for (const v of diff.resolvedVulnerabilities) {
|
|
208
|
+
console.log(theme_1.theme.green(` ✓ ${v.title}`));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
console.log("");
|
|
212
|
+
console.log(theme_1.theme.gray(` Total: ${diff.previousTotal} → ${diff.currentTotal} `) +
|
|
213
|
+
(diff.currentTotal < diff.previousTotal
|
|
214
|
+
? theme_1.theme.green(`(↓ ${diff.previousTotal - diff.currentTotal})`)
|
|
215
|
+
: diff.currentTotal > diff.previousTotal
|
|
216
|
+
? theme_1.theme.error(`(↑ ${diff.currentTotal - diff.previousTotal})`)
|
|
217
|
+
: theme_1.theme.gray("(no change)")));
|
|
218
|
+
}
|
|
219
|
+
console.log("");
|
|
220
|
+
}
|
|
221
|
+
function checkThreshold(result, failOn) {
|
|
222
|
+
const severityLevels = {
|
|
223
|
+
critical: 4,
|
|
224
|
+
high: 3,
|
|
225
|
+
medium: 2,
|
|
226
|
+
low: 1,
|
|
227
|
+
info: 0,
|
|
228
|
+
};
|
|
229
|
+
const threshold = severityLevels[failOn.toLowerCase()] ?? 3;
|
|
230
|
+
for (const v of result.vulnerabilities) {
|
|
231
|
+
if ((severityLevels[v.severity] ?? 0) >= threshold) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|