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.
Files changed (91) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +419 -236
  3. package/dist/agent/confirmation.d.ts +5 -1
  4. package/dist/agent/confirmation.js +29 -9
  5. package/dist/agent/context.js +2 -3
  6. package/dist/agent/orchestrator.d.ts +2 -0
  7. package/dist/agent/orchestrator.js +50 -8
  8. package/dist/agent/prompts/system.d.ts +1 -1
  9. package/dist/agent/prompts/system.js +5 -7
  10. package/dist/agent/skills/health-check.js +22 -2
  11. package/dist/agent/skills/index.d.ts +1 -0
  12. package/dist/agent/skills/index.js +3 -1
  13. package/dist/agent/skills/verify-finding.d.ts +17 -0
  14. package/dist/agent/skills/verify-finding.js +91 -0
  15. package/dist/agent/skills/web-scan.js +46 -0
  16. package/dist/cli.js +156 -149
  17. package/dist/commands/agent.js +38 -38
  18. package/dist/commands/ai.d.ts +2 -0
  19. package/dist/commands/ai.js +112 -0
  20. package/dist/commands/analyze.js +103 -54
  21. package/dist/commands/config.js +55 -29
  22. package/dist/commands/dev.d.ts +2 -0
  23. package/dist/commands/dev.js +236 -0
  24. package/dist/commands/doctor.js +20 -15
  25. package/dist/commands/gate.d.ts +2 -0
  26. package/dist/commands/gate.js +109 -0
  27. package/dist/commands/onboard.js +188 -141
  28. package/dist/commands/report.js +68 -76
  29. package/dist/commands/scan.js +262 -81
  30. package/dist/commands/scans.d.ts +2 -0
  31. package/dist/commands/scans.js +55 -0
  32. package/dist/core/ai-client.d.ts +6 -1
  33. package/dist/core/ai-client.js +80 -12
  34. package/dist/core/ai-payloads.d.ts +17 -0
  35. package/dist/core/ai-payloads.js +54 -0
  36. package/dist/core/config-schema.d.ts +197 -0
  37. package/dist/core/config-schema.js +68 -0
  38. package/dist/core/config-schema.test.d.ts +1 -0
  39. package/dist/core/config-schema.test.js +151 -0
  40. package/dist/core/config.d.ts +8 -31
  41. package/dist/core/config.js +71 -14
  42. package/dist/core/diff-engine.d.ts +12 -0
  43. package/dist/core/diff-engine.js +47 -0
  44. package/dist/core/errors.d.ts +71 -0
  45. package/dist/core/errors.js +162 -0
  46. package/dist/core/scan-index.d.ts +20 -0
  47. package/dist/core/scan-index.js +52 -0
  48. package/dist/core/scan-storage.d.ts +11 -0
  49. package/dist/core/scan-storage.js +69 -0
  50. package/dist/core/scanner.d.ts +95 -13
  51. package/dist/core/scanner.js +342 -248
  52. package/dist/core/server-probe.d.ts +20 -0
  53. package/dist/core/server-probe.js +109 -0
  54. package/dist/core/vulnerability-detector.d.ts +9 -0
  55. package/dist/core/vulnerability-detector.js +46 -15
  56. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  57. package/dist/core/vulnerability-detector.test.js +210 -0
  58. package/dist/index.js +3 -0
  59. package/dist/plugins/PluginManager.d.ts +27 -0
  60. package/dist/plugins/PluginManager.js +166 -0
  61. package/dist/plugins/index.d.ts +12 -0
  62. package/dist/plugins/index.js +29 -0
  63. package/dist/plugins/types.d.ts +55 -0
  64. package/dist/plugins/types.js +25 -0
  65. package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
  66. package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
  67. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  68. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  69. package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
  70. package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
  71. package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
  72. package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
  73. package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
  74. package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
  75. package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
  76. package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -0
  77. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  78. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  79. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  80. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  81. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  82. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  83. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  84. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  85. package/dist/reports/PdfGenerator.d.ts +36 -0
  86. package/dist/reports/PdfGenerator.js +404 -0
  87. package/dist/utils/logger.d.ts +33 -1
  88. package/dist/utils/logger.js +127 -8
  89. package/dist/utils/theme.d.ts +56 -0
  90. package/dist/utils/theme.js +201 -0
  91. package/package.json +6 -3
