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/scan.js
CHANGED
|
@@ -1,124 +1,305 @@
|
|
|
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.registerScanCommand = registerScanCommand;
|
|
7
|
-
const
|
|
40
|
+
const commander_1 = require("commander");
|
|
8
41
|
const scanner_1 = require("../core/scanner");
|
|
42
|
+
const scan_index_1 = require("../core/scan-index");
|
|
43
|
+
const scan_storage_1 = require("../core/scan-storage");
|
|
44
|
+
const config_1 = require("../core/config");
|
|
45
|
+
const PdfGenerator_1 = require("../reports/PdfGenerator");
|
|
46
|
+
const theme_1 = require("../utils/theme");
|
|
9
47
|
const logger_1 = require("../utils/logger");
|
|
10
48
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
49
|
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const os_1 = __importDefault(require("os"));
|
|
13
50
|
function registerScanCommand(program) {
|
|
14
51
|
program
|
|
15
52
|
.command("scan <url>")
|
|
16
53
|
.description("Scan a target URL for vulnerabilities")
|
|
17
|
-
.option("
|
|
18
|
-
.option("-
|
|
54
|
+
.option("--profile <name>", "Scan profile: quick|balanced|deep", "balanced")
|
|
55
|
+
.option("-d, --depth <number>", "Crawl depth (overrides profile)")
|
|
56
|
+
.option("-t, --timeout <ms>", "Request timeout (overrides profile)")
|
|
57
|
+
.option("--max-pages <number>", "Maximum pages to crawl (overrides profile)")
|
|
58
|
+
.option("--max-links-per-page <number>", "Maximum links to follow per page (overrides profile)")
|
|
59
|
+
.option("--include <regex...>", "Only include URLs matching these regex patterns")
|
|
60
|
+
.option("--exclude <regex...>", "Exclude URLs matching these regex patterns")
|
|
61
|
+
.option("--no-pdf", "Disable automatic PDF report generation")
|
|
62
|
+
.option("--json", "Output scan results as JSON to stdout (CI/CD mode)")
|
|
19
63
|
.option("-o, --output <file>", "Save results to file")
|
|
20
64
|
.option("--headless", "Run in headless mode", true)
|
|
65
|
+
.option("--no-plugins", "Disable plugin-based scanning (use legacy mode)")
|
|
21
66
|
.action(async (url, options) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
67
|
+
const jsonMode = options.json === true;
|
|
68
|
+
if (!jsonMode) {
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(theme_1.theme.brand.bold("🔍 Starting Security Scan"));
|
|
71
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
72
|
+
console.log("");
|
|
73
|
+
}
|
|
26
74
|
// Validate URL
|
|
27
75
|
try {
|
|
28
76
|
new URL(url);
|
|
29
77
|
}
|
|
30
78
|
catch (error) {
|
|
31
|
-
|
|
79
|
+
if (jsonMode) {
|
|
80
|
+
console.log(JSON.stringify({ error: `Invalid URL: ${url}` }));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
logger_1.logger.error(`Invalid URL: ${url}`);
|
|
84
|
+
}
|
|
32
85
|
process.exit(1);
|
|
33
86
|
}
|
|
34
|
-
const spinner = logger_1.logger.spinner("Initializing scanner...");
|
|
87
|
+
const spinner = jsonMode ? null : logger_1.logger.spinner("Initializing scanner...");
|
|
35
88
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
89
|
+
const profile = String(options.profile || "balanced").toLowerCase();
|
|
90
|
+
const defaults = config_1.scanProfiles[profile] || config_1.scanProfiles.balanced;
|
|
91
|
+
const parsedDepth = Number.parseInt(options.depth ?? String(defaults.depth), 10);
|
|
92
|
+
const parsedTimeout = Number.parseInt(options.timeout ?? String(defaults.timeout), 10);
|
|
93
|
+
const parsedMaxPages = Number.parseInt(options.maxPages ?? String(defaults.maxPages), 10);
|
|
94
|
+
const parsedMaxLinksPerPage = Number.parseInt(options.maxLinksPerPage ?? String(defaults.maxLinksPerPage), 10);
|
|
95
|
+
if (!Number.isFinite(parsedDepth) || parsedDepth < 1 || parsedDepth > 5) {
|
|
96
|
+
throw new Error("Depth must be a number between 1 and 5.");
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isFinite(parsedTimeout) || parsedTimeout < 1000) {
|
|
99
|
+
throw new Error("Timeout must be a positive number (milliseconds).");
|
|
100
|
+
}
|
|
101
|
+
if (!Number.isFinite(parsedMaxPages) || parsedMaxPages < 1) {
|
|
102
|
+
throw new Error("max-pages must be a positive number.");
|
|
103
|
+
}
|
|
104
|
+
if (!Number.isFinite(parsedMaxLinksPerPage) || parsedMaxLinksPerPage < 1) {
|
|
105
|
+
throw new Error("max-links-per-page must be a positive number.");
|
|
106
|
+
}
|
|
107
|
+
// Display scan estimate
|
|
108
|
+
if (!jsonMode) {
|
|
109
|
+
const estimateMap = {
|
|
110
|
+
quick: "~15–30s",
|
|
111
|
+
balanced: "~30–90s",
|
|
112
|
+
deep: "~2–5min",
|
|
113
|
+
};
|
|
114
|
+
const estimate = estimateMap[profile] || estimateMap.balanced;
|
|
115
|
+
console.log(theme_1.theme.gray(` ⏱ Estimated duration: ${estimate} (${profile} profile)`));
|
|
116
|
+
console.log("");
|
|
117
|
+
}
|
|
118
|
+
const scanner = new scanner_1.Scanner(options.plugins !== false);
|
|
119
|
+
// Set up event listeners for progress feedback
|
|
120
|
+
let currentStage = "initializing";
|
|
121
|
+
let vulnerabilitiesFound = 0;
|
|
122
|
+
scanner.on("scan:start", () => {
|
|
123
|
+
if (spinner)
|
|
124
|
+
spinner.text = `Starting scan of ${url}...`;
|
|
125
|
+
currentStage = "scanning";
|
|
126
|
+
});
|
|
127
|
+
scanner.on("crawl:page", (data) => {
|
|
128
|
+
if (spinner)
|
|
129
|
+
spinner.text = `Crawling: ${data.url} (${data.crawledCount}/${data.maxPages})`;
|
|
130
|
+
currentStage = "crawling";
|
|
131
|
+
});
|
|
132
|
+
scanner.on("form:test", (data) => {
|
|
133
|
+
if (spinner)
|
|
134
|
+
spinner.text = `Testing forms on ${data.url} (${data.formCount} forms)...`;
|
|
135
|
+
currentStage = "testing forms";
|
|
42
136
|
});
|
|
43
|
-
|
|
137
|
+
scanner.on("vuln:found", (data) => {
|
|
138
|
+
vulnerabilitiesFound++;
|
|
139
|
+
if (spinner) {
|
|
140
|
+
spinner.stopAndPersist({
|
|
141
|
+
symbol: theme_1.theme.warning("⚠️"),
|
|
142
|
+
text: `Found ${data.vulnerability.severity} vulnerability: ${data.vulnerability.title}`
|
|
143
|
+
});
|
|
144
|
+
spinner.start(`Continuing scan (${vulnerabilitiesFound} vulns found)...`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
scanner.on("scan:complete", () => {
|
|
148
|
+
if (spinner)
|
|
149
|
+
spinner.text = "Finalizing scan results...";
|
|
150
|
+
});
|
|
151
|
+
scanner.on("crawl:error", (data) => {
|
|
152
|
+
if (!jsonMode)
|
|
153
|
+
logger_1.logger.warn(`Failed to crawl ${data.url}: ${data.error.message}`);
|
|
154
|
+
});
|
|
155
|
+
const scanOptions = {
|
|
156
|
+
depth: parsedDepth,
|
|
157
|
+
timeout: parsedTimeout,
|
|
158
|
+
headless: options.headless,
|
|
159
|
+
maxPages: parsedMaxPages,
|
|
160
|
+
maxLinksPerPage: parsedMaxLinksPerPage,
|
|
161
|
+
include: options.include,
|
|
162
|
+
exclude: options.exclude,
|
|
163
|
+
profile,
|
|
164
|
+
};
|
|
165
|
+
const result = await scanner.scan(url, scanOptions);
|
|
166
|
+
if (spinner)
|
|
167
|
+
spinner.succeed("Scan complete!");
|
|
44
168
|
// Save results
|
|
45
|
-
const scanDir =
|
|
46
|
-
await promises_1.default.mkdir(scanDir, { recursive: true });
|
|
169
|
+
const scanDir = await (0, scan_storage_1.ensureScansDirectory)();
|
|
47
170
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
48
171
|
const filename = options.output || `scan-${timestamp}.json`;
|
|
49
172
|
const filepath = path_1.default.isAbsolute(filename)
|
|
50
173
|
? filename
|
|
51
174
|
: path_1.default.join(scanDir, filename);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log("");
|
|
68
|
-
const { summary } = result;
|
|
69
|
-
if (summary.total === 0) {
|
|
70
|
-
console.log(chalk_1.default.green("✓ No vulnerabilities found!"));
|
|
175
|
+
// Include error data in JSON
|
|
176
|
+
const scanErrors = scanner.getScanErrors();
|
|
177
|
+
const pluginErrors = scanner.getPluginErrors();
|
|
178
|
+
const resultWithErrors = {
|
|
179
|
+
...result,
|
|
180
|
+
errors: {
|
|
181
|
+
scan: scanErrors,
|
|
182
|
+
plugins: Object.fromEntries(pluginErrors),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
await promises_1.default.writeFile(filepath, JSON.stringify(resultWithErrors, null, 2));
|
|
186
|
+
// In JSON mode, output the result to stdout and exit
|
|
187
|
+
if (jsonMode) {
|
|
188
|
+
console.log(JSON.stringify(resultWithErrors, null, 2));
|
|
189
|
+
return;
|
|
71
190
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
191
|
+
let pdfPath = null;
|
|
192
|
+
if (options.pdf !== false) {
|
|
193
|
+
const pdfSpinner = logger_1.logger.spinner("Generating PDF report...");
|
|
194
|
+
try {
|
|
195
|
+
const pdfData = {
|
|
196
|
+
scanResult: result,
|
|
197
|
+
scanErrors,
|
|
198
|
+
pluginErrors,
|
|
199
|
+
};
|
|
200
|
+
pdfPath = await PdfGenerator_1.pdfGenerator.generate(pdfData);
|
|
201
|
+
pdfSpinner.succeed("PDF report generated!");
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
pdfSpinner.fail("PDF report generation failed");
|
|
205
|
+
logger_1.logger.warn(`Could not generate PDF automatically: ${error.message}`);
|
|
206
|
+
}
|
|
83
207
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
208
|
+
try {
|
|
209
|
+
const target = new URL(result.target);
|
|
210
|
+
await (0, scan_index_1.addScanToIndex)({
|
|
211
|
+
target: result.target,
|
|
212
|
+
hostname: target.hostname || "unknown",
|
|
213
|
+
timestamp: result.timestamp,
|
|
214
|
+
jsonPath: filepath,
|
|
215
|
+
pdfPath: pdfPath || undefined,
|
|
216
|
+
summary: result.summary,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
logger_1.logger.debug(`Failed to update scan index: ${error.message}`);
|
|
221
|
+
}
|
|
222
|
+
// Display summary using theme
|
|
223
|
+
(0, theme_1.displayScanSummary)({
|
|
224
|
+
target: result.target,
|
|
225
|
+
duration: result.duration,
|
|
226
|
+
metadata: result.metadata,
|
|
227
|
+
summary: result.summary,
|
|
228
|
+
vulnerabilities: result.vulnerabilities,
|
|
229
|
+
score: result.score,
|
|
230
|
+
filepath,
|
|
231
|
+
pdfPath,
|
|
232
|
+
});
|
|
233
|
+
// Display any scan errors
|
|
234
|
+
const scanErrorsList = scanner.getScanErrors();
|
|
235
|
+
const pluginErrorsMap = scanner.getPluginErrors();
|
|
236
|
+
if (scanErrorsList.length > 0 || pluginErrorsMap.size > 0) {
|
|
237
|
+
console.log(theme_1.theme.warning("⚠️ Some URLs/plugins encountered errors:"));
|
|
238
|
+
if (scanErrorsList.length > 0) {
|
|
239
|
+
console.log(theme_1.theme.yellow(" Crawl Errors:"));
|
|
240
|
+
for (const error of scanErrorsList.slice(0, 5)) {
|
|
241
|
+
console.log(theme_1.theme.gray(` - ${error.url}: ${error.error}`));
|
|
242
|
+
}
|
|
243
|
+
if (scanErrorsList.length > 5) {
|
|
244
|
+
console.log(theme_1.theme.gray(` ... and ${scanErrorsList.length - 5} more`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (pluginErrorsMap.size > 0) {
|
|
248
|
+
console.log(theme_1.theme.yellow(" Plugin Errors:"));
|
|
249
|
+
for (const [pluginName, errors] of pluginErrorsMap) {
|
|
250
|
+
console.log(theme_1.theme.gray(` ${pluginName}:`));
|
|
251
|
+
for (const error of errors.slice(0, 3)) {
|
|
252
|
+
console.log(theme_1.theme.gray(` - ${error.url}: ${error.error}`));
|
|
253
|
+
}
|
|
254
|
+
if (errors.length > 3) {
|
|
255
|
+
console.log(theme_1.theme.gray(` ... and ${errors.length - 3} more`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const totalPluginErrors = Array.from(pluginErrorsMap.values()).reduce((sum, errs) => sum + errs.length, 0);
|
|
259
|
+
if (totalPluginErrors > 10) {
|
|
260
|
+
console.log(theme_1.theme.gray(` Total plugin errors: ${totalPluginErrors}`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
91
263
|
console.log("");
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
:
|
|
102
|
-
|
|
103
|
-
:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
264
|
+
}
|
|
265
|
+
// Add "What's Next" interactive prompt
|
|
266
|
+
if (!jsonMode) {
|
|
267
|
+
const inquirer = (await Promise.resolve().then(() => __importStar(require("inquirer")))).default;
|
|
268
|
+
const { nextAction } = await inquirer.prompt([
|
|
269
|
+
{
|
|
270
|
+
type: "list",
|
|
271
|
+
name: "nextAction",
|
|
272
|
+
message: theme_1.theme.cyan("Scan complete! What would you like to do next?"),
|
|
273
|
+
choices: [
|
|
274
|
+
{ name: "🧠 Analyze findings with AI", value: "analyze" },
|
|
275
|
+
{ name: "📄 Generate a professional report", value: "report" },
|
|
276
|
+
{ name: "👋 Exit to main menu", value: "exit" }
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
]);
|
|
280
|
+
if (nextAction === "analyze") {
|
|
281
|
+
const { registerAnalyzeCommand } = await Promise.resolve().then(() => __importStar(require("./analyze")));
|
|
282
|
+
const analyzeProgram = new commander_1.Command();
|
|
283
|
+
registerAnalyzeCommand(analyzeProgram);
|
|
284
|
+
await analyzeProgram.parseAsync(["node", "kramscan", "analyze", filepath]);
|
|
108
285
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
286
|
+
else if (nextAction === "report") {
|
|
287
|
+
const { registerReportCommand } = await Promise.resolve().then(() => __importStar(require("./report")));
|
|
288
|
+
const reportProgram = new commander_1.Command();
|
|
289
|
+
registerReportCommand(reportProgram);
|
|
290
|
+
await reportProgram.parseAsync(["node", "kramscan", "report", filepath]);
|
|
112
291
|
}
|
|
113
292
|
}
|
|
114
|
-
console.log(chalk_1.default.cyan("💡 Next steps:"));
|
|
115
|
-
console.log(chalk_1.default.white(` 1. Run ${chalk_1.default.cyan(`kramscan analyze ${filepath}`)} for AI-powered insights`));
|
|
116
|
-
console.log(chalk_1.default.white(` 2. Run ${chalk_1.default.cyan(`kramscan report ${filepath}`)} to generate a report`));
|
|
117
|
-
console.log("");
|
|
118
293
|
}
|
|
119
294
|
catch (error) {
|
|
120
|
-
spinner
|
|
121
|
-
|
|
295
|
+
if (spinner)
|
|
296
|
+
spinner.fail("Scan failed");
|
|
297
|
+
if (jsonMode) {
|
|
298
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
logger_1.logger.error(error.message);
|
|
302
|
+
}
|
|
122
303
|
process.exit(1);
|
|
123
304
|
}
|
|
124
305
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
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.registerScansCommand = registerScansCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const scan_index_1 = require("../core/scan-index");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
function registerScansCommand(program) {
|
|
11
|
+
const scans = program.command("scans").description("Manage saved scans");
|
|
12
|
+
scans
|
|
13
|
+
.command("list")
|
|
14
|
+
.description("List recent scans")
|
|
15
|
+
.option("-n, --limit <number>", "How many scans to show", "20")
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
18
|
+
const entries = await (0, scan_index_1.listScans)(Number.isFinite(limit) ? limit : 20);
|
|
19
|
+
if (entries.length === 0) {
|
|
20
|
+
logger_1.logger.warn("No scans found in index yet. Run 'kramscan scan <url>' first.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(chalk_1.default.bold.cyan("Recent Scans"));
|
|
25
|
+
console.log(chalk_1.default.gray("-".repeat(60)));
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
console.log(chalk_1.default.white(entry.timestamp), chalk_1.default.gray("-"), chalk_1.default.cyan(entry.hostname));
|
|
28
|
+
console.log(chalk_1.default.gray(" JSON:"), chalk_1.default.white(entry.jsonPath));
|
|
29
|
+
if (entry.pdfPath) {
|
|
30
|
+
console.log(chalk_1.default.gray(" PDF :"), chalk_1.default.white(entry.pdfPath));
|
|
31
|
+
}
|
|
32
|
+
console.log(chalk_1.default.gray(" Findings:"), chalk_1.default.white(`${entry.summary.total} total (${entry.summary.critical}C ${entry.summary.high}H ${entry.summary.medium}M ${entry.summary.low}L ${entry.summary.info}I)`));
|
|
33
|
+
if (entry.score !== undefined) {
|
|
34
|
+
const scoreColor = entry.score > 80 ? chalk_1.default.green : (entry.score > 50 ? chalk_1.default.yellow : chalk_1.default.red);
|
|
35
|
+
console.log(chalk_1.default.gray(" Score :"), scoreColor(`${entry.score}/100`));
|
|
36
|
+
}
|
|
37
|
+
console.log("");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
scans
|
|
41
|
+
.command("latest")
|
|
42
|
+
.description("Show the latest scan paths")
|
|
43
|
+
.action(async () => {
|
|
44
|
+
const latest = await (0, scan_index_1.getLatestScan)();
|
|
45
|
+
if (!latest) {
|
|
46
|
+
logger_1.logger.warn("No scans found in index yet. Run 'kramscan scan <url>' first.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk_1.default.bold("Latest scan:"));
|
|
50
|
+
console.log(chalk_1.default.gray("Target:"), chalk_1.default.cyan(latest.target));
|
|
51
|
+
console.log(chalk_1.default.gray("Time :"), chalk_1.default.white(latest.timestamp));
|
|
52
|
+
console.log(chalk_1.default.gray("JSON :"), chalk_1.default.white(latest.jsonPath));
|
|
53
|
+
console.log(chalk_1.default.gray("PDF :"), chalk_1.default.white(latest.pdfPath || "N/A"));
|
|
54
|
+
});
|
|
55
|
+
}
|
package/dist/core/ai-client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ScanResult } from "./vulnerability-detector";
|
|
1
2
|
export interface AIResponse {
|
|
2
3
|
content: string;
|
|
3
4
|
usage?: {
|
|
@@ -7,6 +8,10 @@ export interface AIResponse {
|
|
|
7
8
|
};
|
|
8
9
|
}
|
|
9
10
|
export interface AIClient {
|
|
10
|
-
analyze(prompt: string): Promise<AIResponse>;
|
|
11
|
+
analyze(prompt: string, systemPrompt?: string): Promise<AIResponse>;
|
|
11
12
|
}
|
|
12
13
|
export declare function createAIClient(): Promise<AIClient>;
|
|
14
|
+
/**
|
|
15
|
+
* Generates a high-level executive summary of the scan results using AI.
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateExecutiveSummary(client: AIClient, result: ScanResult): Promise<string>;
|
package/dist/core/ai-client.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createAIClient = createAIClient;
|
|
7
|
+
exports.generateExecutiveSummary = generateExecutiveSummary;
|
|
7
8
|
const openai_1 = __importDefault(require("openai"));
|
|
8
9
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
9
10
|
const generative_ai_1 = require("@google/generative-ai");
|
|
@@ -17,6 +18,7 @@ function getApiKeyFromEnv(provider) {
|
|
|
17
18
|
mistral: process.env.MISTRAL_API_KEY || "",
|
|
18
19
|
openrouter: process.env.OPENROUTER_API_KEY || "",
|
|
19
20
|
kimi: process.env.KIMI_API_KEY || "",
|
|
21
|
+
groq: process.env.GROQ_API_KEY || "",
|
|
20
22
|
};
|
|
21
23
|
return envVars[provider] || "";
|
|
22
24
|
}
|
|
@@ -44,6 +46,8 @@ async function createAIClient() {
|
|
|
44
46
|
return new MistralClient(apiKey, model);
|
|
45
47
|
case "kimi":
|
|
46
48
|
return new KimiClient(apiKey, model);
|
|
49
|
+
case "groq":
|
|
50
|
+
return new GroqClient(apiKey, model);
|
|
47
51
|
default:
|
|
48
52
|
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
49
53
|
}
|
|
@@ -55,13 +59,14 @@ class OpenAIClient {
|
|
|
55
59
|
this.client = new openai_1.default({ apiKey });
|
|
56
60
|
this.model = model;
|
|
57
61
|
}
|
|
58
|
-
async analyze(prompt) {
|
|
62
|
+
async analyze(prompt, systemPrompt) {
|
|
59
63
|
const response = await this.client.chat.completions.create({
|
|
60
64
|
model: this.model,
|
|
61
65
|
messages: [
|
|
62
66
|
{
|
|
63
67
|
role: "system",
|
|
64
|
-
content:
|
|
68
|
+
content: systemPrompt ||
|
|
69
|
+
"You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
65
70
|
},
|
|
66
71
|
{ role: "user", content: prompt },
|
|
67
72
|
],
|
|
@@ -85,7 +90,7 @@ class AnthropicClient {
|
|
|
85
90
|
this.client = new sdk_1.default({ apiKey });
|
|
86
91
|
this.model = model;
|
|
87
92
|
}
|
|
88
|
-
async analyze(prompt) {
|
|
93
|
+
async analyze(prompt, systemPrompt) {
|
|
89
94
|
const response = await this.client.messages.create({
|
|
90
95
|
model: this.model,
|
|
91
96
|
max_tokens: 4096,
|
|
@@ -95,7 +100,8 @@ class AnthropicClient {
|
|
|
95
100
|
content: prompt,
|
|
96
101
|
},
|
|
97
102
|
],
|
|
98
|
-
system:
|
|
103
|
+
system: systemPrompt ||
|
|
104
|
+
"You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
99
105
|
});
|
|
100
106
|
const content = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
101
107
|
return {
|
|
@@ -115,10 +121,10 @@ class GeminiClient {
|
|
|
115
121
|
this.client = new generative_ai_1.GoogleGenerativeAI(apiKey);
|
|
116
122
|
this.model = model;
|
|
117
123
|
}
|
|
118
|
-
async analyze(prompt) {
|
|
124
|
+
async analyze(prompt, systemPrompt) {
|
|
119
125
|
const generativeModel = this.client.getGenerativeModel({ model: this.model });
|
|
120
126
|
const result = await generativeModel.generateContent([
|
|
121
|
-
"You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
127
|
+
systemPrompt || "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
122
128
|
prompt
|
|
123
129
|
]);
|
|
124
130
|
const response = await result.response;
|
|
@@ -143,13 +149,13 @@ class OpenRouterClient {
|
|
|
143
149
|
});
|
|
144
150
|
this.model = model;
|
|
145
151
|
}
|
|
146
|
-
async analyze(prompt) {
|
|
152
|
+
async analyze(prompt, systemPrompt) {
|
|
147
153
|
const response = await this.client.chat.completions.create({
|
|
148
154
|
model: this.model,
|
|
149
155
|
messages: [
|
|
150
156
|
{
|
|
151
157
|
role: "system",
|
|
152
|
-
content: "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
158
|
+
content: systemPrompt || "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
153
159
|
},
|
|
154
160
|
{ role: "user", content: prompt },
|
|
155
161
|
],
|
|
@@ -173,13 +179,13 @@ class MistralClient {
|
|
|
173
179
|
this.client = new mistralai_1.Mistral({ apiKey });
|
|
174
180
|
this.model = model;
|
|
175
181
|
}
|
|
176
|
-
async analyze(prompt) {
|
|
182
|
+
async analyze(prompt, systemPrompt) {
|
|
177
183
|
const response = await this.client.chat.complete({
|
|
178
184
|
model: this.model,
|
|
179
185
|
messages: [
|
|
180
186
|
{
|
|
181
187
|
role: "system",
|
|
182
|
-
content: "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
188
|
+
content: systemPrompt || "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
183
189
|
},
|
|
184
190
|
{ role: "user", content: prompt },
|
|
185
191
|
],
|
|
@@ -207,13 +213,13 @@ class KimiClient {
|
|
|
207
213
|
});
|
|
208
214
|
this.model = model;
|
|
209
215
|
}
|
|
210
|
-
async analyze(prompt) {
|
|
216
|
+
async analyze(prompt, systemPrompt) {
|
|
211
217
|
const response = await this.client.chat.completions.create({
|
|
212
218
|
model: this.model,
|
|
213
219
|
messages: [
|
|
214
220
|
{
|
|
215
221
|
role: "system",
|
|
216
|
-
content: "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
222
|
+
content: systemPrompt || "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
217
223
|
},
|
|
218
224
|
{ role: "user", content: prompt },
|
|
219
225
|
],
|
|
@@ -230,3 +236,65 @@ class KimiClient {
|
|
|
230
236
|
};
|
|
231
237
|
}
|
|
232
238
|
}
|
|
239
|
+
class GroqClient {
|
|
240
|
+
client;
|
|
241
|
+
model;
|
|
242
|
+
constructor(apiKey, model) {
|
|
243
|
+
this.client = new openai_1.default({
|
|
244
|
+
apiKey,
|
|
245
|
+
baseURL: "https://api.groq.com/openai/v1",
|
|
246
|
+
});
|
|
247
|
+
this.model = model;
|
|
248
|
+
}
|
|
249
|
+
async analyze(prompt, systemPrompt) {
|
|
250
|
+
const response = await this.client.chat.completions.create({
|
|
251
|
+
model: this.model,
|
|
252
|
+
messages: [
|
|
253
|
+
{
|
|
254
|
+
role: "system",
|
|
255
|
+
content: systemPrompt || "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
256
|
+
},
|
|
257
|
+
{ role: "user", content: prompt },
|
|
258
|
+
],
|
|
259
|
+
temperature: 0.3,
|
|
260
|
+
});
|
|
261
|
+
const content = response.choices[0]?.message?.content || "";
|
|
262
|
+
return {
|
|
263
|
+
content,
|
|
264
|
+
usage: {
|
|
265
|
+
promptTokens: response.usage?.prompt_tokens || 0,
|
|
266
|
+
completionTokens: response.usage?.completion_tokens || 0,
|
|
267
|
+
totalTokens: response.usage?.total_tokens || 0,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Generates a high-level executive summary of the scan results using AI.
|
|
274
|
+
*/
|
|
275
|
+
async function generateExecutiveSummary(client, result) {
|
|
276
|
+
const vulnSummary = result.vulnerabilities.map(v => `- [${v.severity.toUpperCase()}] ${v.title} on ${v.url}`).join("\n");
|
|
277
|
+
const prompt = `
|
|
278
|
+
Please provide a professional executive summary for a web security scan.
|
|
279
|
+
|
|
280
|
+
Target: ${result.target}
|
|
281
|
+
Scan Date: ${result.timestamp}
|
|
282
|
+
Total Vulnerabilities: ${result.summary.total}
|
|
283
|
+
Critical: ${result.summary.critical}
|
|
284
|
+
High: ${result.summary.high}
|
|
285
|
+
Medium: ${result.summary.medium}
|
|
286
|
+
Low: ${result.summary.low}
|
|
287
|
+
|
|
288
|
+
Detailed Findings:
|
|
289
|
+
${vulnSummary}
|
|
290
|
+
|
|
291
|
+
The summary should:
|
|
292
|
+
1. Briefly state the overall security posture.
|
|
293
|
+
2. Highlight the most critical risks.
|
|
294
|
+
3. Provide high-level recommendations for management.
|
|
295
|
+
4. Be concise (max 3-4 paragraphs).
|
|
296
|
+
`;
|
|
297
|
+
const systemPrompt = "You are a senior cybersecurity consultant writing for a non-technical executive audience. Focus on business risk and impact.";
|
|
298
|
+
const response = await client.analyze(prompt, systemPrompt);
|
|
299
|
+
return response.content;
|
|
300
|
+
}
|