kramscan 0.1.0 → 0.2.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 (105) hide show
  1. package/README.md +392 -87
  2. package/dist/agent/confirmation.d.ts +38 -0
  3. package/dist/agent/confirmation.js +210 -0
  4. package/dist/agent/context.d.ts +81 -0
  5. package/dist/agent/context.js +227 -0
  6. package/dist/agent/index.d.ts +10 -0
  7. package/dist/agent/index.js +32 -0
  8. package/dist/agent/orchestrator.d.ts +63 -0
  9. package/dist/agent/orchestrator.js +370 -0
  10. package/dist/agent/prompts/system.d.ts +6 -0
  11. package/dist/agent/prompts/system.js +116 -0
  12. package/dist/agent/skill-registry.d.ts +78 -0
  13. package/dist/agent/skill-registry.js +202 -0
  14. package/dist/agent/skills/analyze-findings.d.ts +22 -0
  15. package/dist/agent/skills/analyze-findings.js +191 -0
  16. package/dist/agent/skills/generate-report.d.ts +26 -0
  17. package/dist/agent/skills/generate-report.js +436 -0
  18. package/dist/agent/skills/health-check.d.ts +28 -0
  19. package/dist/agent/skills/health-check.js +344 -0
  20. package/dist/agent/skills/index.d.ts +9 -0
  21. package/dist/agent/skills/index.js +17 -0
  22. package/dist/agent/skills/verify-finding.d.ts +17 -0
  23. package/dist/agent/skills/verify-finding.js +91 -0
  24. package/dist/agent/skills/web-scan.d.ts +22 -0
  25. package/dist/agent/skills/web-scan.js +203 -0
  26. package/dist/agent/types.d.ts +141 -0
  27. package/dist/agent/types.js +16 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.js +176 -139
  30. package/dist/commands/agent.d.ts +6 -0
  31. package/dist/commands/agent.js +250 -0
  32. package/dist/commands/ai.d.ts +2 -0
  33. package/dist/commands/ai.js +112 -0
  34. package/dist/commands/analyze.js +104 -55
  35. package/dist/commands/config.js +63 -37
  36. package/dist/commands/doctor.js +22 -17
  37. package/dist/commands/onboard.js +190 -125
  38. package/dist/commands/report.js +69 -77
  39. package/dist/commands/scan.js +261 -81
  40. package/dist/commands/scans.d.ts +2 -0
  41. package/dist/commands/scans.js +51 -0
  42. package/dist/core/ai-client.d.ts +7 -2
  43. package/dist/core/ai-client.js +231 -20
  44. package/dist/core/ai-payloads.d.ts +17 -0
  45. package/dist/core/ai-payloads.js +54 -0
  46. package/dist/core/config-schema.d.ts +197 -0
  47. package/dist/core/config-schema.js +68 -0
  48. package/dist/core/config-schema.test.d.ts +1 -0
  49. package/dist/core/config-schema.test.js +151 -0
  50. package/dist/core/config.d.ts +17 -36
  51. package/dist/core/config.js +261 -20
  52. package/dist/core/errors.d.ts +71 -0
  53. package/dist/core/errors.js +162 -0
  54. package/dist/core/scan-index.d.ts +19 -0
  55. package/dist/core/scan-index.js +52 -0
  56. package/dist/core/scan-storage.d.ts +11 -0
  57. package/dist/core/scan-storage.js +69 -0
  58. package/dist/core/scanner.d.ts +101 -4
  59. package/dist/core/scanner.js +432 -63
  60. package/dist/core/vulnerability-detector.d.ts +18 -2
  61. package/dist/core/vulnerability-detector.js +349 -38
  62. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  63. package/dist/core/vulnerability-detector.test.js +210 -0
  64. package/dist/index.js +3 -0
  65. package/dist/plugins/PluginManager.d.ts +27 -0
  66. package/dist/plugins/PluginManager.js +166 -0
  67. package/dist/plugins/index.d.ts +7 -0
  68. package/dist/plugins/index.js +19 -0
  69. package/dist/plugins/types.d.ts +55 -0
  70. package/dist/plugins/types.js +25 -0
  71. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  72. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  73. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  74. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  75. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  76. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  77. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  78. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  79. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  80. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  81. package/dist/reports/PdfGenerator.d.ts +36 -0
  82. package/dist/reports/PdfGenerator.js +379 -0
  83. package/dist/utils/logger.d.ts +33 -1
  84. package/dist/utils/logger.js +127 -8
  85. package/dist/utils/theme.d.ts +55 -0
  86. package/dist/utils/theme.js +195 -0
  87. package/package.json +27 -6
  88. package/dist/core/executor.d.ts +0 -2
  89. package/dist/core/executor.js +0 -74
  90. package/dist/core/logger.d.ts +0 -12
  91. package/dist/core/logger.js +0 -51
  92. package/dist/core/registry.d.ts +0 -3
  93. package/dist/core/registry.js +0 -35
  94. package/dist/core/storage.d.ts +0 -4
  95. package/dist/core/storage.js +0 -39
  96. package/dist/core/types.d.ts +0 -24
  97. package/dist/core/types.js +0 -2
  98. package/dist/skills/base.d.ts +0 -8
  99. package/dist/skills/base.js +0 -6
  100. package/dist/skills/builtin.d.ts +0 -4
  101. package/dist/skills/builtin.js +0 -71
  102. package/dist/skills/loader.d.ts +0 -2
  103. package/dist/skills/loader.js +0 -27
  104. package/dist/skills/types.d.ts +0 -46
  105. package/dist/skills/types.js +0 -2
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_schema_1 = require("./config-schema");
4
+ describe("config-schema", () => {
5
+ // ─── validateConfig ────────────────────────────────────────────
6
+ describe("validateConfig", () => {
7
+ const validConfig = {
8
+ ai: {
9
+ provider: "openai",
10
+ apiKey: "sk-test",
11
+ defaultModel: "gpt-4",
12
+ enabled: true,
13
+ },
14
+ scan: {
15
+ defaultTimeout: 30000,
16
+ maxThreads: 5,
17
+ userAgent: "KramScan/0.1.0",
18
+ followRedirects: true,
19
+ verifySSL: true,
20
+ rateLimitPerSecond: 5,
21
+ strictScope: true,
22
+ profiles: {
23
+ quick: { depth: 1, timeout: 15000, maxPages: 10, maxLinksPerPage: 20 },
24
+ },
25
+ defaultProfile: "balanced",
26
+ },
27
+ report: {
28
+ defaultFormat: "word",
29
+ companyName: "Test Corp",
30
+ includeScreenshots: false,
31
+ severityThreshold: "low",
32
+ },
33
+ skills: {
34
+ sqli: { enabled: true },
35
+ xss: { enabled: true },
36
+ },
37
+ };
38
+ it("should accept a valid config", () => {
39
+ const result = (0, config_schema_1.validateConfig)(validConfig);
40
+ expect(result.ai.provider).toBe("openai");
41
+ expect(result.scan.maxThreads).toBe(5);
42
+ });
43
+ it("should reject invalid AI provider", () => {
44
+ expect(() => (0, config_schema_1.validateConfig)({
45
+ ...validConfig,
46
+ ai: { ...validConfig.ai, provider: "invalid_provider" },
47
+ })).toThrow();
48
+ });
49
+ it("should reject invalid report format", () => {
50
+ expect(() => (0, config_schema_1.validateConfig)({
51
+ ...validConfig,
52
+ report: { ...validConfig.report, defaultFormat: "pdf" },
53
+ })).toThrow();
54
+ });
55
+ it("should reject timeout below minimum", () => {
56
+ expect(() => (0, config_schema_1.validateConfig)({
57
+ ...validConfig,
58
+ scan: { ...validConfig.scan, defaultTimeout: 500 },
59
+ })).toThrow();
60
+ });
61
+ it("should reject maxThreads above maximum", () => {
62
+ expect(() => (0, config_schema_1.validateConfig)({
63
+ ...validConfig,
64
+ scan: { ...validConfig.scan, maxThreads: 25 },
65
+ })).toThrow();
66
+ });
67
+ it("should reject maxThreads below minimum", () => {
68
+ expect(() => (0, config_schema_1.validateConfig)({
69
+ ...validConfig,
70
+ scan: { ...validConfig.scan, maxThreads: 0 },
71
+ })).toThrow();
72
+ });
73
+ it("should accept all valid AI providers", () => {
74
+ const providers = ["openai", "anthropic", "gemini", "openrouter", "mistral", "kimi", "groq"];
75
+ for (const provider of providers) {
76
+ expect(() => (0, config_schema_1.validateConfig)({
77
+ ...validConfig,
78
+ ai: { ...validConfig.ai, provider },
79
+ })).not.toThrow();
80
+ }
81
+ });
82
+ it("should accept all valid severity thresholds", () => {
83
+ const thresholds = ["info", "low", "medium", "high", "critical"];
84
+ for (const threshold of thresholds) {
85
+ expect(() => (0, config_schema_1.validateConfig)({
86
+ ...validConfig,
87
+ report: { ...validConfig.report, severityThreshold: threshold },
88
+ })).not.toThrow();
89
+ }
90
+ });
91
+ });
92
+ // ─── validateScanProfile ───────────────────────────────────────
93
+ describe("validateScanProfile", () => {
94
+ it("should accept a valid scan profile", () => {
95
+ const profile = (0, config_schema_1.validateScanProfile)({
96
+ depth: 2,
97
+ timeout: 30000,
98
+ maxPages: 50,
99
+ maxLinksPerPage: 30,
100
+ });
101
+ expect(profile.depth).toBe(2);
102
+ expect(profile.maxPages).toBe(50);
103
+ });
104
+ it("should reject depth below minimum", () => {
105
+ expect(() => (0, config_schema_1.validateScanProfile)({
106
+ depth: 0,
107
+ timeout: 30000,
108
+ maxPages: 50,
109
+ maxLinksPerPage: 30,
110
+ })).toThrow();
111
+ });
112
+ it("should reject depth above maximum", () => {
113
+ expect(() => (0, config_schema_1.validateScanProfile)({
114
+ depth: 6,
115
+ timeout: 30000,
116
+ maxPages: 50,
117
+ maxLinksPerPage: 30,
118
+ })).toThrow();
119
+ });
120
+ it("should reject timeout below minimum", () => {
121
+ expect(() => (0, config_schema_1.validateScanProfile)({
122
+ depth: 2,
123
+ timeout: 500,
124
+ maxPages: 50,
125
+ maxLinksPerPage: 30,
126
+ })).toThrow();
127
+ });
128
+ it("should reject maxPages below minimum", () => {
129
+ expect(() => (0, config_schema_1.validateScanProfile)({
130
+ depth: 2,
131
+ timeout: 30000,
132
+ maxPages: 0,
133
+ maxLinksPerPage: 30,
134
+ })).toThrow();
135
+ });
136
+ it("should accept boundary values", () => {
137
+ expect(() => (0, config_schema_1.validateScanProfile)({
138
+ depth: 1,
139
+ timeout: 1000,
140
+ maxPages: 1,
141
+ maxLinksPerPage: 1,
142
+ })).not.toThrow();
143
+ expect(() => (0, config_schema_1.validateScanProfile)({
144
+ depth: 5,
145
+ timeout: 120000,
146
+ maxPages: 1000,
147
+ maxLinksPerPage: 500,
148
+ })).not.toThrow();
149
+ });
150
+ });
151
+ });
@@ -1,45 +1,26 @@
1
- export type AiProviderName = "openai" | "anthropic";
2
- export type ReportFormat = "word" | "txt" | "json";
3
- export interface Config {
4
- ai: {
5
- provider: AiProviderName;
6
- apiKey: string;
7
- defaultModel: string;
8
- enabled: boolean;
9
- };
10
- scan: {
11
- defaultTimeout: number;
12
- maxThreads: number;
13
- userAgent: string;
14
- followRedirects: boolean;
15
- verifySSL: boolean;
16
- rateLimitPerSecond: number;
17
- strictScope: boolean;
18
- };
19
- report: {
20
- defaultFormat: ReportFormat;
21
- companyName: string;
22
- includeScreenshots: boolean;
23
- severityThreshold: "info" | "low" | "medium" | "high" | "critical";
24
- };
25
- skills: Record<string, {
26
- enabled: boolean;
27
- timeout?: number;
28
- }>;
29
- proxy?: string;
30
- }
1
+ import { Config, ScanProfile } from "./config-schema";
2
+ export type { AiProviderName, ReportFormat, Config } from "./config-schema";
3
+ export { defaultScanProfiles as scanProfiles, validateConfig, validateScanProfile, ScanProfile } from "./config-schema";
4
+ export * from "./errors";
5
+ export declare function isDebugEnabled(): boolean;
31
6
  declare class ConfigStore {
32
7
  private configPath;
33
8
  private data;
9
+ private credentialManager;
34
10
  constructor(projectName: string, defaultConfig: Config);
11
+ initialize(): Promise<void>;
35
12
  get store(): Config;
36
13
  get(key: string): unknown;
37
- set(key: string, value: unknown): void;
14
+ set(key: string, value: unknown): Promise<void>;
38
15
  private save;
16
+ setConfig(config: Config): Promise<void>;
17
+ getScanProfile(name: string): ScanProfile | undefined;
18
+ addScanProfile(name: string, profile: ScanProfile): Promise<void>;
39
19
  }