@@ -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, options) => {
53
+ .action(async (scanFile) => {
20
54
  console.log("");
21
- console.log(chalk_1.default.bold.cyan("🧠 AI Security Analysis"));
22
- console.log(chalk_1.default.gray("".repeat(50)));
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
- // Load scan results
26
- let filepath;
27
- if (scanFile) {
28
- filepath = path_1.default.isAbsolute(scanFile)
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
- const spinner = logger_1.logger.spinner("Analyzing vulnerabilities with AI...");
53
- // Create AI client
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("📝 AI Analysis"));
62
- console.log(chalk_1.default.gray("".repeat(50)));
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("".repeat(50)));
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
- ${v.evidence ? `Evidence: ${v.evidence}` : ""}
96
- ${v.cwe ? `CWE: ${v.cwe}` : ""}`)
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. **Executive Summary**: Brief overview of the security posture
109
- 2. **Risk Assessment**: Overall risk level and business impact
110
- 3. **Priority Recommendations**: Top 3-5 vulnerabilities to fix first, with specific remediation steps
111
- 4. **Attack Scenarios**: How an attacker could chain these vulnerabilities
112
- 5. **Remediation Roadmap**: Step-by-step plan to address all findings
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
  }
@@ -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
- console.log(chalk_1.default.cyan(key), "=", chalk_1.default.white(JSON.stringify(value, null, 2)));
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
- else if (value === "false")
38
+ }
39
+ else if (value === "false") {
37
40
  parsedValue = false;
38
- else if (!isNaN(Number(value)))
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("📋 Current Configuration"));
51
- console.log(chalk_1.default.gray("".repeat(50)));
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(config, null, 2));
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("⚙️ Configuration Editor"));
63
- console.log(chalk_1.default.gray("".repeat(50)));
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: (answers) => answers.aiEnabled,
90
+ when: (ans) => ans.aiEnabled,
79
91
  },
80
92
  {
81
93
  type: "input",
82
94
  name: "apiKey",
83
- message: "API key:",
84
- default: config.ai.apiKey,
85
- when: (answers) => answers.aiEnabled,
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: (answers) => answers.aiEnabled,
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
- // Update config
109
- if (answers.aiEnabled !== undefined)
123
+ if (answers.aiEnabled !== undefined) {
110
124
  config.ai.enabled = answers.aiEnabled;
111
- if (answers.aiProvider)
125
+ }
126
+ if (answers.aiProvider) {
112
127
  config.ai.provider = answers.aiProvider;
113
- if (answers.apiKey)
128
+ }
129
+ if (answers.apiKey) {
114
130
  config.ai.apiKey = answers.apiKey;
115
- if (answers.model)
131
+ }
132
+ if (answers.model) {
116
133
  config.ai.defaultModel = answers.model;
117
- if (answers.reportFormat)
134
+ }
135
+ if (answers.reportFormat) {
118
136
  config.report.defaultFormat = answers.reportFormat;
119
- if (answers.rateLimit)
137
+ }
138
+ if (Number.isFinite(answers.rateLimit) && answers.rateLimit > 0) {
120
139
  config.scan.rateLimitPerSecond = answers.rateLimit;
121
- (0, config_1.setConfig)(config);
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, path) {
128
- return path.split(".").reduce((current, key) => current?.[key], obj);
147
+ function getNestedValue(obj, keyPath) {
148
+ return keyPath
149
+ .split(".")
150
+ .reduce((current, key) => current?.[key], obj);
129
151
  }
130
- function setNestedValue(obj, path, value) {
131
- const keys = path.split(".");
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,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerDevCommand(program: Command): void;
@@ -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
+ }