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
@@ -32,16 +32,30 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.validateScanProfile = exports.validateConfig = exports.scanProfiles = void 0;
36
40
  exports.isDebugEnabled = isDebugEnabled;
37
41
  exports.getConfigStore = getConfigStore;
38
42
  exports.getConfig = getConfig;
39
43
  exports.getConfigValue = getConfigValue;
40
44
  exports.setConfigValue = setConfigValue;
41
45
  exports.setConfig = setConfig;
46
+ exports.getScanProfile = getScanProfile;
47
+ exports.addScanProfile = addScanProfile;
42
48
  const fs = __importStar(require("fs"));
43
49
  const path = __importStar(require("path"));
44
50
  const os = __importStar(require("os"));
51
+ const config_schema_1 = require("./config-schema");
52
+ const theme_1 = require("../utils/theme");
53
+ var config_schema_2 = require("./config-schema");
54
+ Object.defineProperty(exports, "scanProfiles", { enumerable: true, get: function () { return config_schema_2.defaultScanProfiles; } });
55
+ Object.defineProperty(exports, "validateConfig", { enumerable: true, get: function () { return config_schema_2.validateConfig; } });
56
+ Object.defineProperty(exports, "validateScanProfile", { enumerable: true, get: function () { return config_schema_2.validateScanProfile; } });
57
+ // Re-export errors for convenience
58
+ __exportStar(require("./errors"), exports);
45
59
  const defaults = {
46
60
  ai: {
47
61
  provider: "openai",
@@ -50,28 +64,30 @@ const defaults = {
50
64
  enabled: false,
51
65
  },
52
66
  scan: {
53
- defaultTimeout: 60,
67
+ defaultTimeout: 30000,
54
68
  maxThreads: 5,
55
- userAgent: "KramScan/0.1.0",
69
+ userAgent: `KramScan/${theme_1.CLI_VERSION}`,
56
70
  followRedirects: true,
57
71
  verifySSL: true,
58
72
  rateLimitPerSecond: 5,
59
- strictScope: true
73
+ strictScope: true,
74
+ profiles: config_schema_1.defaultScanProfiles,
75
+ defaultProfile: "balanced",
60
76
  },
61
77
  report: {
62
78
  defaultFormat: "word",
63
79
  companyName: "Your Company",
64
80
  includeScreenshots: false,
65
- severityThreshold: "low"
81
+ severityThreshold: "low",
66
82
  },
67
83
  skills: {
68
- sqli: { enabled: true, timeout: 120 },
69
- xss: { enabled: true, timeout: 90 },
84
+ sqli: { enabled: true, timeout: 120000 },
85
+ xss: { enabled: true, timeout: 90000 },
70
86
  headers: { enabled: true },
71
87
  csrf: { enabled: true },
72
88
  idor: { enabled: true },
73
- jwt: { enabled: true }
74
- }
89
+ jwt: { enabled: true },
90
+ },
75
91
  };
