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/doctor.js
CHANGED
|
@@ -40,11 +40,9 @@ exports.registerDoctorCommand = registerDoctorCommand;
|
|
|
40
40
|
const chalk_1 = __importDefault(require("chalk"));
|
|
41
41
|
const config_1 = require("../core/config");
|
|
42
42
|
const logger_1 = require("../utils/logger");
|
|
43
|
-
const
|
|
44
|
-
const util_1 = require("util");
|
|
43
|
+
const dns = __importStar(require("dns/promises"));
|
|
45
44
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
46
45
|
const os_1 = __importDefault(require("os"));
|
|
47
|
-
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
48
46
|
function registerDoctorCommand(program) {
|
|
49
47
|
program
|
|
50
48
|
.command("doctor")
|
|
@@ -123,7 +121,7 @@ async function checkNodeVersion() {
|
|
|
123
121
|
}
|
|
124
122
|
async function checkPuppeteer() {
|
|
125
123
|
try {
|
|
126
|
-
|
|
124
|
+
await Promise.resolve().then(() => __importStar(require("puppeteer")));
|
|
127
125
|
return {
|
|
128
126
|
name: "Puppeteer",
|
|
129
127
|
status: "pass",
|
|
@@ -158,6 +156,15 @@ async function checkConfig() {
|
|
|
158
156
|
async function checkAPIKeys() {
|
|
159
157
|
try {
|
|
160
158
|
const config = await (0, config_1.getConfig)();
|
|
159
|
+
const envFallback = {
|
|
160
|
+
openai: process.env.OPENAI_API_KEY,
|
|
161
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
162
|
+
gemini: process.env.GEMINI_API_KEY,
|
|
163
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
164
|
+
mistral: process.env.MISTRAL_API_KEY,
|
|
165
|
+
kimi: process.env.KIMI_API_KEY,
|
|
166
|
+
groq: process.env.GROQ_API_KEY,
|
|
167
|
+
};
|
|
161
168
|
if (!config.ai.enabled) {
|
|
162
169
|
return {
|
|
163
170
|
name: "AI Configuration",
|
|
@@ -165,7 +172,8 @@ async function checkAPIKeys() {
|
|
|
165
172
|
message: "AI analysis is disabled. Run 'kramscan onboard' to enable it.",
|
|
166
173
|
};
|
|
167
174
|
}
|
|
168
|
-
|
|
175
|
+
const apiKey = config.ai.apiKey || envFallback[config.ai.provider] || "";
|
|
176
|
+
if (!apiKey) {
|
|
169
177
|
return {
|
|
170
178
|
name: "AI Configuration",
|
|
171
179
|
status: "warn",
|
|
@@ -208,16 +216,13 @@ async function checkDiskSpace() {
|
|
|
208
216
|
}
|
|
209
217
|
async function checkNetwork() {
|
|
210
218
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
resolve(true);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
219
|
+
const timeoutMs = 3000;
|
|
220
|
+
await Promise.race([
|
|
221
|
+
dns.lookup("example.com"),
|
|
222
|
+
new Promise((_, reject) => {
|
|
223
|
+
setTimeout(() => reject(new Error("DNS lookup timeout")), timeoutMs);
|
|
224
|
+
}),
|
|
225
|
+
]);
|
|
221
226
|
return {
|
|
222
227
|
name: "Network Connectivity",
|
|
223
228
|
status: "pass",
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerGateCommand = registerGateCommand;
|
|
4
|
+
const scanner_1 = require("../core/scanner");
|
|
5
|
+
const server_probe_1 = require("../core/server-probe");
|
|
6
|
+
const theme_1 = require("../utils/theme");
|
|
7
|
+
const logger_1 = require("../utils/logger");
|
|
8
|
+
function registerGateCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("gate <url>")
|
|
11
|
+
.description("CI/CD security quality gate — scan and exit with code 1 if thresholds are breached")
|
|
12
|
+
.option("--fail-on <severity>", "Minimum severity to fail on (critical|high|medium|low)", "high")
|
|
13
|
+
.option("--max-vulns <number>", "Maximum allowed vulnerabilities before failing", "0")
|
|
14
|
+
.option("--profile <name>", "Scan profile: quick|balanced|deep", "quick")
|
|
15
|
+
.option("--timeout <ms>", "Maximum scan duration", "60000")
|
|
16
|
+
.option("--json", "Output results as JSON")
|
|
17
|
+
.action(async (url, options) => {
|
|
18
|
+
const jsonMode = options.json === true;
|
|
19
|
+
if (!jsonMode) {
|
|
20
|
+
console.log("");
|
|
21
|
+
console.log(theme_1.theme.brand.bold("🚧 KramScan Security Gate"));
|
|
22
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
23
|
+
console.log("");
|
|
24
|
+
}
|
|
25
|
+
// Probe server
|
|
26
|
+
if (!jsonMode) {
|
|
27
|
+
const probeSpinner = logger_1.logger.spinner(`Checking server at ${url}...`);
|
|
28
|
+
const probeResult = await (0, server_probe_1.probeServer)(url, { timeout: 10000, maxAttempts: 5 });
|
|
29
|
+
if (!probeResult.reachable) {
|
|
30
|
+
probeSpinner.fail(`Server at ${url} is not responding`);
|
|
31
|
+
if (jsonMode) {
|
|
32
|
+
console.log(JSON.stringify({ error: "Server unreachable", passed: false }));
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
probeSpinner.succeed(`Server ready (${probeResult.responseTime}ms)`);
|
|
37
|
+
}
|
|
38
|
+
// Run scan
|
|
39
|
+
const scanSpinner = jsonMode ? null : logger_1.logger.spinner("Running security scan...");
|
|
40
|
+
try {
|
|
41
|
+
const scanner = new scanner_1.Scanner(true);
|
|
42
|
+
const scanOptions = {
|
|
43
|
+
depth: 2,
|
|
44
|
+
timeout: parseInt(options.timeout, 10) || 60000,
|
|
45
|
+
headless: true,
|
|
46
|
+
maxPages: 20,
|
|
47
|
+
maxLinksPerPage: 30,
|
|
48
|
+
profile: options.profile,
|
|
49
|
+
};
|
|
50
|
+
const result = await scanner.scan(url, scanOptions);
|
|
51
|
+
await scanner.close();
|
|
52
|
+
if (scanSpinner)
|
|
53
|
+
scanSpinner.succeed(`Scan complete: ${result.summary.total} vulnerabilities`);
|
|
54
|
+
// Evaluate threshold
|
|
55
|
+
const severityLevels = {
|
|
56
|
+
critical: 4, high: 3, medium: 2, low: 1, info: 0,
|
|
57
|
+
};
|
|
58
|
+
const threshold = severityLevels[options.failOn.toLowerCase()] ?? 3;
|
|
59
|
+
const maxVulns = parseInt(options.maxVulns, 10) || 0;
|
|
60
|
+
const vulnsAboveThreshold = result.vulnerabilities.filter((v) => (severityLevels[v.severity] ?? 0) >= threshold);
|
|
61
|
+
const passed = vulnsAboveThreshold.length <= maxVulns;
|
|
62
|
+
if (jsonMode) {
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
passed,
|
|
65
|
+
total: result.summary.total,
|
|
66
|
+
threshold: options.failOn,
|
|
67
|
+
vulnsAboveThreshold: vulnsAboveThreshold.length,
|
|
68
|
+
maxAllowed: maxVulns,
|
|
69
|
+
summary: result.summary,
|
|
70
|
+
vulnerabilities: vulnsAboveThreshold,
|
|
71
|
+
}, null, 2));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log("");
|
|
75
|
+
if (passed) {
|
|
76
|
+
console.log(theme_1.theme.success.bold("✅ SECURITY GATE: PASSED"));
|
|
77
|
+
console.log(theme_1.theme.gray(` ${result.summary.total} total vulnerabilities found`));
|
|
78
|
+
console.log(theme_1.theme.gray(` ${vulnsAboveThreshold.length} at or above '${options.failOn}' severity (max allowed: ${maxVulns})`));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log(theme_1.theme.error.bold("❌ SECURITY GATE: FAILED"));
|
|
82
|
+
console.log(theme_1.theme.error(` ${vulnsAboveThreshold.length} vulnerabilities at or above '${options.failOn}' severity (max allowed: ${maxVulns})`));
|
|
83
|
+
console.log("");
|
|
84
|
+
for (const v of vulnsAboveThreshold.slice(0, 10)) {
|
|
85
|
+
const color = v.severity === "critical" ? theme_1.theme.critical : theme_1.theme.high;
|
|
86
|
+
console.log(color(` [${v.severity.toUpperCase()}] ${v.title}`));
|
|
87
|
+
console.log(theme_1.theme.gray(` ${v.url}`));
|
|
88
|
+
if (v.remediation) {
|
|
89
|
+
console.log(theme_1.theme.dim(` Fix: ${v.remediation}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (vulnsAboveThreshold.length > 10) {
|
|
93
|
+
console.log(theme_1.theme.gray(` ... and ${vulnsAboveThreshold.length - 10} more`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
console.log("");
|
|
97
|
+
}
|
|
98
|
+
process.exit(passed ? 0 : 1);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (scanSpinner)
|
|
102
|
+
scanSpinner.fail(`Scan failed: ${err.message}`);
|
|
103
|
+
if (jsonMode) {
|
|
104
|
+
console.log(JSON.stringify({ error: err.message, passed: false }));
|
|
105
|
+
}
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,164 +1,211 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
35
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
6
|
exports.registerOnboardCommand = registerOnboardCommand;
|
|
37
|
-
const
|
|
7
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const openai_1 = __importDefault(require("openai"));
|
|
38
10
|
const config_1 = require("../core/config");
|
|
39
11
|
const logger_1 = require("../utils/logger");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
function askConfirm(rl, question, defaultVal = true) {
|
|
62
|
-
const hint = defaultVal ? `${c.gray}(Y/n)${c.reset}` : `${c.gray}(y/N)${c.reset}`;
|
|
63
|
-
return new Promise((resolve) => {
|
|
64
|
-
rl.question(` ${c.cyan}?${c.reset} ${question} ${hint} `, (answer) => {
|
|
65
|
-
const a = answer.trim().toLowerCase();
|
|
66
|
-
if (a === "")
|
|
67
|
-
resolve(defaultVal);
|
|
68
|
-
else
|
|
69
|
-
resolve(a === "y" || a === "yes");
|
|
70
|
-
});
|
|
71
|
-
});
|
|
12
|
+
const theme_1 = require("../utils/theme");
|
|
13
|
+
function getDefaultModel(provider) {
|
|
14
|
+
switch (provider) {
|
|
15
|
+
case "anthropic":
|
|
16
|
+
return "claude-3-5-sonnet-20241022";
|
|
17
|
+
case "gemini":
|
|
18
|
+
return "gemini-2.0-flash";
|
|
19
|
+
case "openrouter":
|
|
20
|
+
return "anthropic/claude-3.5-sonnet";
|
|
21
|
+
case "mistral":
|
|
22
|
+
return "mistral-large-latest";
|
|
23
|
+
case "kimi":
|
|
24
|
+
return "moonshot-v1-8k";
|
|
25
|
+
case "groq":
|
|
26
|
+
return "llama-3.1-8b-instant";
|
|
27
|
+
default:
|
|
28
|
+
return "gpt-4";
|
|
29
|
+
}
|
|
72
30
|
}
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const trimmed = answer.trim();
|
|
85
|
-
if (choices.includes(trimmed)) {
|
|
86
|
-
resolve(trimmed);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
resolve(defaultVal || choices[0]);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
});
|
|
31
|
+
function getEnvApiKey(provider) {
|
|
32
|
+
const envVars = {
|
|
33
|
+
openai: process.env.OPENAI_API_KEY,
|
|
34
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
35
|
+
gemini: process.env.GEMINI_API_KEY,
|
|
36
|
+
mistral: process.env.MISTRAL_API_KEY,
|
|
37
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
38
|
+
kimi: process.env.KIMI_API_KEY,
|
|
39
|
+
groq: process.env.GROQ_API_KEY,
|
|
40
|
+
};
|
|
41
|
+
return envVars[provider] || "";
|
|
93
42
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
43
|
+
async function modelExists(provider, apiKey, model) {
|
|
44
|
+
if (!apiKey || !model) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (provider === "gemini") {
|
|
48
|
+
const resp = await axios_1.default.get("https://generativelanguage.googleapis.com/v1beta/models", { params: { key: apiKey } });
|
|
49
|
+
const models = resp.data?.models || [];
|
|
50
|
+
return models.some((m) => {
|
|
51
|
+
const id = m.name?.startsWith("models/")
|
|
52
|
+
? m.name.slice("models/".length)
|
|
53
|
+
: m.name;
|
|
54
|
+
return (id === model &&
|
|
55
|
+
(m.supportedGenerationMethods || []).includes("generateContent"));
|
|
98
56
|
});
|
|
99
|
-
}
|
|
57
|
+
}
|
|
58
|
+
if (provider === "openai" ||
|
|
59
|
+
provider === "openrouter" ||
|
|
60
|
+
provider === "kimi" ||
|
|
61
|
+
provider === "groq") {
|
|
62
|
+
const baseURL = provider === "openrouter"
|
|
63
|
+
? "https://openrouter.ai/api/v1"
|
|
64
|
+
: provider === "kimi"
|
|
65
|
+
? "https://api.moonshot.cn/v1"
|
|
66
|
+
: provider === "groq"
|
|
67
|
+
? "https://api.groq.com/openai/v1"
|
|
68
|
+
: undefined;
|
|
69
|
+
const client = new openai_1.default(baseURL ? { apiKey, baseURL } : { apiKey });
|
|
70
|
+
const resp = await client.models.list();
|
|
71
|
+
return (resp.data || []).some((m) => m.id === model);
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
100
74
|
}
|
|
101
|
-
// ─── Command Registration ─────────────────────────────────────────
|
|
102
75
|
function registerOnboardCommand(program) {
|
|
103
76
|
program
|
|
104
77
|
.command("onboard")
|
|
105
78
|
.description("First-time setup wizard")
|
|
106
79
|
.action(async () => {
|
|
107
|
-
const
|
|
108
|
-
const rl = readline.createInterface({
|
|
109
|
-
input: process.stdin,
|
|
110
|
-
output: process.stdout,
|
|
111
|
-
});
|
|
80
|
+
const config = await (0, config_1.getConfig)();
|
|
112
81
|
console.log("");
|
|
113
|
-
console.log(
|
|
114
|
-
console.log(
|
|
82
|
+
console.log(theme_1.theme.brand.bold("🚀 KramScan Setup Wizard"));
|
|
83
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
84
|
+
console.log(theme_1.theme.white(" Configure your scanning environment in a few easy steps."));
|
|
115
85
|
console.log("");
|
|
116
|
-
//
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
86
|
+
// Smart detection for API keys
|
|
87
|
+
const detectedProviders = Object.keys({
|
|
88
|
+
openai: process.env.OPENAI_API_KEY,
|
|
89
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
90
|
+
gemini: process.env.GEMINI_API_KEY,
|
|
91
|
+
mistral: process.env.MISTRAL_API_KEY,
|
|
92
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
93
|
+
kimi: process.env.KIMI_API_KEY,
|
|
94
|
+
groq: process.env.GROQ_API_KEY,
|
|
95
|
+
}).filter(p => !!getEnvApiKey(p));
|
|
96
|
+
if (detectedProviders.length > 0) {
|
|
97
|
+
console.log(theme_1.theme.green(` ✨ Detected API keys in environment for: ${detectedProviders.join(", ")}`));
|
|
98
|
+
console.log("");
|
|
99
|
+
}
|
|
100
|
+
const answers = await inquirer_1.default.prompt([
|
|
101
|
+
{
|
|
102
|
+
type: "confirm",
|
|
103
|
+
name: "aiEnabled",
|
|
104
|
+
message: "Enable AI analysis?",
|
|
105
|
+
default: config.ai.enabled,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "list",
|
|
109
|
+
name: "aiProvider",
|
|
110
|
+
message: "Select AI provider",
|
|
111
|
+
choices: [
|
|
112
|
+
"openai",
|
|
113
|
+
"anthropic",
|
|
114
|
+
"gemini",
|
|
115
|
+
"openrouter",
|
|
116
|
+
"mistral",
|
|
117
|
+
"kimi",
|
|
118
|
+
"groq",
|
|
119
|
+
],
|
|
120
|
+
default: config.ai.provider,
|
|
121
|
+
when: (a) => a.aiEnabled,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: "password",
|
|
125
|
+
name: "apiKey",
|
|
126
|
+
message: "API key (leave blank to keep existing)",
|
|
127
|
+
default: "",
|
|
128
|
+
mask: "*",
|
|
129
|
+
when: (a) => a.aiEnabled,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "input",
|
|
133
|
+
name: "model",
|
|
134
|
+
message: "Default AI model",
|
|
135
|
+
default: (a) => config.ai.defaultModel ||
|
|
136
|
+
getDefaultModel((a.aiProvider || config.ai.provider)),
|
|
137
|
+
when: (a) => a.aiEnabled,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: "list",
|
|
141
|
+
name: "reportFormat",
|
|
142
|
+
message: "Default report format",
|
|
143
|
+
choices: ["word", "txt", "json"],
|
|
144
|
+
default: config.report.defaultFormat,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "confirm",
|
|
148
|
+
name: "strictScope",
|
|
149
|
+
message: "Enable strict scope enforcement?",
|
|
150
|
+
default: config.scan.strictScope,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: "number",
|
|
154
|
+
name: "rateLimit",
|
|
155
|
+
message: "Requests per second rate limit",
|
|
156
|
+
default: config.scan.rateLimitPerSecond,
|
|
157
|
+
validate: (v) => Number.isFinite(v) && v > 0 ? true : "Enter a positive number",
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
config.ai.enabled = !!answers.aiEnabled;
|
|
161
|
+
if (config.ai.enabled) {
|
|
162
|
+
config.ai.provider = answers.aiProvider;
|
|
163
|
+
if (answers.apiKey) {
|
|
164
|
+
config.ai.apiKey = answers.apiKey;
|
|
165
|
+
}
|
|
166
|
+
const keyForCheck = config.ai.apiKey || getEnvApiKey(config.ai.provider);
|
|
167
|
+
let chosenModel = String(answers.model || getDefaultModel(config.ai.provider));
|
|
168
|
+
if (keyForCheck) {
|
|
169
|
+
try {
|
|
170
|
+
const ok = await modelExists(config.ai.provider, keyForCheck, chosenModel);
|
|
171
|
+
if (!ok) {
|
|
172
|
+
logger_1.logger.warn(`Model '${chosenModel}' is not available for provider '${config.ai.provider}'.`);
|
|
173
|
+
logger_1.logger.warn("Tip: run 'kramscan ai models' to see valid models.");
|
|
174
|
+
const retry = await inquirer_1.default.prompt([
|
|
175
|
+
{
|
|
176
|
+
type: "confirm",
|
|
177
|
+
name: "retry",
|
|
178
|
+
message: "Enter a different model now?",
|
|
179
|
+
default: true,
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
if (retry.retry) {
|
|
183
|
+
const modelAns = await inquirer_1.default.prompt([
|
|
184
|
+
{
|
|
185
|
+
type: "input",
|
|
186
|
+
name: "model",
|
|
187
|
+
message: "Default AI model",
|
|
188
|
+
default: getDefaultModel(config.ai.provider),
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
chosenModel = String(modelAns.model || chosenModel);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
logger_1.logger.warn(`Model preflight check failed: ${error.message}`);
|
|
197
|
+
}
|
|
132
198
|
}
|
|
133
|
-
|
|
134
|
-
let defaultModel = "gpt-4";
|
|
135
|
-
if (aiProvider === "anthropic")
|
|
136
|
-
defaultModel = "claude-3-5-sonnet-20241022";
|
|
137
|
-
else if (aiProvider === "gemini")
|
|
138
|
-
defaultModel = "gemini-2.0-flash-exp";
|
|
139
|
-
else if (aiProvider === "openrouter")
|
|
140
|
-
defaultModel = "anthropic/claude-3.5-sonnet";
|
|
141
|
-
else if (aiProvider === "mistral")
|
|
142
|
-
defaultModel = "mistral-large-latest";
|
|
143
|
-
else if (aiProvider === "kimi")
|
|
144
|
-
defaultModel = "moonshot-v1-8k";
|
|
145
|
-
const model = await ask(rl, "Default AI model", defaultModel);
|
|
146
|
-
store.set("ai.defaultModel", model);
|
|
199
|
+
config.ai.defaultModel = chosenModel;
|
|
147
200
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const strictScope = await askConfirm(rl, "Enable strict scope enforcement?", true);
|
|
153
|
-
store.set("scan.strictScope", strictScope);
|
|
154
|
-
const rateLimitStr = await ask(rl, "Requests per second rate limit", "5");
|
|
155
|
-
const rateLimit = parseInt(rateLimitStr, 10) || 5;
|
|
156
|
-
store.set("scan.rateLimitPerSecond", rateLimit);
|
|
157
|
-
rl.close();
|
|
201
|
+
config.report.defaultFormat = answers.reportFormat;
|
|
202
|
+
config.scan.strictScope = !!answers.strictScope;
|
|
203
|
+
config.scan.rateLimitPerSecond = answers.rateLimit;
|
|
204
|
+
await (0, config_1.setConfig)(config);
|
|
158
205
|
console.log("");
|
|
159
206
|
logger_1.logger.success("Onboarding complete! Your configuration has been saved.");
|
|
160
|
-
console.log(
|
|
161
|
-
console.log(
|
|
207
|
+
console.log("Config location: ~/.kramscan/config.json");
|
|
208
|
+
console.log("Run 'kramscan' to get started.");
|
|
162
209
|
console.log("");
|
|
163
210
|
});
|
|
164
211
|
}
|