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.
- package/README.md +392 -87
- package/dist/agent/confirmation.d.ts +38 -0
- package/dist/agent/confirmation.js +210 -0
- package/dist/agent/context.d.ts +81 -0
- package/dist/agent/context.js +227 -0
- package/dist/agent/index.d.ts +10 -0
- package/dist/agent/index.js +32 -0
- package/dist/agent/orchestrator.d.ts +63 -0
- package/dist/agent/orchestrator.js +370 -0
- package/dist/agent/prompts/system.d.ts +6 -0
- package/dist/agent/prompts/system.js +116 -0
- package/dist/agent/skill-registry.d.ts +78 -0
- package/dist/agent/skill-registry.js +202 -0
- package/dist/agent/skills/analyze-findings.d.ts +22 -0
- package/dist/agent/skills/analyze-findings.js +191 -0
- package/dist/agent/skills/generate-report.d.ts +26 -0
- package/dist/agent/skills/generate-report.js +436 -0
- package/dist/agent/skills/health-check.d.ts +28 -0
- package/dist/agent/skills/health-check.js +344 -0
- package/dist/agent/skills/index.d.ts +9 -0
- package/dist/agent/skills/index.js +17 -0
- package/dist/agent/skills/verify-finding.d.ts +17 -0
- package/dist/agent/skills/verify-finding.js +91 -0
- package/dist/agent/skills/web-scan.d.ts +22 -0
- package/dist/agent/skills/web-scan.js +203 -0
- package/dist/agent/types.d.ts +141 -0
- package/dist/agent/types.js +16 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +176 -139
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +250 -0
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +112 -0
- package/dist/commands/analyze.js +104 -55
- package/dist/commands/config.js +63 -37
- package/dist/commands/doctor.js +22 -17
- package/dist/commands/onboard.js +190 -125
- package/dist/commands/report.js +69 -77
- package/dist/commands/scan.js +261 -81
- package/dist/commands/scans.d.ts +2 -0
- package/dist/commands/scans.js +51 -0
- package/dist/core/ai-client.d.ts +7 -2
- package/dist/core/ai-client.js +231 -20
- package/dist/core/ai-payloads.d.ts +17 -0
- package/dist/core/ai-payloads.js +54 -0
- package/dist/core/config-schema.d.ts +197 -0
- package/dist/core/config-schema.js +68 -0
- package/dist/core/config-schema.test.d.ts +1 -0
- package/dist/core/config-schema.test.js +151 -0
- package/dist/core/config.d.ts +17 -36
- package/dist/core/config.js +261 -20
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +162 -0
- package/dist/core/scan-index.d.ts +19 -0
- package/dist/core/scan-index.js +52 -0
- package/dist/core/scan-storage.d.ts +11 -0
- package/dist/core/scan-storage.js +69 -0
- package/dist/core/scanner.d.ts +101 -4
- package/dist/core/scanner.js +432 -63
- package/dist/core/vulnerability-detector.d.ts +18 -2
- package/dist/core/vulnerability-detector.js +349 -38
- package/dist/core/vulnerability-detector.test.d.ts +1 -0
- package/dist/core/vulnerability-detector.test.js +210 -0
- package/dist/index.js +3 -0
- package/dist/plugins/PluginManager.d.ts +27 -0
- package/dist/plugins/PluginManager.js +166 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.js +19 -0
- package/dist/plugins/types.d.ts +55 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
- package/dist/reports/PdfGenerator.d.ts +36 -0
- package/dist/reports/PdfGenerator.js +379 -0
- package/dist/utils/logger.d.ts +33 -1
- package/dist/utils/logger.js +127 -8
- package/dist/utils/theme.d.ts +55 -0
- package/dist/utils/theme.js +195 -0
- package/package.json +27 -6
- package/dist/core/executor.d.ts +0 -2
- package/dist/core/executor.js +0 -74
- package/dist/core/logger.d.ts +0 -12
- package/dist/core/logger.js +0 -51
- package/dist/core/registry.d.ts +0 -3
- package/dist/core/registry.js +0 -35
- package/dist/core/storage.d.ts +0 -4
- package/dist/core/storage.js +0 -39
- package/dist/core/types.d.ts +0 -24
- package/dist/core/types.js +0 -2
- package/dist/skills/base.d.ts +0 -8
- package/dist/skills/base.js +0 -6
- package/dist/skills/builtin.d.ts +0 -4
- package/dist/skills/builtin.js +0 -71
- package/dist/skills/loader.d.ts +0 -2
- package/dist/skills/loader.js +0 -27
- package/dist/skills/types.d.ts +0 -46
- 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
|
+
});
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,45 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
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>;
|
package/dist/core/config.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
258
|
+
this.data = JSON.parse(JSON.stringify(defaultConfig));
|
|
92
259
|
}
|
|
93
260
|
}
|
|
94
261
|
else {
|
|
95
|
-
this.data =
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
store.
|
|
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;
|