40
20
  export declare function getConfigStore(): ConfigStore;
41
- export declare function getConfig(): Config;
42
- export declare function getConfigValue(key: string): unknown;
43
- export declare function setConfigValue(key: string, value: unknown): void;
44
- export declare function setConfig(config: Config): void;
45
- export {};
21
+ export declare function getConfig(): Promise<Config>;
22
+ export declare function getConfigValue(key: string): Promise<unknown>;
23
+ export declare function setConfigValue(key: string, value: unknown): Promise<void>;
24
+ export declare function setConfig(config: Config): Promise<void>;
25
+ export declare function getScanProfile(name: string): Promise<ScanProfile | undefined>;
26
+ export declare function addScanProfile(name: string, profile: ScanProfile): Promise<void>;
@@ -32,15 +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;
40
+ exports.isDebugEnabled = isDebugEnabled;
36
41
  exports.getConfigStore = getConfigStore;
37
42
  exports.getConfig = getConfig;
38
43
  exports.getConfigValue = getConfigValue;
39
44
  exports.setConfigValue = setConfigValue;
40
45
  exports.setConfig = setConfig;
46
+ exports.getScanProfile = getScanProfile;
47
+ exports.addScanProfile = addScanProfile;
41
48
  const fs = __importStar(require("fs"));