76
92
  function isDebugEnabled() {
77
93
  return process.env.KRAMSCAN_DEBUG === "true" || process.env.KRAMSCAN_DEBUG === "1";
@@ -230,7 +246,13 @@ class ConfigStore {
230
246
  if (fs.existsSync(this.configPath)) {
231
247
  try {
232
248
  const raw = fs.readFileSync(this.configPath, "utf-8");
233
- this.data = { ...defaultConfig, ...JSON.parse(raw) };
249
+ const parsed = JSON.parse(raw);
250
+ // Validate and merge with defaults
251
+ this.data = { ...defaultConfig, ...parsed };
252
+ // Ensure profiles are merged properly
253
+ if (parsed.scan?.profiles) {
254
+ this.data.scan.profiles = { ...defaultConfig.scan.profiles, ...parsed.scan.profiles };
255
+ }
234
256
  }
235
257
  catch {
236
258
  this.data = JSON.parse(JSON.stringify(defaultConfig));
@@ -246,6 +268,14 @@ class ConfigStore {
246
268
  if (storedKey) {
247
269
  this.data.ai.apiKey = storedKey;
248
270
  }
271
+ // Validate config on initialization
272
+ try {
273
+ this.data = (0, config_schema_1.validateConfig)(this.data);
274
+ }
275
+ catch (error) {
276
+ console.warn("Invalid config detected, using defaults:", error.message);
277
+ this.data = JSON.parse(JSON.stringify(defaults));
278
+ }
249
279
  }
250
280
  get store() {
251
281
  return this.data;
@@ -274,8 +304,13 @@ class ConfigStore {
274
304
  }
275
305
  current[keys[keys.length - 1]] = value;
276
306
  // If setting API key, store it securely
277
- if (key === "ai.apiKey" && typeof value === "string" && value) {
278
- await this.credentialManager.setPassword("apiKey", value);
307
+ if (key === "ai.apiKey" && typeof value === "string") {
308
+ if (value) {
309
+ await this.credentialManager.setPassword("apiKey", value);
310
+ }
311
+ else {
312
+ await this.credentialManager.deletePassword("apiKey");
313
+ }
279
314
  }
280
315
  await this.save();
281
316
  }
@@ -291,14 +326,28 @@ class ConfigStore {
291
326
  fs.writeFileSync(this.configPath, JSON.stringify(configToSave, null, 2), "utf-8");
292
327
  }
293
328
  async setConfig(config) {
294
- Object.assign(this.data, config);
329
+ // Validate before setting
330
+ const validated = (0, config_schema_1.validateConfig)(config);
331
+ Object.assign(this.data, validated);
295
332
  // Save API key securely if present
296
- if (config.ai?.apiKey) {
297
- await this.credentialManager.setPassword("apiKey", config.ai.apiKey);
333
+ if (typeof config.ai?.apiKey === "string") {
334
+ if (config.ai.apiKey) {
335
+ await this.credentialManager.setPassword("apiKey", config.ai.apiKey);
336
+ }
337
+ else {
338
+ await this.credentialManager.deletePassword("apiKey");
339
+ }
298
340
  this.data.ai.apiKey = config.ai.apiKey;
299
341
  }
300
342
  await this.save();
301
343
  }
344
+ getScanProfile(name) {
345
+ return this.data.scan.profiles[name];
346
+ }
347
+ async addScanProfile(name, profile) {
348
+ this.data.scan.profiles[name] = profile;
349
+ await this.save();
350
+ }
302
351
  }
303
352
  const store = new ConfigStore("kramscan", defaults);
304
353
  // Initialize async
@@ -328,3 +377,11 @@ async function setConfig(config) {
328
377
  await ensureInitialized();
329
378
  await store.setConfig(config);
330
379
  }
380
+ async function getScanProfile(name) {
381
+ await ensureInitialized();
382
+ return store.getScanProfile(name);
383
+ }
384
+ async function addScanProfile(name, profile) {
385
+ await ensureInitialized();
386
+ await store.addScanProfile(name, profile);
387
+ }
@@ -0,0 +1,12 @@
1
+ import { Vulnerability, ScanResult } from "./vulnerability-detector";
2
+ export interface ScanDiff {
3
+ newVulnerabilities: Vulnerability[];
4
+ resolvedVulnerabilities: Vulnerability[];
5
+ unchangedCount: number;
6
+ previousTotal: number;
7
+ currentTotal: number;
8
+ }
9
+ /**
10
+ * Compares two scan results and produces a diff of new and resolved vulnerabilities.
11
+ */
12
+ export declare function diffScanResults(previous: ScanResult, current: ScanResult): ScanDiff;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diffScanResults = diffScanResults;
4
+ /**
5
+ * Creates a unique fingerprint for a vulnerability to enable comparison.
6
+ */
7
+ function vulnFingerprint(v) {
8
+ return `${v.type}:${v.severity}:${v.title}:${new URL(v.url).pathname}`;
9
+ }
10
+ /**
11
+ * Compares two scan results and produces a diff of new and resolved vulnerabilities.
12
+ */
13
+ function diffScanResults(previous, current) {
14
+ const prevFingerprints = new Map();
15
+ const currFingerprints = new Map();
16
+ for (const v of previous.vulnerabilities) {
17
+ prevFingerprints.set(vulnFingerprint(v), v);
18
+ }
19
+ for (const v of current.vulnerabilities) {
20
+ currFingerprints.set(vulnFingerprint(v), v);
21
+ }
22
+ const newVulnerabilities = [];
23
+ const resolvedVulnerabilities = [];
24
+ let unchangedCount = 0;
25
+ // Find new vulnerabilities (in current but not in previous)
26
+ for (const [fp, vuln] of currFingerprints) {
27
+ if (!prevFingerprints.has(fp)) {
28
+ newVulnerabilities.push(vuln);
29
+ }
30
+ else {
31
+ unchangedCount++;
32
+ }
33
+ }
34
+ // Find resolved vulnerabilities (in previous but not in current)
35
+ for (const [fp, vuln] of prevFingerprints) {
36
+ if (!currFingerprints.has(fp)) {
37
+ resolvedVulnerabilities.push(vuln);
38
+ }
39
+ }
40
+ return {
41
+ newVulnerabilities,
42
+ resolvedVulnerabilities,
43
+ unchangedCount,
44
+ previousTotal: previous.vulnerabilities.length,
45
+ currentTotal: current.vulnerabilities.length,
46
+ };
47
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Custom Error Types for KramScan
3
+ * Provides structured error handling with error codes
4
+ */
5
+ export declare enum ErrorCode {
6
+ SCN_INIT_FAILED = "SCN_INIT_FAILED",
7
+ SCN_CRAWL_FAILED = "SCN_CRAWL_FAILED",
8
+ SCN_TIMEOUT = "SCN_TIMEOUT",
9
+ SCN_INVALID_URL = "SCN_INVALID_URL",
10
+ SCN_SCOPE_VIOLATION = "SCN_SCOPE_VIOLATION",
11
+ SCN_BROWSER_ERROR = "SCN_BROWSER_ERROR",
12
+ PLG_INIT_FAILED = "PLG_INIT_FAILED",
13
+ PLG_EXECUTION_FAILED = "PLG_EXECUTION_FAILED",
14
+ PLG_NOT_FOUND = "PLG_NOT_FOUND",
15
+ PLG_DISABLED = "PLG_DISABLED",
16
+ PLG_TIMEOUT = "PLG_TIMEOUT",
17
+ CFG_INVALID = "CFG_INVALID",
18
+ CFG_NOT_FOUND = "CFG_NOT_FOUND",
19
+ CFG_WRITE_FAILED = "CFG_WRITE_FAILED",
20
+ NET_REQUEST_FAILED = "NET_REQUEST_FAILED",
21
+ NET_RATE_LIMITED = "NET_RATE_LIMITED",
22
+ NET_SSL_ERROR = "NET_SSL_ERROR",
23
+ AI_INIT_FAILED = "AI_INIT_FAILED",
24
+ AI_REQUEST_FAILED = "AI_REQUEST_FAILED",
25
+ AI_QUOTA_EXCEEDED = "AI_QUOTA_EXCEEDED",
26
+ RPT_GENERATION_FAILED = "RPT_GENERATION_FAILED",
27
+ RPT_INVALID_FORMAT = "RPT_INVALID_FORMAT"
28
+ }
29
+ export interface KramScanErrorOptions {
30
+ code: ErrorCode;
31
+ statusCode?: number;
32
+ retryable?: boolean;
33
+ context?: Record<string, unknown>;
34
+ }
35
+ export declare class KramScanError extends Error {
36
+ readonly code: ErrorCode;
37
+ readonly statusCode?: number;
38
+ readonly retryable: boolean;
39
+ readonly context?: Record<string, unknown>;
40
+ readonly timestamp: string;
41
+ constructor(message: string, options: KramScanErrorOptions);
42
+ toJSON(): Record<string, unknown>;
43
+ }
44
+ export declare class ScannerError extends KramScanError {
45
+ constructor(message: string, code?: ErrorCode, context?: Record<string, unknown>);
46
+ }
47
+ export declare class PluginError extends KramScanError {
48
+ constructor(message: string, code?: ErrorCode, context?: Record<string, unknown>);
49
+ }
50
+ export declare class ConfigError extends KramScanError {
51
+ constructor(message: string, code?: ErrorCode, context?: Record<string, unknown>);
52
+ }
53
+ export declare class NetworkError extends KramScanError {
54
+ constructor(message: string, code?: ErrorCode, context?: Record<string, unknown>);
55
+ }
56
+ export declare class AiError extends KramScanError {
57
+ constructor(message: string, code?: ErrorCode, context?: Record<string, unknown>);
58
+ }
59
+ export declare class ReportError extends KramScanError {
60
+ constructor(message: string, code?: ErrorCode, context?: Record<string, unknown>);
61
+ }
62
+ export interface ErrorHandlerConfig {
63
+ maxRetries: number;
64
+ baseDelay: number;
65
+ maxDelay: number;
66
+ retryableCodes: ErrorCode[];
67
+ }
68
+ export declare function isRetryable(error: KramScanError, config?: ErrorHandlerConfig): boolean;
69
+ export declare function shouldRetry(error: KramScanError, attempt: number, config?: ErrorHandlerConfig): boolean;
70
+ export declare function getRetryDelay(error: KramScanError, attempt: number, config?: ErrorHandlerConfig): number;
71
+ export declare function setupGlobalErrorHandlers(): void;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Custom Error Types for KramScan
4
+ * Provides structured error handling with error codes
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ReportError = exports.AiError = exports.NetworkError = exports.ConfigError = exports.PluginError = exports.ScannerError = exports.KramScanError = exports.ErrorCode = void 0;
8
+ exports.isRetryable = isRetryable;
9
+ exports.shouldRetry = shouldRetry;
10
+ exports.getRetryDelay = getRetryDelay;
11
+ exports.setupGlobalErrorHandlers = setupGlobalErrorHandlers;
12
+ var ErrorCode;
13
+ (function (ErrorCode) {
14
+ // Scanner errors (SCN-xxx)
15
+ ErrorCode["SCN_INIT_FAILED"] = "SCN_INIT_FAILED";
16
+ ErrorCode["SCN_CRAWL_FAILED"] = "SCN_CRAWL_FAILED";
17
+ ErrorCode["SCN_TIMEOUT"] = "SCN_TIMEOUT";
18
+ ErrorCode["SCN_INVALID_URL"] = "SCN_INVALID_URL";
19
+ ErrorCode["SCN_SCOPE_VIOLATION"] = "SCN_SCOPE_VIOLATION";
20
+ ErrorCode["SCN_BROWSER_ERROR"] = "SCN_BROWSER_ERROR";
21
+ // Plugin errors (PLG-xxx)
22
+ ErrorCode["PLG_INIT_FAILED"] = "PLG_INIT_FAILED";
23
+ ErrorCode["PLG_EXECUTION_FAILED"] = "PLG_EXECUTION_FAILED";
24
+ ErrorCode["PLG_NOT_FOUND"] = "PLG_NOT_FOUND";
25
+ ErrorCode["PLG_DISABLED"] = "PLG_DISABLED";
26
+ ErrorCode["PLG_TIMEOUT"] = "PLG_TIMEOUT";
27
+ // Config errors (CFG-xxx)
28
+ ErrorCode["CFG_INVALID"] = "CFG_INVALID";
29
+ ErrorCode["CFG_NOT_FOUND"] = "CFG_NOT_FOUND";
30
+ ErrorCode["CFG_WRITE_FAILED"] = "CFG_WRITE_FAILED";
31
+ // Network errors (NET-xxx)
32
+ ErrorCode["NET_REQUEST_FAILED"] = "NET_REQUEST_FAILED";
33
+ ErrorCode["NET_RATE_LIMITED"] = "NET_RATE_LIMITED";
34
+ ErrorCode["NET_SSL_ERROR"] = "NET_SSL_ERROR";
35
+ // AI errors (AI-xxx)
36
+ ErrorCode["AI_INIT_FAILED"] = "AI_INIT_FAILED";
37
+ ErrorCode["AI_REQUEST_FAILED"] = "AI_REQUEST_FAILED";
38
+ ErrorCode["AI_QUOTA_EXCEEDED"] = "AI_QUOTA_EXCEEDED";
39
+ // Report errors (RPT-xxx)
40
+ ErrorCode["RPT_GENERATION_FAILED"] = "RPT_GENERATION_FAILED";
41
+ ErrorCode["RPT_INVALID_FORMAT"] = "RPT_INVALID_FORMAT";
42
+ })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
43
+ class KramScanError extends Error {
44
+ code;
45
+ statusCode;
46
+ retryable;
47
+ context;
48
+ timestamp;
49
+ constructor(message, options) {
50
+ super(message);
51
+ this.name = "KramScanError";
52
+ this.code = options.code;
53
+ this.statusCode = options.statusCode;
54
+ this.retryable = options.retryable ?? false;
55
+ this.context = options.context;
56
+ this.timestamp = new Date().toISOString();
57
+ // Maintains proper stack trace in V8 environments
58
+ if (Error.captureStackTrace) {
59
+ Error.captureStackTrace(this, KramScanError);
60
+ }
61
+ }
62
+ toJSON() {
63
+ return {
64
+ name: this.name,
65
+ message: this.message,
66
+ code: this.code,
67
+ statusCode: this.statusCode,
68
+ retryable: this.retryable,
69
+ context: this.context,
70
+ timestamp: this.timestamp,
71
+ stack: this.stack,
72
+ };
73
+ }
74
+ }
75
+ exports.KramScanError = KramScanError;
76
+ // Convenience error classes for common scenarios
77
+ class ScannerError extends KramScanError {
78
+ constructor(message, code = ErrorCode.SCN_CRAWL_FAILED, context) {
79
+ super(message, { code, retryable: true, context });
80
+ this.name = "ScannerError";
81
+ }
82
+ }
83
+ exports.ScannerError = ScannerError;
84
+ class PluginError extends KramScanError {
85
+ constructor(message, code = ErrorCode.PLG_EXECUTION_FAILED, context) {
86
+ super(message, { code, retryable: false, context });
87
+ this.name = "PluginError";
88
+ }
89
+ }
90
+ exports.PluginError = PluginError;
91
+ class ConfigError extends KramScanError {
92
+ constructor(message, code = ErrorCode.CFG_INVALID, context) {
93
+ super(message, { code, retryable: false, context });
94
+ this.name = "ConfigError";
95
+ }
96
+ }
97
+ exports.ConfigError = ConfigError;
98
+ class NetworkError extends KramScanError {
99
+ constructor(message, code = ErrorCode.NET_REQUEST_FAILED, context) {
100
+ super(message, { code, retryable: true, context });
101
+ this.name = "NetworkError";
102
+ }
103
+ }
104
+ exports.NetworkError = NetworkError;
105
+ class AiError extends KramScanError {
106
+ constructor(message, code = ErrorCode.AI_REQUEST_FAILED, context) {
107
+ super(message, { code, retryable: true, context });
108
+ this.name = "AiError";
109
+ }
110
+ }
111
+ exports.AiError = AiError;
112
+ class ReportError extends KramScanError {
113
+ constructor(message, code = ErrorCode.RPT_GENERATION_FAILED, context) {
114
+ super(message, { code, retryable: false, context });
115
+ this.name = "ReportError";
116
+ }
117
+ }
118
+ exports.ReportError = ReportError;
119
+ const defaultErrorHandlerConfig = {
120
+ maxRetries: 3,
121
+ baseDelay: 1000,
122
+ maxDelay: 10000,
123
+ retryableCodes: [
124
+ ErrorCode.SCN_CRAWL_FAILED,
125
+ ErrorCode.SCN_TIMEOUT,
126
+ ErrorCode.SCN_BROWSER_ERROR,
127
+ ErrorCode.NET_REQUEST_FAILED,
128
+ ErrorCode.PLG_EXECUTION_FAILED,
129
+ ErrorCode.AI_REQUEST_FAILED,
130
+ ],
131
+ };
132
+ function isRetryable(error, config = defaultErrorHandlerConfig) {
133
+ if (!error.retryable)
134
+ return false;
135
+ return config.retryableCodes.includes(error.code);
136
+ }
137
+ function shouldRetry(error, attempt, config = defaultErrorHandlerConfig) {
138
+ return attempt < config.maxRetries && isRetryable(error, config);
139
+ }
140
+ function getRetryDelay(error, attempt, config = defaultErrorHandlerConfig) {
141
+ const delay = Math.min(config.baseDelay * Math.pow(2, attempt), config.maxDelay);
142
+ // Add jitter to prevent thundering herd
143
+ const jitter = Math.random() * 0.3 * delay;
144
+ return Math.floor(delay + jitter);
145
+ }
146
+ // Global error handler for uncaught errors
147
+ function setupGlobalErrorHandlers() {
148
+ process.on("uncaughtException", (error) => {
149
+ console.error("[FATAL] Uncaught Exception:");
150
+ console.error(error.message);
151
+ if (error.stack) {
152
+ console.error(error.stack);
153
+ }
154
+ process.exit(1);
155
+ });
156
+ process.on("unhandledRejection", (reason, promise) => {
157
+ console.error("[FATAL] Unhandled Promise Rejection:");
158
+ console.error("Reason:", reason);
159
+ console.error("Promise:", promise);
160
+ process.exit(1);
161
+ });
162
+ }
@@ -0,0 +1,20 @@
1
+ export interface ScanIndexEntry {
2
+ id: string;
3
+ target: string;
4
+ hostname: string;
5
+ timestamp: string;
6
+ jsonPath: string;
7
+ pdfPath?: string;
8
+ summary: {
9
+ total: number;
10
+ critical: number;
11
+ high: number;
12
+ medium: number;
13
+ low: number;
14
+ info: number;
15
+ };
16
+ score?: number;
17
+ }
18
+ export declare function addScanToIndex(entry: Omit<ScanIndexEntry, "id">): Promise<ScanIndexEntry>;
19
+ export declare function listScans(limit?: number): Promise<ScanIndexEntry[]>;
20
+ export declare function getLatestScan(): Promise<ScanIndexEntry | null>;
@@ -0,0 +1,52 @@
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.addScanToIndex = addScanToIndex;
7
+ exports.listScans = listScans;
8
+ exports.getLatestScan = getLatestScan;
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ function getIndexPath() {
13
+ return path_1.default.join(os_1.default.homedir(), ".kramscan", "scans", "index.json");
14
+ }
15
+ async function readIndex() {
16
+ const indexPath = getIndexPath();
17
+ try {
18
+ const raw = await promises_1.default.readFile(indexPath, "utf-8");
19
+ const parsed = JSON.parse(raw);
20
+ if (Array.isArray(parsed)) {
21
+ return parsed;
22
+ }
23
+ return [];
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ }
29
+ async function writeIndex(entries) {
30
+ const indexPath = getIndexPath();
31
+ await promises_1.default.mkdir(path_1.default.dirname(indexPath), { recursive: true });
32
+ await promises_1.default.writeFile(indexPath, JSON.stringify(entries, null, 2), "utf-8");
33
+ }
34
+ async function addScanToIndex(entry) {
35
+ const hostname = entry.hostname || "unknown";
36
+ const id = `${new Date(entry.timestamp).getTime()}-${hostname}-${Math.random().toString(36).slice(2, 8)}`;
37
+ const full = { ...entry, id };
38
+ const existing = await readIndex();
39
+ const merged = [full, ...existing]
40
+ .filter((e, idx, arr) => arr.findIndex((x) => x.jsonPath === e.jsonPath) === idx)
41
+ .slice(0, 500);
42
+ await writeIndex(merged);
43
+ return full;
44
+ }
45
+ async function listScans(limit = 20) {
46
+ const entries = await readIndex();
47
+ return entries.slice(0, Math.max(1, limit));
48
+ }
49
+ async function getLatestScan() {
50
+ const entries = await readIndex();
51
+ return entries.length > 0 ? entries[0] : null;
52
+ }
@@ -0,0 +1,11 @@
1
+ export interface ResolvedScanFile {
2
+ filepath: string;
3
+ filename: string;
4
+ isLatest: boolean;
5
+ }
6
+ export declare function getKramScanHome(): string;
7
+ export declare function getScansDirectory(): string;
8
+ export declare function getReportsDirectory(): string;
9
+ export declare function ensureScansDirectory(): Promise<string>;
10
+ export declare function ensureReportsDirectory(): Promise<string>;
11
+ export declare function resolveScanFile(scanFile?: string): Promise<ResolvedScanFile>;
@@ -0,0 +1,69 @@
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.getKramScanHome = getKramScanHome;
7
+ exports.getScansDirectory = getScansDirectory;
8
+ exports.getReportsDirectory = getReportsDirectory;
9
+ exports.ensureScansDirectory = ensureScansDirectory;
10
+ exports.ensureReportsDirectory = ensureReportsDirectory;
11
+ exports.resolveScanFile = resolveScanFile;
12
+ const promises_1 = __importDefault(require("fs/promises"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ function getKramScanHome() {
16
+ return path_1.default.join(os_1.default.homedir(), ".kramscan");
17
+ }
18
+ function getScansDirectory() {
19
+ return path_1.default.join(getKramScanHome(), "scans");
20
+ }
21
+ function getReportsDirectory() {
22
+ return path_1.default.join(getKramScanHome(), "reports");
23
+ }
24
+ async function ensureScansDirectory() {
25
+ const scanDir = getScansDirectory();
26
+ await promises_1.default.mkdir(scanDir, { recursive: true });
27
+ return scanDir;
28
+ }
29
+ async function ensureReportsDirectory() {
30
+ const reportsDir = getReportsDirectory();
31
+ await promises_1.default.mkdir(reportsDir, { recursive: true });
32
+ return reportsDir;
33
+ }
34
+ async function resolveScanFile(scanFile) {
35
+ if (scanFile) {
36
+ const filepath = path_1.default.isAbsolute(scanFile)
37
+ ? scanFile
38
+ : path_1.default.join(process.cwd(), scanFile);
39
+ return {
40
+ filepath,
41
+ filename: path_1.default.basename(filepath),
42
+ isLatest: false,
43
+ };
44
+ }
45
+ const scanDir = await ensureScansDirectory();
46
+ const entries = await promises_1.default.readdir(scanDir, { withFileTypes: true });
47
+ const files = entries
48
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
49
+ .map((entry) => entry.name);
50
+ if (files.length === 0) {
51
+ throw new Error("No scan results found. Run 'kramscan scan <url>' first.");
52
+ }
53
+ const filesWithStats = await Promise.all(files.map(async (filename) => {
54
+ const filepath = path_1.default.join(scanDir, filename);
55
+ const stats = await promises_1.default.stat(filepath);
56
+ return {
57
+ filename,
58
+ filepath,
59
+ mtimeMs: stats.mtimeMs,
60
+ };
61
+ }));
62
+ filesWithStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
63
+ const latest = filesWithStats[0];
64
+ return {
65
+ filepath: latest.filepath,
66
+ filename: latest.filename,
67
+ isLatest: true,
68
+ };
69
+ }