kramscan 0.2.0 → 0.3.1
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 +81 -54
- package/dist/cli.js +8 -1
- package/dist/commands/config.js +2 -2
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +239 -0
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +112 -0
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/report.js +89 -11
- package/dist/commands/scan.js +11 -0
- package/dist/commands/scans.js +4 -0
- package/dist/core/config-schema.js +1 -1
- package/dist/core/config.js +3 -3
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/scan-index.d.ts +1 -0
- package/dist/core/scanner.js +7 -1
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +6 -0
- package/dist/core/vulnerability-detector.js +21 -0
- package/dist/index.js +14 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.js +11 -1
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -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/reports/PdfGenerator.js +26 -1
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.js +7 -1
- package/package.json +8 -3
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -42,15 +42,17 @@ Web security is complex and often fragmented. Developers rely on multiple disjoi
|
|
|
42
42
|
## ✨ Key Features
|
|
43
43
|
| Feature | Description |
|
|
44
44
|
| :--- | :--- |
|
|
45
|
-
| 🔍 **Automated Vulnerability Engine** | Detects XSS, SQL Injection, CSRF, insecure headers,
|
|
46
|
-
| 🔌 **
|
|
45
|
+
| 🔍 **Automated Vulnerability Engine** | Detects XSS, SQL Injection, CSRF, insecure headers, CORS misconfigs, open redirects, and more. |
|
|
46
|
+
| 🔌 **10 Built-in Security Plugins** | CORS, debug endpoints, directory traversal, cookie auditing, open redirects, sensitive data, and more. |
|
|
47
|
+
| 🛠️ **Dev Mode (Watch Scanner)** | Watch your localhost for file changes and auto re-scan with diff reports (new vs resolved vulns). |
|
|
48
|
+
| 🚧 **CI/CD Security Gate** | `kramscan gate` exits with code 1 if vulnerabilities exceed your threshold. Plug into any pipeline. |
|
|
47
49
|
| 🤖 **Interactive AI Agent** | A conversational security assistant with **Autonomous Verification** skills to confirm findings live. |
|
|
48
50
|
| 🧠 **Multi-Provider AI Analysis** | Supports OpenAI, Anthropic, Google Gemini, Mistral, OpenRouter, and more for results auditing. |
|
|
49
51
|
| 📝 **AI Executive Summaries** | Automatically generates business-oriented summaries for Word, JSON, and TXT reports. |
|
|
50
52
|
| 📊 **Event-Driven Feedback** | Real-time progress updates with dynamic spinners and live vulnerability alerts during scanning. |
|
|
51
53
|
| 📄 **Professional Reporting** | Generates detailed PDF, DOCX, TXT, and JSON reports with remediation steps and error tracking. |
|
|
52
54
|
| 🌐 **Headless Browser Testing** | Renders modern SPAs (Single Page Applications) to find vulnerabilities in dynamic content. |
|
|
53
|
-
| ⚡ **Smarter User Flow** |
|
|
55
|
+
| ⚡ **Smarter User Flow** | Interactive menu and post-scan "Golden Path" prompts for a guided experience. |
|
|
54
56
|
| 🛡️ **Error Resilience** | Robust configuration defaults and graceful recovery if individual URLs or plugins fail. |
|
|
55
57
|
|
|
56
58
|
<br />
|
|
@@ -82,19 +84,24 @@ graph LR
|
|
|
82
84
|
|
|
83
85
|
### Plugin Architecture
|
|
84
86
|
|
|
85
|
-
KramScan
|
|
87
|
+
KramScan is built on a modular plugin system that makes extending vulnerability detection effortless:
|
|
86
88
|
|
|
87
89
|
```
|
|
88
90
|
src/plugins/
|
|
89
|
-
├── types.ts
|
|
90
|
-
├── PluginManager.ts
|
|
91
|
-
├── index.ts
|
|
92
|
-
└── vulnerabilities/
|
|
93
|
-
├── XSSPlugin.ts
|
|
94
|
-
├── SQLInjectionPlugin.ts
|
|
95
|
-
├── SecurityHeadersPlugin.ts
|
|
96
|
-
├── SensitiveDataPlugin.ts
|
|
97
|
-
|
|
91
|
+
├── types.ts # Base interfaces and types
|
|
92
|
+
├── PluginManager.ts # Plugin orchestration
|
|
93
|
+
├── index.ts # Plugin exports
|
|
94
|
+
└── vulnerabilities/ # Built-in plugins
|
|
95
|
+
├── XSSPlugin.ts # Cross-Site Scripting
|
|
96
|
+
├── SQLInjectionPlugin.ts # SQL Injection
|
|
97
|
+
├── SecurityHeadersPlugin.ts # Missing security headers
|
|
98
|
+
├── SensitiveDataPlugin.ts # Exposed secrets & API keys
|
|
99
|
+
├── CSRFPlugin.ts # Cross-Site Request Forgery
|
|
100
|
+
├── CORSAnalyzerPlugin.ts # CORS misconfiguration
|
|
101
|
+
├── DebugEndpointPlugin.ts # Exposed debug/dev endpoints
|
|
102
|
+
├── DirectoryTraversalPlugin.ts # Path traversal / LFI
|
|
103
|
+
├── CookieSecurityPlugin.ts # Insecure cookie flags
|
|
104
|
+
└── OpenRedirectPlugin.ts # Open redirect detection
|
|
98
105
|
```
|
|
99
106
|
|
|
100
107
|
**Creating a custom plugin:**
|
|
@@ -183,10 +190,10 @@ You can provide API keys via environment variables (useful for CI/CD) instead of
|
|
|
183
190
|
KramScan automatically detects API keys in your environment variables. During `kramscan onboard`, the tool will identify and pre-configure providers like OpenAI, Anthropic, and Gemini if their keys are found in your session.
|
|
184
191
|
|
|
185
192
|
### AI-Powered Context-Aware Payloads
|
|
186
|
-
The scanning engine
|
|
193
|
+
The scanning engine utilizes AI to generate payloads tailored to the specific context of your application, significantly increasing detection rates against filtered inputs and complex WAFs.
|
|
187
194
|
|
|
188
195
|
### Autonomous Finding Verification
|
|
189
|
-
The `kramscan agent`
|
|
196
|
+
The `kramscan agent` independently verifies reported vulnerabilities using non-destructive, context-aware payloads to differentiate between theoretical findings and exploitable risks.
|
|
190
197
|
|
|
191
198
|
<br />
|
|
192
199
|
|
|
@@ -225,18 +232,64 @@ kramscan scan https://example.com
|
|
|
225
232
|
|
|
226
233
|
## 📖 Usage & Commands
|
|
227
234
|
|
|
228
|
-
| Command | Description |
|
|
229
|
-
| :--- | :--- |
|
|
230
|
-
| `kramscan` | Launch the interactive dashboard menu with smart argument prompting. |
|
|
231
|
-
| `kramscan scan <url>` | Run a comprehensive vulnerability scan with post-scan prompts. |
|
|
232
|
-
| `kramscan
|
|
233
|
-
| `kramscan
|
|
234
|
-
| `kramscan
|
|
235
|
-
| `kramscan
|
|
236
|
-
| `kramscan
|
|
237
|
-
| `kramscan
|
|
238
|
-
| `kramscan
|
|
239
|
-
| `kramscan
|
|
235
|
+
| Command | Description |
|
|
236
|
+
| :--- | :--- |
|
|
237
|
+
| `kramscan` | Launch the interactive dashboard menu with smart argument prompting. |
|
|
238
|
+
| `kramscan scan <url>` | Run a comprehensive vulnerability scan with post-scan prompts. |
|
|
239
|
+
| `kramscan dev [url]` | Watch-mode localhost scanner with diff reports and desktop notifications. |
|
|
240
|
+
| `kramscan gate <url>` | CI/CD security quality gate — exits with code 1 on threshold breach. |
|
|
241
|
+
| `kramscan agent` | Start the AI security assistant with autonomous verification skills. |
|
|
242
|
+
| `kramscan analyze` | AI-powered analysis with proactive onboarding redirection. |
|
|
243
|
+
| `kramscan report` | Generate professional reports with optional AI executive summaries. |
|
|
244
|
+
| `kramscan onboard` | Smart setup wizard with environment key detection. |
|
|
245
|
+
| `kramscan doctor` | Verify environment health and check for Docker dependencies. |
|
|
246
|
+
| `kramscan config` | View and edit current configuration with robust schema defaults. |
|
|
247
|
+
| `kramscan scans` | List and inspect recent scans from the persistent index. |
|
|
248
|
+
| `kramscan ai` | AI helpers (model listing and connectivity test). |
|
|
249
|
+
|
|
250
|
+
<br />
|
|
251
|
+
|
|
252
|
+
### 🛠️ Dev Mode — Localhost Watch Scanner
|
|
253
|
+
|
|
254
|
+
Scan your local dev server continuously. KramScan watches for file changes and **auto re-scans**, showing a diff of new vs. resolved vulnerabilities:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Watch-mode with port shorthand
|
|
258
|
+
kramscan dev --port 3000
|
|
259
|
+
|
|
260
|
+
# Full URL with notifications
|
|
261
|
+
kramscan dev http://localhost:3000 --watch-dir ./src --notify
|
|
262
|
+
|
|
263
|
+
# Single scan (no watching)
|
|
264
|
+
kramscan dev http://localhost:8080 --no-watch --fail-on high
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**How it works:**
|
|
268
|
+
1. Probes your server until it's ready (auto-detects Express, Next.js, Django, etc.)
|
|
269
|
+
2. Runs an initial security scan
|
|
270
|
+
3. Watches `--watch-dir` for file changes (debounced)
|
|
271
|
+
4. Re-scans and shows only **new** and **resolved** vulnerabilities
|
|
272
|
+
|
|
273
|
+
### 🚧 CI/CD Security Gate
|
|
274
|
+
|
|
275
|
+
Block deployments with vulnerabilities above your threshold:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Fail if any high+ vulnerabilities found
|
|
279
|
+
kramscan gate http://localhost:3000 --fail-on high
|
|
280
|
+
|
|
281
|
+
# JSON output for pipeline processing
|
|
282
|
+
kramscan gate $APP_URL --fail-on medium --json
|
|
283
|
+
|
|
284
|
+
# Allow up to 3 low-severity findings
|
|
285
|
+
kramscan gate http://staging.example.com --fail-on low --max-vulns 3
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Pipeline example (GitHub Actions):**
|
|
289
|
+
```yaml
|
|
290
|
+
- name: Security Gate
|
|
291
|
+
run: npx kramscan gate http://localhost:3000 --fail-on high
|
|
292
|
+
```
|
|
240
293
|
|
|
241
294
|
<br />
|
|
242
295
|
|
|
@@ -272,7 +325,7 @@ kramscan scan https://example.com --no-pdf
|
|
|
272
325
|
```
|
|
273
326
|
|
|
274
327
|
### Error Tracking and Recovery
|
|
275
|
-
KramScan
|
|
328
|
+
KramScan features comprehensive error handling:
|
|
276
329
|
|
|
277
330
|
- **Continue on Failure**: Scan continues even if individual URLs fail to load
|
|
278
331
|
- **Plugin Error Isolation**: If one vulnerability plugin fails, others continue working
|
|
@@ -327,33 +380,7 @@ Agent: Scan complete! Found 2 High severity issues.
|
|
|
327
380
|
|
|
328
381
|
---
|
|
329
382
|
|
|
330
|
-
<br />
|
|
331
383
|
|
|
332
|
-
## 🗺️ Roadmap
|
|
333
|
-
|
|
334
|
-
- [x] Core vulnerability scanner (XSS, SQLi, CSRF, headers)
|
|
335
|
-
- [x] Multi-provider AI analysis engine
|
|
336
|
-
- [x] Interactive AI agent mode
|
|
337
|
-
- [x] Professional report generation (DOCX, TXT, JSON)
|
|
338
|
-
- [x] Configuration wizard & management
|
|
339
|
-
- [x] **Plugin system for custom scan modules** ✅
|
|
340
|
-
- [x] **PDF report generation** ✅
|
|
341
|
-
- [x] **Event-driven progress feedback** ✅
|
|
342
|
-
- [x] **Error resilience and recovery** ✅
|
|
343
|
-
- [x] **Zod schema validation** ✅
|
|
344
|
-
- [x] **AI Executive Summaries** ✅
|
|
345
|
-
- [x] **Autonomous Verification Agent** ✅
|
|
346
|
-
- [x] **Smarter Interactive Flows** ✅
|
|
347
|
-
- [ ] CI/CD integration (GitHub Actions, GitLab CI)
|
|
348
|
-
- [ ] Web-based dashboard UI
|
|
349
|
-
- [ ] SARIF export format
|
|
350
|
-
- [ ] OWASP ZAP integration
|
|
351
|
-
|
|
352
|
-
<br />
|
|
353
|
-
|
|
354
|
-
---
|
|
355
|
-
|
|
356
|
-
<br />
|
|
357
384
|
|
|
358
385
|
## 🔒 Security & Privacy
|
|
359
386
|
- **Local Execution:** All scanning logic runs locally on your machine.
|
package/dist/cli.js
CHANGED
|
@@ -51,6 +51,8 @@ const doctor_1 = require("./commands/doctor");
|
|
|
51
51
|
const agent_1 = require("./commands/agent");
|
|
52
52
|
const scans_1 = require("./commands/scans");
|
|
53
53
|
const ai_1 = require("./commands/ai");
|
|
54
|
+
const dev_1 = require("./commands/dev");
|
|
55
|
+
const gate_1 = require("./commands/gate");
|
|
54
56
|
const config_2 = require("./core/config");
|
|
55
57
|
const theme_1 = require("./utils/theme");
|
|
56
58
|
let verboseMode = false;
|
|
@@ -70,6 +72,8 @@ const menuChoices = [
|
|
|
70
72
|
{ label: "Agent", value: "agent", description: "AI-powered interactive security assistant", icon: "🤖", status: "active" },
|
|
71
73
|
{ label: "Onboard", value: "onboard", description: "First-time setup wizard", icon: "⚡", status: "active" },
|
|
72
74
|
{ label: "Scan", value: "scan", description: "Scan a target URL for vulnerabilities", icon: "🔍", status: "active" },
|
|
75
|
+
{ label: "Dev", value: "dev", description: "Watch-mode scanning for localhost dev servers", icon: "🛠️", status: "active" },
|
|
76
|
+
{ label: "Gate", value: "gate", description: "CI/CD security quality gate", icon: "🚧", status: "active" },
|
|
73
77
|
{ label: "Analyze", value: "analyze", description: "Deep AI analysis of scan results", icon: "🧠", status: "active" },
|
|
74
78
|
{ label: "Report", value: "report", description: "Generate a professional report", icon: "📄", status: "active" },
|
|
75
79
|
{ label: "Config", value: "config", description: "View or edit your configuration", icon: "⚙️", status: "active" },
|
|
@@ -113,7 +117,8 @@ async function showInteractiveMenu() {
|
|
|
113
117
|
message: theme_1.theme.cyan("Enter the URL to scan:"),
|
|
114
118
|
validate: (input) => {
|
|
115
119
|
try {
|
|
116
|
-
|
|
120
|
+
const urlToTest = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
|
121
|
+
new URL(urlToTest);
|
|
117
122
|
return true;
|
|
118
123
|
}
|
|
119
124
|
catch {
|
|
@@ -220,6 +225,8 @@ function createProgram() {
|
|
|
220
225
|
(0, agent_1.registerAgentCommand)(program);
|
|
221
226
|
(0, scans_1.registerScansCommand)(program);
|
|
222
227
|
(0, ai_1.registerAiCommand)(program);
|
|
228
|
+
(0, dev_1.registerDevCommand)(program);
|
|
229
|
+
(0, gate_1.registerGateCommand)(program);
|
|
223
230
|
// Version subcommand with detailed environment info
|
|
224
231
|
program
|
|
225
232
|
.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",
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
let 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
|
+
if (!/^https?:\/\//i.test(targetUrl)) {
|
|
38
|
+
targetUrl = `http://${targetUrl}`;
|
|
39
|
+
}
|
|
40
|
+
const isLocal = (0, server_probe_1.isLocalhost)(targetUrl);
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(theme_1.theme.brand.bold("🛠️ KramScan Dev Mode"));
|
|
43
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log(theme_1.theme.white("Target:"), theme_1.theme.cyan(targetUrl));
|
|
46
|
+
console.log(theme_1.theme.white("Profile:"), theme_1.theme.cyan(options.profile));
|
|
47
|
+
if (isLocal) {
|
|
48
|
+
console.log(theme_1.theme.white("Environment:"), theme_1.theme.green("localhost (dev mode)"));
|
|
49
|
+
}
|
|
50
|
+
console.log("");
|
|
51
|
+
// Probe server readiness
|
|
52
|
+
const probeSpinner = logger_1.logger.spinner(`Waiting for ${targetUrl} to be ready...`);
|
|
53
|
+
const probeResult = await (0, server_probe_1.probeServer)(targetUrl, { timeout: 30000 });
|
|
54
|
+
if (!probeResult.reachable) {
|
|
55
|
+
probeSpinner.fail(`Server at ${targetUrl} is not responding`);
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(theme_1.theme.warning("⚠️ Make sure your dev server is running:"));
|
|
58
|
+
console.log(theme_1.theme.gray(" • npm run dev"));
|
|
59
|
+
console.log(theme_1.theme.gray(" • yarn dev"));
|
|
60
|
+
console.log(theme_1.theme.gray(" • python manage.py runserver"));
|
|
61
|
+
console.log("");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
probeSpinner.succeed(`Server ready! (${probeResult.responseTime}ms` +
|
|
65
|
+
`${probeResult.framework ? `, ${probeResult.framework}` : ""}` +
|
|
66
|
+
`${probeResult.server ? `, ${probeResult.server}` : ""})`);
|
|
67
|
+
// Run initial scan
|
|
68
|
+
let previousResult = null;
|
|
69
|
+
const runScan = async (isRescan = false) => {
|
|
70
|
+
const scanSpinner = logger_1.logger.spinner(isRescan ? "Re-scanning after code change..." : "Running initial security scan...");
|
|
71
|
+
try {
|
|
72
|
+
const scanner = new scanner_1.Scanner(true);
|
|
73
|
+
const scanOptions = {
|
|
74
|
+
depth: 2,
|
|
75
|
+
timeout: 10000,
|
|
76
|
+
headless: true,
|
|
77
|
+
maxPages: 15,
|
|
78
|
+
maxLinksPerPage: 30,
|
|
79
|
+
profile: options.profile,
|
|
80
|
+
};
|
|
81
|
+
const result = await scanner.scan(targetUrl, scanOptions);
|
|
82
|
+
scanSpinner.succeed(isRescan
|
|
83
|
+
? `Re-scan complete: ${result.summary.total} vulnerabilities`
|
|
84
|
+
: `Initial scan complete: ${result.summary.total} vulnerabilities`);
|
|
85
|
+
await scanner.close();
|
|
86
|
+
if (isRescan && previousResult) {
|
|
87
|
+
// Show diff
|
|
88
|
+
const diff = (0, diff_engine_1.diffScanResults)(previousResult, result);
|
|
89
|
+
displayDiff(diff);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Show full summary for initial scan
|
|
93
|
+
(0, theme_1.displayScanSummary)({
|
|
94
|
+
target: result.target,
|
|
95
|
+
duration: result.duration,
|
|
96
|
+
metadata: result.metadata,
|
|
97
|
+
summary: result.summary,
|
|
98
|
+
vulnerabilities: result.vulnerabilities,
|
|
99
|
+
score: result.score,
|
|
100
|
+
filepath: "(dev mode — results in memory)",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Desktop notification for critical/high findings
|
|
104
|
+
if (options.notify && result.summary.critical + result.summary.high > 0) {
|
|
105
|
+
try {
|
|
106
|
+
// node-notifier is optional — skip if not installed
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
108
|
+
const notifier = require("node-notifier");
|
|
109
|
+
notifier.notify({
|
|
110
|
+
title: "⚠️ KramScan Security Alert",
|
|
111
|
+
message: `Found ${result.summary.critical} critical, ${result.summary.high} high vulnerabilities on ${targetUrl}`,
|
|
112
|
+
sound: true,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// node-notifier not installed, skip silently
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Check fail threshold
|
|
120
|
+
if (options.failOn) {
|
|
121
|
+
const shouldFail = checkThreshold(result, options.failOn);
|
|
122
|
+
if (shouldFail && !options.watch) {
|
|
123
|
+
console.log(theme_1.theme.error(`\n✗ Security gate failed: found vulnerabilities at or above '${options.failOn}' severity.\n`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
scanSpinner.fail(`Scan failed: ${err.message}`);
|
|
131
|
+
return previousResult;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
// Initial scan
|
|
135
|
+
previousResult = await runScan(false);
|
|
136
|
+
// Watch mode
|
|
137
|
+
if (options.watch !== false) {
|
|
138
|
+
const watchDir = path_1.default.resolve(options.watchDir);
|
|
139
|
+
try {
|
|
140
|
+
await promises_1.default.access(watchDir);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
console.log(theme_1.theme.warning(`⚠️ Watch directory not found: ${watchDir}`));
|
|
144
|
+
console.log(theme_1.theme.gray(" Falling back to current directory."));
|
|
145
|
+
}
|
|
146
|
+
console.log("");
|
|
147
|
+
console.log(theme_1.theme.brand("👁️ Watching for changes..."));
|
|
148
|
+
console.log(theme_1.theme.gray(` Directory: ${watchDir}`));
|
|
149
|
+
console.log(theme_1.theme.gray(` Debounce: ${options.debounce}ms`));
|
|
150
|
+
console.log(theme_1.theme.gray(" Press Ctrl+C to stop."));
|
|
151
|
+
console.log("");
|
|
152
|
+
// Use fs.watch with recursive option (Node.js 19+)
|
|
153
|
+
let debounceTimer = null;
|
|
154
|
+
let scanning = false;
|
|
155
|
+
try {
|
|
156
|
+
const watcher = promises_1.default.watch(watchDir, { recursive: true });
|
|
157
|
+
for await (const event of watcher) {
|
|
158
|
+
if (scanning)
|
|
159
|
+
continue;
|
|
160
|
+
// Ignore node_modules, dist, .git, etc.
|
|
161
|
+
const filename = event.filename || "";
|
|
162
|
+
if (filename.includes("node_modules") ||
|
|
163
|
+
filename.includes("dist") ||
|
|
164
|
+
filename.includes(".git") ||
|
|
165
|
+
filename.includes(".next") ||
|
|
166
|
+
filename.includes("__pycache__")) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (debounceTimer)
|
|
170
|
+
clearTimeout(debounceTimer);
|
|
171
|
+
debounceTimer = setTimeout(async () => {
|
|
172
|
+
scanning = true;
|
|
173
|
+
console.log("");
|
|
174
|
+
console.log(theme_1.theme.dim(`📝 Change detected: ${filename}`));
|
|
175
|
+
previousResult = await runScan(true);
|
|
176
|
+
scanning = false;
|
|
177
|
+
}, parseInt(options.debounce, 10));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.log(theme_1.theme.warning(`⚠️ File watching failed: ${err.message}`));
|
|
182
|
+
console.log(theme_1.theme.gray(" Make sure the watch directory exists and is readable."));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function displayDiff(diff) {
|
|
188
|
+
console.log("");
|
|
189
|
+
console.log(theme_1.theme.brightWhite.bold("🔄 Scan Diff Report"));
|
|
190
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
191
|
+
if (diff.newVulnerabilities.length === 0 && diff.resolvedVulnerabilities.length === 0) {
|
|
192
|
+
console.log(theme_1.theme.gray(" No changes since last scan."));
|
|
193
|
+
console.log(theme_1.theme.gray(` ${diff.unchangedCount} vulnerabilities unchanged.`));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// New vulnerabilities
|
|
197
|
+
if (diff.newVulnerabilities.length > 0) {
|
|
198
|
+
console.log("");
|
|
199
|
+
console.log(theme_1.theme.error(` 🆕 ${diff.newVulnerabilities.length} New Vulnerabilities`));
|
|
200
|
+
for (const v of diff.newVulnerabilities) {
|
|
201
|
+
const color = (0, theme_2.getSeverityColor)(v.severity);
|
|
202
|
+
console.log(color(` [${v.severity.toUpperCase()}] ${v.title}`));
|
|
203
|
+
console.log(theme_1.theme.gray(` ${v.url}`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Resolved vulnerabilities
|
|
207
|
+
if (diff.resolvedVulnerabilities.length > 0) {
|
|
208
|
+
console.log("");
|
|
209
|
+
console.log(theme_1.theme.success(` ✅ ${diff.resolvedVulnerabilities.length} Resolved`));
|
|
210
|
+
for (const v of diff.resolvedVulnerabilities) {
|
|
211
|
+
console.log(theme_1.theme.green(` ✓ ${v.title}`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
console.log("");
|
|
215
|
+
console.log(theme_1.theme.gray(` Total: ${diff.previousTotal} → ${diff.currentTotal} `) +
|
|
216
|
+
(diff.currentTotal < diff.previousTotal
|
|
217
|
+
? theme_1.theme.green(`(↓ ${diff.previousTotal - diff.currentTotal})`)
|
|
218
|
+
: diff.currentTotal > diff.previousTotal
|
|
219
|
+
? theme_1.theme.error(`(↑ ${diff.currentTotal - diff.previousTotal})`)
|
|
220
|
+
: theme_1.theme.gray("(no change)")));
|
|
221
|
+
}
|
|
222
|
+
console.log("");
|
|
223
|
+
}
|
|
224
|
+
function checkThreshold(result, failOn) {
|
|
225
|
+
const severityLevels = {
|
|
226
|
+
critical: 4,
|
|
227
|
+
high: 3,
|
|
228
|
+
medium: 2,
|
|
229
|
+
low: 1,
|
|
230
|
+
info: 0,
|
|
231
|
+
};
|
|
232
|
+
const threshold = severityLevels[failOn.toLowerCase()] ?? 3;
|
|
233
|
+
for (const v of result.vulnerabilities) {
|
|
234
|
+
if ((severityLevels[v.severity] ?? 0) >= threshold) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
19
|
+
url = `http://${url}`;
|
|
20
|
+
}
|
|
21
|
+
const jsonMode = options.json === true;
|
|
22
|
+
if (!jsonMode) {
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(theme_1.theme.brand.bold("🚧 KramScan Security Gate"));
|
|
25
|
+
console.log(theme_1.theme.gray("─".repeat(50)));
|
|
26
|
+
console.log("");
|
|
27
|
+
}
|
|
28
|
+
// Probe server
|
|
29
|
+
if (!jsonMode) {
|
|
30
|
+
const probeSpinner = logger_1.logger.spinner(`Checking server at ${url}...`);
|
|
31
|
+
const probeResult = await (0, server_probe_1.probeServer)(url, { timeout: 10000, maxAttempts: 5 });
|
|
32
|
+
if (!probeResult.reachable) {
|
|
33
|
+
probeSpinner.fail(`Server at ${url} is not responding`);
|
|
34
|
+
if (jsonMode) {
|
|
35
|
+
console.log(JSON.stringify({ error: "Server unreachable", passed: false }));
|
|
36
|
+
}
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
probeSpinner.succeed(`Server ready (${probeResult.responseTime}ms)`);
|
|
40
|
+
}
|
|
41
|
+
// Run scan
|
|
42
|
+
const scanSpinner = jsonMode ? null : logger_1.logger.spinner("Running security scan...");
|
|
43
|
+
try {
|
|
44
|
+
const scanner = new scanner_1.Scanner(true);
|
|
45
|
+
const scanOptions = {
|
|
46
|
+
depth: 2,
|
|
47
|
+
timeout: parseInt(options.timeout, 10) || 60000,
|
|
48
|
+
headless: true,
|
|
49
|
+
maxPages: 20,
|
|
50
|
+
maxLinksPerPage: 30,
|
|
51
|
+
profile: options.profile,
|
|
52
|
+
};
|
|
53
|
+
const result = await scanner.scan(url, scanOptions);
|
|
54
|
+
await scanner.close();
|
|
55
|
+
if (scanSpinner)
|
|
56
|
+
scanSpinner.succeed(`Scan complete: ${result.summary.total} vulnerabilities`);
|
|
57
|
+
// Evaluate threshold
|
|
58
|
+
const severityLevels = {
|
|
59
|
+
critical: 4, high: 3, medium: 2, low: 1, info: 0,
|
|
60
|
+
};
|
|
61
|
+
const threshold = severityLevels[options.failOn.toLowerCase()] ?? 3;
|
|
62
|
+
const maxVulns = parseInt(options.maxVulns, 10) || 0;
|
|
63
|
+
const vulnsAboveThreshold = result.vulnerabilities.filter((v) => (severityLevels[v.severity] ?? 0) >= threshold);
|
|
64
|
+
const passed = vulnsAboveThreshold.length <= maxVulns;
|
|
65
|
+
if (jsonMode) {
|
|
66
|
+
console.log(JSON.stringify({
|
|
67
|
+
passed,
|
|
68
|
+
total: result.summary.total,
|
|
69
|
+
threshold: options.failOn,
|
|
70
|
+
vulnsAboveThreshold: vulnsAboveThreshold.length,
|
|
71
|
+
maxAllowed: maxVulns,
|
|
72
|
+
summary: result.summary,
|
|
73
|
+
vulnerabilities: vulnsAboveThreshold,
|
|
74
|
+
}, null, 2));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log("");
|
|
78
|
+
if (passed) {
|
|
79
|
+
console.log(theme_1.theme.success.bold("✅ SECURITY GATE: PASSED"));
|
|
80
|
+
console.log(theme_1.theme.gray(` ${result.summary.total} total vulnerabilities found`));
|
|
81
|
+
console.log(theme_1.theme.gray(` ${vulnsAboveThreshold.length} at or above '${options.failOn}' severity (max allowed: ${maxVulns})`));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(theme_1.theme.error.bold("❌ SECURITY GATE: FAILED"));
|
|
85
|
+
console.log(theme_1.theme.error(` ${vulnsAboveThreshold.length} vulnerabilities at or above '${options.failOn}' severity (max allowed: ${maxVulns})`));
|
|
86
|
+
console.log("");
|
|
87
|
+
for (const v of vulnsAboveThreshold.slice(0, 10)) {
|
|
88
|
+
const color = v.severity === "critical" ? theme_1.theme.critical : theme_1.theme.high;
|
|
89
|
+
console.log(color(` [${v.severity.toUpperCase()}] ${v.title}`));
|
|
90
|
+
console.log(theme_1.theme.gray(` ${v.url}`));
|
|
91
|
+
if (v.remediation) {
|
|
92
|
+
console.log(theme_1.theme.dim(` Fix: ${v.remediation}`));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (vulnsAboveThreshold.length > 10) {
|
|
96
|
+
console.log(theme_1.theme.gray(` ... and ${vulnsAboveThreshold.length - 10} more`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
console.log("");
|
|
100
|
+
}
|
|
101
|
+
process.exit(passed ? 0 : 1);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (scanSpinner)
|
|
105
|
+
scanSpinner.fail(`Scan failed: ${err.message}`);
|
|
106
|
+
if (jsonMode) {
|
|
107
|
+
console.log(JSON.stringify({ error: err.message, passed: false }));
|
|
108
|
+
}
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
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",
|