42
49
  const path = __importStar(require("path"));
43
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);
44
59
  const defaults = {
45
60
  ai: {
46
61
  provider: "openai",
@@ -51,17 +66,19 @@ const defaults = {
51
66
  scan: {
52
67
  defaultTimeout: 60,
53
68
  maxThreads: 5,
54
- userAgent: "KramScan/0.1.0",
69
+ userAgent: `KramScan/${theme_1.CLI_VERSION}`,
55
70
  followRedirects: true,
56
71
  verifySSL: true,
57
72
  rateLimitPerSecond: 5,
58
- strictScope: true
73
+ strictScope: true,
74
+ profiles: config_schema_1.defaultScanProfiles,
75
+ defaultProfile: "balanced",
59
76
  },
60
77
  report: {
61
78
  defaultFormat: "word",
62
79
  companyName: "Your Company",
63
80
  includeScreenshots: false,
64
- severityThreshold: "low"
81
+ severityThreshold: "low",
65
82
  },
66
83
  skills: {
67
84
  sqli: { enabled: true, timeout: 120 },
@@ -69,30 +86,195 @@ const defaults = {
69
86
  headers: { enabled: true },
70
87
  csrf: { enabled: true },
71
88
  idor: { enabled: true },
72
- jwt: { enabled: true }
73
- }
89
+ jwt: { enabled: true },
90
+ },
74
91
  };
75
- // Simple JSON-file config store (replaces ESM-only 'conf' package)
92
+ function isDebugEnabled() {
93
+ return process.env.KRAMSCAN_DEBUG === "true" || process.env.KRAMSCAN_DEBUG === "1";
94
+ }
95
+ // Secure credential manager using OS keychain
96
+ class SecureCredentialManager {
97
+ serviceName;
98
+ useKeychain;
99
+ fallbackPath;
100
+ constructor(serviceName) {
101
+ this.serviceName = serviceName;
102
+ this.useKeychain = this.detectKeychainSupport();
103
+ const configDir = path.join(os.homedir(), `.${serviceName}`);
104
+ if (!fs.existsSync(configDir)) {
105
+ fs.mkdirSync(configDir, { recursive: true });
106
+ }
107
+ this.fallbackPath = path.join(configDir, ".secure");
108
+ }
109
+ detectKeychainSupport() {
110
+ try {
111
+ // Check if we're in a CI environment or if keytar is available
112
+ if (process.env.CI || process.env.KRAMSCAN_DISABLE_KEYCHAIN) {
113
+ return false;
114
+ }
115
+ require("keytar");
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ async getPassword(account) {
123
+ if (this.useKeychain) {
124
+ try {
125
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
126
+ const keytar = await Promise.resolve().then(() => __importStar(require("keytar")));
127
+ return await keytar.getPassword(this.serviceName, account);
128
+ }
129
+ catch (error) {
130
+ // Fallback to file-based storage
131
+ return this.getFromFallback(account);
132
+ }
133
+ }
134
+ return this.getFromFallback(account);
135
+ }
136
+ async setPassword(account, password) {
137
+ if (this.useKeychain) {
138
+ try {
139
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
140
+ const keytar = await Promise.resolve().then(() => __importStar(require("keytar")));
141
+ await keytar.setPassword(this.serviceName, account, password);
142
+ return;
143
+ }
144
+ catch (error) {
145
+ // Fallback to file-based storage
146
+ }
147
+ }
148
+ await this.saveToFallback(account, password);
149
+ }
150
+ async deletePassword(account) {
151
+ if (this.useKeychain) {
152
+ try {
153
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
154
+ const keytar = await Promise.resolve().then(() => __importStar(require("keytar")));
155
+ return await keytar.deletePassword(this.serviceName, account);
156
+ }
157
+ catch (error) {
158
+ return this.deleteFromFallback(account);
159
+ }
160
+ }
161
+ return this.deleteFromFallback(account);
162
+ }
163
+ getFromFallback(account) {
164
+ try {
165
+ if (!fs.existsSync(this.fallbackPath)) {
166
+ return null;
167
+ }
168
+ const data = JSON.parse(fs.readFileSync(this.fallbackPath, "utf-8"));
169
+ const encrypted = data[account];
170
+ if (!encrypted)
171
+ return null;
172
+ return this.decrypt(encrypted);
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ async saveToFallback(account, password) {
179
+ let data = {};
180
+ try {
181
+ if (fs.existsSync(this.fallbackPath)) {
182
+ data = JSON.parse(fs.readFileSync(this.fallbackPath, "utf-8"));
183
+ }
184
+ }
185
+ catch {
186
+ // File doesn't exist or is corrupt, start fresh
187
+ }
188
+ data[account] = this.encrypt(password);
189
+ // Set restrictive permissions (owner read/write only)
190
+ fs.writeFileSync(this.fallbackPath, JSON.stringify(data, null, 2), { mode: 0o600 });
191
+ }
192
+ deleteFromFallback(account) {
193
+ try {
194
+ if (!fs.existsSync(this.fallbackPath)) {
195
+ return false;
196
+ }
197
+ const data = JSON.parse(fs.readFileSync(this.fallbackPath, "utf-8"));
198
+ if (!(account in data)) {
199
+ return false;
200
+ }
201
+ delete data[account];
202
+ fs.writeFileSync(this.fallbackPath, JSON.stringify(data, null, 2), { mode: 0o600 });
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ encrypt(text) {
210
+ // Simple XOR encryption with a machine-specific key
211
+ // This is not high-security but better than plaintext for fallback storage
212
+ const key = this.getMachineKey();
213
+ const buffer = Buffer.from(text);
214
+ const encrypted = Uint8Array.from(buffer, (byte, i) => byte ^ key[i % key.length]);
215
+ return Buffer.from(encrypted).toString("base64");
216
+ }
217
+ decrypt(encrypted) {
218
+ const key = this.getMachineKey();
219
+ const buffer = Buffer.from(encrypted, "base64");
220
+ const decrypted = Uint8Array.from(buffer, (byte, i) => byte ^ key[i % key.length]);
221
+ return Buffer.from(decrypted).toString("utf-8");
222
+ }
223
+ getMachineKey() {
224
+ // Use machine-specific data to generate a key
225
+ // This makes the encrypted data harder to decrypt on other machines
226
+ const data = [
227
+ os.hostname(),
228
+ os.userInfo().username,
229
+ os.platform(),
230
+ ].join("|");
231
+ return Buffer.from(data.repeat(4).slice(0, 32));
232
+ }
233
+ }
234
+ // Simple JSON-file config store with encrypted sensitive values
76
235
  class ConfigStore {
77
236
  configPath;
78
237
  data;
238
+ credentialManager;
79
239
  constructor(projectName, defaultConfig) {
80
240
  const configDir = path.join(os.homedir(), `.${projectName}`);
81
241
  if (!fs.existsSync(configDir)) {
82
242
  fs.mkdirSync(configDir, { recursive: true });
83
243
  }
84
244
  this.configPath = path.join(configDir, "config.json");
245
+ this.credentialManager = new SecureCredentialManager(projectName);
85
246
  if (fs.existsSync(this.configPath)) {
86
247
  try {
87
248
  const raw = fs.readFileSync(this.configPath, "utf-8");
88
- 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
+ }
89
256
  }
90
257
  catch {
91
- this.data = { ...defaultConfig };
258
+ this.data = JSON.parse(JSON.stringify(defaultConfig));
92
259
  }
93
260
  }
94
261
  else {
95
- this.data = { ...defaultConfig };
262
+ this.data = JSON.parse(JSON.stringify(defaultConfig));
263
+ }
264
+ }
265
+ async initialize() {
266
+ // Load API key from secure storage
267
+ const storedKey = await this.credentialManager.getPassword("apiKey");
268
+ if (storedKey) {
269
+ this.data.ai.apiKey = storedKey;
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));
96
278
  }
97
279
  }
98
280
  get store() {
@@ -111,7 +293,7 @@ class ConfigStore {
111
293
  }
112
294
  return current;
113
295
  }
114
- set(key, value) {
296
+ async set(key, value) {
115
297
  const keys = key.split(".");
116
298
  let current = this.data;
117
299
  for (let i = 0; i < keys.length - 1; i++) {
@@ -121,26 +303,85 @@ class ConfigStore {
121
303
  current = current[keys[i]];
122
304
  }
123
305
  current[keys[keys.length - 1]] = value;
124
- this.save();
306
+ // If setting API key, store it securely
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
+ }
314
+ }
315
+ await this.save();
316
+ }
317
+ async save() {
318
+ // Create a copy without the API key for the config file
319
+ const configToSave = {
320
+ ...this.data,
321
+ ai: {
322
+ ...this.data.ai,
323
+ apiKey: "", // Don't save API key in plain text
324
+ }
325
+ };
326
+ fs.writeFileSync(this.configPath, JSON.stringify(configToSave, null, 2), "utf-8");
327
+ }
328
+ async setConfig(config) {
329
+ // Validate before setting
330
+ const validated = (0, config_schema_1.validateConfig)(config);
331
+ Object.assign(this.data, validated);
332
+ // Save API key securely if present
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
+ }
340
+ this.data.ai.apiKey = config.ai.apiKey;
341
+ }
342
+ await this.save();
343
+ }
344
+ getScanProfile(name) {
345
+ return this.data.scan.profiles[name];
125
346
  }
126
- save() {
127
- fs.writeFileSync(this.configPath, JSON.stringify(this.data, null, 2), "utf-8");
347
+ async addScanProfile(name, profile) {
348
+ this.data.scan.profiles[name] = profile;
349
+ await this.save();
128
350
  }
129
351
  }
130
352
  const store = new ConfigStore("kramscan", defaults);
353
+ // Initialize async
354
+ let initialized = false;
355
+ async function ensureInitialized() {
356
+ if (!initialized) {
357
+ await store.initialize();
358
+ initialized = true;
359
+ }
360
+ }
131
361
  function getConfigStore() {
132
362
  return store;
133
363
  }
134
- function getConfig() {
364
+ async function getConfig() {
365
+ await ensureInitialized();
135
366
  return store.store;
136
367
  }
137
- function getConfigValue(key) {
368
+ async function getConfigValue(key) {
369
+ await ensureInitialized();
138
370
  return store.get(key);
139
371
  }
140
- function setConfigValue(key, value) {
141
- store.set(key, value);
372
+ async function setConfigValue(key, value) {
373
+ await ensureInitialized();
374
+ await store.set(key, value);
375
+ }
376
+ async function setConfig(config) {
377
+ await ensureInitialized();
378
+ await store.setConfig(config);
379
+ }
380
+ async function getScanProfile(name) {
381
+ await ensureInitialized();
382
+ return store.getScanProfile(name);
142
383
  }
143
- function setConfig(config) {
144
- Object.assign(store.store, config);
145
- store.save();
384
+ async function addScanProfile(name, profile) {
385
+ await ensureInitialized();
386
+ await store.addScanProfile(name, profile);
146
387
  }
@@ -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;