kramscan 0.1.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/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/kramscan.js +4 -0
- package/bin/openscan.js +4 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +225 -0
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.js +115 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +139 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +234 -0
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.js +146 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +225 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +125 -0
- package/dist/core/ai-client.d.ts +12 -0
- package/dist/core/ai-client.js +89 -0
- package/dist/core/config.d.ts +45 -0
- package/dist/core/config.js +146 -0
- package/dist/core/executor.d.ts +2 -0
- package/dist/core/executor.js +74 -0
- package/dist/core/logger.d.ts +12 -0
- package/dist/core/logger.js +51 -0
- package/dist/core/registry.d.ts +3 -0
- package/dist/core/registry.js +35 -0
- package/dist/core/scanner.d.ts +24 -0
- package/dist/core/scanner.js +197 -0
- package/dist/core/storage.d.ts +4 -0
- package/dist/core/storage.js +39 -0
- package/dist/core/types.d.ts +24 -0
- package/dist/core/types.js +2 -0
- package/dist/core/vulnerability-detector.d.ts +47 -0
- package/dist/core/vulnerability-detector.js +150 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/skills/base.d.ts +8 -0
- package/dist/skills/base.js +6 -0
- package/dist/skills/builtin.d.ts +4 -0
- package/dist/skills/builtin.js +71 -0
- package/dist/skills/loader.d.ts +2 -0
- package/dist/skills/loader.js +27 -0
- package/dist/skills/types.d.ts +46 -0
- package/dist/skills/types.js +2 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +34 -0
- package/package.json +62 -0
|
@@ -0,0 +1,125 @@
|
|
|
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.registerScanCommand = registerScanCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const scanner_1 = require("../core/scanner");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
function registerScanCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command("scan <url>")
|
|
16
|
+
.description("Scan a target URL for vulnerabilities")
|
|
17
|
+
.option("-d, --depth <number>", "Crawl depth", "2")
|
|
18
|
+
.option("-t, --timeout <ms>", "Request timeout", "30000")
|
|
19
|
+
.option("-o, --output <file>", "Save results to file")
|
|
20
|
+
.option("--headless", "Run in headless mode", true)
|
|
21
|
+
.action(async (url, options) => {
|
|
22
|
+
console.log("");
|
|
23
|
+
console.log(chalk_1.default.bold.cyan("🔍 Starting Security Scan"));
|
|
24
|
+
console.log(chalk_1.default.gray("─".repeat(50)));
|
|
25
|
+
console.log("");
|
|
26
|
+
// Validate URL
|
|
27
|
+
try {
|
|
28
|
+
new URL(url);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger_1.logger.error(`Invalid URL: ${url}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const spinner = logger_1.logger.spinner("Initializing scanner...");
|
|
35
|
+
try {
|
|
36
|
+
const scanner = new scanner_1.Scanner();
|
|
37
|
+
spinner.text = `Scanning ${url}...`;
|
|
38
|
+
const result = await scanner.scan(url, {
|
|
39
|
+
depth: parseInt(options.depth),
|
|
40
|
+
timeout: parseInt(options.timeout),
|
|
41
|
+
headless: options.headless,
|
|
42
|
+
});
|
|
43
|
+
spinner.succeed("Scan complete!");
|
|
44
|
+
// Save results
|
|
45
|
+
const scanDir = path_1.default.join(os_1.default.homedir(), ".kramscan", "scans");
|
|
46
|
+
await promises_1.default.mkdir(scanDir, { recursive: true });
|
|
47
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
48
|
+
const filename = options.output || `scan-${timestamp}.json`;
|
|
49
|
+
const filepath = path_1.default.isAbsolute(filename)
|
|
50
|
+
? filename
|
|
51
|
+
: path_1.default.join(scanDir, filename);
|
|
52
|
+
await promises_1.default.writeFile(filepath, JSON.stringify(result, null, 2));
|
|
53
|
+
// Display summary
|
|
54
|
+
console.log("");
|
|
55
|
+
console.log(chalk_1.default.bold("📊 Scan Summary"));
|
|
56
|
+
console.log(chalk_1.default.gray("─".repeat(50)));
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(chalk_1.default.white("Target:"), chalk_1.default.cyan(result.target));
|
|
59
|
+
console.log(chalk_1.default.white("Duration:"), chalk_1.default.cyan(`${(result.duration / 1000).toFixed(2)}s`));
|
|
60
|
+
console.log(chalk_1.default.white("URLs Crawled:"), chalk_1.default.cyan(result.metadata.crawledUrls));
|
|
61
|
+
console.log(chalk_1.default.white("Forms Tested:"), chalk_1.default.cyan(result.metadata.testedForms));
|
|
62
|
+
console.log(chalk_1.default.white("Requests Made:"), chalk_1.default.cyan(result.metadata.requestsMade));
|
|
63
|
+
console.log("");
|
|
64
|
+
// Vulnerability summary
|
|
65
|
+
console.log(chalk_1.default.bold("🛡️ Vulnerabilities Found"));
|
|
66
|
+
console.log(chalk_1.default.gray("─".repeat(50)));
|
|
67
|
+
console.log("");
|
|
68
|
+
const { summary } = result;
|
|
69
|
+
if (summary.total === 0) {
|
|
70
|
+
console.log(chalk_1.default.green("✓ No vulnerabilities found!"));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
if (summary.critical > 0)
|
|
74
|
+
console.log(chalk_1.default.red(` ${summary.critical} Critical`), chalk_1.default.gray("- Immediate action required"));
|
|
75
|
+
if (summary.high > 0)
|
|
76
|
+
console.log(chalk_1.default.red(` ${summary.high} High`), chalk_1.default.gray("- Should be fixed soon"));
|
|
77
|
+
if (summary.medium > 0)
|
|
78
|
+
console.log(chalk_1.default.yellow(` ${summary.medium} Medium`), chalk_1.default.gray("- Fix when possible"));
|
|
79
|
+
if (summary.low > 0)
|
|
80
|
+
console.log(chalk_1.default.blue(` ${summary.low} Low`), chalk_1.default.gray("- Minor issues"));
|
|
81
|
+
if (summary.info > 0)
|
|
82
|
+
console.log(chalk_1.default.gray(` ${summary.info} Info`), chalk_1.default.gray("- Informational"));
|
|
83
|
+
}
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log(chalk_1.default.gray("Results saved to:"), chalk_1.default.white(filepath));
|
|
86
|
+
console.log("");
|
|
87
|
+
// Show top vulnerabilities
|
|
88
|
+
if (result.vulnerabilities.length > 0) {
|
|
89
|
+
console.log(chalk_1.default.bold("🔴 Top Findings"));
|
|
90
|
+
console.log(chalk_1.default.gray("─".repeat(50)));
|
|
91
|
+
console.log("");
|
|
92
|
+
const topVulns = result.vulnerabilities
|
|
93
|
+
.sort((a, b) => {
|
|
94
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
95
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
96
|
+
})
|
|
97
|
+
.slice(0, 5);
|
|
98
|
+
for (const vuln of topVulns) {
|
|
99
|
+
const severityColor = vuln.severity === "critical" || vuln.severity === "high"
|
|
100
|
+
? chalk_1.default.red
|
|
101
|
+
: vuln.severity === "medium"
|
|
102
|
+
? chalk_1.default.yellow
|
|
103
|
+
: chalk_1.default.blue;
|
|
104
|
+
console.log(severityColor(`[${vuln.severity.toUpperCase()}]`), chalk_1.default.bold(vuln.title));
|
|
105
|
+
console.log(chalk_1.default.gray(` ${vuln.url}`));
|
|
106
|
+
console.log(chalk_1.default.white(` ${vuln.description}`));
|
|
107
|
+
console.log("");
|
|
108
|
+
}
|
|
109
|
+
if (result.vulnerabilities.length > 5) {
|
|
110
|
+
console.log(chalk_1.default.gray(` ... and ${result.vulnerabilities.length - 5} more`));
|
|
111
|
+
console.log("");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log(chalk_1.default.cyan("💡 Next steps:"));
|
|
115
|
+
console.log(chalk_1.default.white(` 1. Run ${chalk_1.default.cyan(`kramscan analyze ${filepath}`)} for AI-powered insights`));
|
|
116
|
+
console.log(chalk_1.default.white(` 2. Run ${chalk_1.default.cyan(`kramscan report ${filepath}`)} to generate a report`));
|
|
117
|
+
console.log("");
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
spinner.fail("Scan failed");
|
|
121
|
+
logger_1.logger.error(error.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface AIResponse {
|
|
2
|
+
content: string;
|
|
3
|
+
usage?: {
|
|
4
|
+
promptTokens: number;
|
|
5
|
+
completionTokens: number;
|
|
6
|
+
totalTokens: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface AIClient {
|
|
10
|
+
analyze(prompt: string): Promise<AIResponse>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createAIClient(): AIClient;
|
|
@@ -0,0 +1,89 @@
|
|
|
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.createAIClient = createAIClient;
|
|
7
|
+
const openai_1 = __importDefault(require("openai"));
|
|
8
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
class OpenAIClient {
|
|
11
|
+
client;
|
|
12
|
+
model;
|
|
13
|
+
constructor(apiKey, model) {
|
|
14
|
+
this.client = new openai_1.default({ apiKey });
|
|
15
|
+
this.model = model;
|
|
16
|
+
}
|
|
17
|
+
async analyze(prompt) {
|
|
18
|
+
const response = await this.client.chat.completions.create({
|
|
19
|
+
model: this.model,
|
|
20
|
+
messages: [
|
|
21
|
+
{
|
|
22
|
+
role: "system",
|
|
23
|
+
content: "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
24
|
+
},
|
|
25
|
+
{ role: "user", content: prompt },
|
|
26
|
+
],
|
|
27
|
+
temperature: 0.3,
|
|
28
|
+
});
|
|
29
|
+
const content = response.choices[0]?.message?.content || "";
|
|
30
|
+
return {
|
|
31
|
+
content,
|
|
32
|
+
usage: {
|
|
33
|
+
promptTokens: response.usage?.prompt_tokens || 0,
|
|
34
|
+
completionTokens: response.usage?.completion_tokens || 0,
|
|
35
|
+
totalTokens: response.usage?.total_tokens || 0,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class AnthropicClient {
|
|
41
|
+
client;
|
|
42
|
+
model;
|
|
43
|
+
constructor(apiKey, model) {
|
|
44
|
+
this.client = new sdk_1.default({ apiKey });
|
|
45
|
+
this.model = model;
|
|
46
|
+
}
|
|
47
|
+
async analyze(prompt) {
|
|
48
|
+
const response = await this.client.messages.create({
|
|
49
|
+
model: this.model,
|
|
50
|
+
max_tokens: 4096,
|
|
51
|
+
messages: [
|
|
52
|
+
{
|
|
53
|
+
role: "user",
|
|
54
|
+
content: prompt,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
system: "You are a security expert analyzing web application vulnerabilities. Provide detailed, actionable insights.",
|
|
58
|
+
});
|
|
59
|
+
const content = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
60
|
+
return {
|
|
61
|
+
content,
|
|
62
|
+
usage: {
|
|
63
|
+
promptTokens: response.usage.input_tokens,
|
|
64
|
+
completionTokens: response.usage.output_tokens,
|
|
65
|
+
totalTokens: response.usage.input_tokens + response.usage.output_tokens,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function createAIClient() {
|
|
71
|
+
const config = (0, config_1.getConfig)();
|
|
72
|
+
if (!config.ai.enabled) {
|
|
73
|
+
throw new Error("AI analysis is not enabled. Run 'kramscan onboard' first.");
|
|
74
|
+
}
|
|
75
|
+
const provider = config.ai.provider;
|
|
76
|
+
const apiKey = config.ai.apiKey;
|
|
77
|
+
const model = config.ai.defaultModel;
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
throw new Error(`No API key configured for ${provider}. Run 'kramscan onboard' to set it up.`);
|
|
80
|
+
}
|
|
81
|
+
switch (provider) {
|
|
82
|
+
case "openai":
|
|
83
|
+
return new OpenAIClient(apiKey, model);
|
|
84
|
+
case "anthropic":
|
|
85
|
+
return new AnthropicClient(apiKey, model);
|
|
86
|
+
default:
|
|
87
|
+
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|
|
31
|
+
declare class ConfigStore {
|
|
32
|
+
private configPath;
|
|
33
|
+
private data;
|
|
34
|
+
constructor(projectName: string, defaultConfig: Config);
|
|
35
|
+
get store(): Config;
|
|
36
|
+
get(key: string): unknown;
|
|
37
|
+
set(key: string, value: unknown): void;
|
|
38
|
+
private save;
|
|
39
|
+
}
|
|
40
|
+
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 {};
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getConfigStore = getConfigStore;
|
|
37
|
+
exports.getConfig = getConfig;
|
|
38
|
+
exports.getConfigValue = getConfigValue;
|
|
39
|
+
exports.setConfigValue = setConfigValue;
|
|
40
|
+
exports.setConfig = setConfig;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const defaults = {
|
|
45
|
+
ai: {
|
|
46
|
+
provider: "openai",
|
|
47
|
+
apiKey: "",
|
|
48
|
+
defaultModel: "gpt-4",
|
|
49
|
+
enabled: false,
|
|
50
|
+
},
|
|
51
|
+
scan: {
|
|
52
|
+
defaultTimeout: 60,
|
|
53
|
+
maxThreads: 5,
|
|
54
|
+
userAgent: "KramScan/0.1.0",
|
|
55
|
+
followRedirects: true,
|
|
56
|
+
verifySSL: true,
|
|
57
|
+
rateLimitPerSecond: 5,
|
|
58
|
+
strictScope: true
|
|
59
|
+
},
|
|
60
|
+
report: {
|
|
61
|
+
defaultFormat: "word",
|
|
62
|
+
companyName: "Your Company",
|
|
63
|
+
includeScreenshots: false,
|
|
64
|
+
severityThreshold: "low"
|
|
65
|
+
},
|
|
66
|
+
skills: {
|
|
67
|
+
sqli: { enabled: true, timeout: 120 },
|
|
68
|
+
xss: { enabled: true, timeout: 90 },
|
|
69
|
+
headers: { enabled: true },
|
|
70
|
+
csrf: { enabled: true },
|
|
71
|
+
idor: { enabled: true },
|
|
72
|
+
jwt: { enabled: true }
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
// Simple JSON-file config store (replaces ESM-only 'conf' package)
|
|
76
|
+
class ConfigStore {
|
|
77
|
+
configPath;
|
|
78
|
+
data;
|
|
79
|
+
constructor(projectName, defaultConfig) {
|
|
80
|
+
const configDir = path.join(os.homedir(), `.${projectName}`);
|
|
81
|
+
if (!fs.existsSync(configDir)) {
|
|
82
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
this.configPath = path.join(configDir, "config.json");
|
|
85
|
+
if (fs.existsSync(this.configPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const raw = fs.readFileSync(this.configPath, "utf-8");
|
|
88
|
+
this.data = { ...defaultConfig, ...JSON.parse(raw) };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
this.data = { ...defaultConfig };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.data = { ...defaultConfig };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
get store() {
|
|
99
|
+
return this.data;
|
|
100
|
+
}
|
|
101
|
+
get(key) {
|
|
102
|
+
const keys = key.split(".");
|
|
103
|
+
let current = this.data;
|
|
104
|
+
for (const k of keys) {
|
|
105
|
+
if (current && typeof current === "object" && k in current) {
|
|
106
|
+
current = current[k];
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return current;
|
|
113
|
+
}
|
|
114
|
+
set(key, value) {
|
|
115
|
+
const keys = key.split(".");
|
|
116
|
+
let current = this.data;
|
|
117
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
118
|
+
if (!(keys[i] in current) || typeof current[keys[i]] !== "object") {
|
|
119
|
+
current[keys[i]] = {};
|
|
120
|
+
}
|
|
121
|
+
current = current[keys[i]];
|
|
122
|
+
}
|
|
123
|
+
current[keys[keys.length - 1]] = value;
|
|
124
|
+
this.save();
|
|
125
|
+
}
|
|
126
|
+
save() {
|
|
127
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const store = new ConfigStore("kramscan", defaults);
|
|
131
|
+
function getConfigStore() {
|
|
132
|
+
return store;
|
|
133
|
+
}
|
|
134
|
+
function getConfig() {
|
|
135
|
+
return store.store;
|
|
136
|
+
}
|
|
137
|
+
function getConfigValue(key) {
|
|
138
|
+
return store.get(key);
|
|
139
|
+
}
|
|
140
|
+
function setConfigValue(key, value) {
|
|
141
|
+
store.set(key, value);
|
|
142
|
+
}
|
|
143
|
+
function setConfig(config) {
|
|
144
|
+
Object.assign(store.store, config);
|
|
145
|
+
store.save();
|
|
146
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeScan = executeScan;
|
|
4
|
+
const scanner_1 = require("./scanner");
|
|
5
|
+
const builtin_1 = require("../skills/builtin");
|
|
6
|
+
const registry_1 = require("./registry");
|
|
7
|
+
const logger_1 = require("./logger");
|
|
8
|
+
async function executeScan(targetUrl, options) {
|
|
9
|
+
const logger = (0, logger_1.createLogger)();
|
|
10
|
+
const startedAt = new Date().toISOString();
|
|
11
|
+
const skills = (0, registry_1.listSkills)();
|
|
12
|
+
const selectedSkills = skills
|
|
13
|
+
.map((skill) => skill.id)
|
|
14
|
+
.filter((skillId) => {
|
|
15
|
+
if (options.skills && options.skills.length > 0) {
|
|
16
|
+
return options.skills.includes(skillId);
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
})
|
|
20
|
+
.filter((skillId) => {
|
|
21
|
+
if (options.excludeSkills && options.excludeSkills.length > 0) {
|
|
22
|
+
return !options.excludeSkills.includes(skillId);
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
const scanner = await (0, scanner_1.createScannerClient)({
|
|
27
|
+
userAgent: "OpenScan/0.1.0",
|
|
28
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
29
|
+
proxy: options.proxy,
|
|
30
|
+
verifySSL: true,
|
|
31
|
+
followRedirects: true,
|
|
32
|
+
enableBrowser: options.deep
|
|
33
|
+
});
|
|
34
|
+
const findings = [];
|
|
35
|
+
for (const skillId of selectedSkills) {
|
|
36
|
+
const runner = builtin_1.builtinSkillRunners[skillId];
|
|
37
|
+
if (!runner) {
|
|
38
|
+
logger.warn(`No runner available for skill ${skillId}.`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const result = await runner({
|
|
43
|
+
targetUrl,
|
|
44
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
45
|
+
logger,
|
|
46
|
+
http: {
|
|
47
|
+
get: async (url) => {
|
|
48
|
+
const response = await scanner.http.get(url);
|
|
49
|
+
return {
|
|
50
|
+
data: response.data,
|
|
51
|
+
headers: response.headers
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
findings.push(...result.findings);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error(`Skill ${skillId} failed: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await scanner.close();
|
|
63
|
+
const finishedAt = new Date().toISOString();
|
|
64
|
+
const result = {
|
|
65
|
+
id: `scan-${Date.now()}`,
|
|
66
|
+
startedAt,
|
|
67
|
+
finishedAt,
|
|
68
|
+
target: { url: targetUrl },
|
|
69
|
+
options,
|
|
70
|
+
skillsRun: selectedSkills,
|
|
71
|
+
findings
|
|
72
|
+
};
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
info(message: string): void;
|
|
3
|
+
warn(message: string): void;
|
|
4
|
+
error(message: string): void;
|
|
5
|
+
success(message: string): void;
|
|
6
|
+
spinner(message: string): {
|
|
7
|
+
stop: () => void;
|
|
8
|
+
succeed: (msg?: string) => void;
|
|
9
|
+
fail: (msg?: string) => void;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function createLogger(): Logger;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLogger = createLogger;
|
|
4
|
+
// ANSI color helpers (replaces ESM-only chalk)
|
|
5
|
+
const c = {
|
|
6
|
+
reset: "\x1b[0m",
|
|
7
|
+
cyan: "\x1b[36m",
|
|
8
|
+
yellow: "\x1b[33m",
|
|
9
|
+
red: "\x1b[31m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
gray: "\x1b[90m",
|
|
12
|
+
bold: "\x1b[1m",
|
|
13
|
+
};
|
|
14
|
+
function createLogger() {
|
|
15
|
+
return {
|
|
16
|
+
info(message) {
|
|
17
|
+
console.log(`${c.cyan}ℹ ${message}${c.reset}`);
|
|
18
|
+
},
|
|
19
|
+
warn(message) {
|
|
20
|
+
console.log(`${c.yellow}⚠ ${message}${c.reset}`);
|
|
21
|
+
},
|
|
22
|
+
error(message) {
|
|
23
|
+
console.error(`${c.red}✖ ${message}${c.reset}`);
|
|
24
|
+
},
|
|
25
|
+
success(message) {
|
|
26
|
+
console.log(`${c.green}✔ ${message}${c.reset}`);
|
|
27
|
+
},
|
|
28
|
+
spinner(message) {
|
|
29
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
30
|
+
let i = 0;
|
|
31
|
+
const id = setInterval(() => {
|
|
32
|
+
process.stdout.write(`\r${c.cyan}${frames[i % frames.length]}${c.reset} ${message}`);
|
|
33
|
+
i++;
|
|
34
|
+
}, 80);
|
|
35
|
+
return {
|
|
36
|
+
stop() {
|
|
37
|
+
clearInterval(id);
|
|
38
|
+
process.stdout.write("\r\x1b[K"); // Clear line
|
|
39
|
+
},
|
|
40
|
+
succeed(msg) {
|
|
41
|
+
clearInterval(id);
|
|
42
|
+
console.log(`\r${c.green}✔${c.reset} ${msg || message}`);
|
|
43
|
+
},
|
|
44
|
+
fail(msg) {
|
|
45
|
+
clearInterval(id);
|
|
46
|
+
console.log(`\r${c.red}✖${c.reset} ${msg || message}`);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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.listSkills = listSkills;
|
|
7
|
+
exports.getSkillMetadata = getSkillMetadata;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const loader_1 = require("../skills/loader");
|
|
11
|
+
function getSkillsRoot() {
|
|
12
|
+
return path_1.default.resolve(process.cwd(), "skills");
|
|
13
|
+
}
|
|
14
|
+
function listSkills() {
|
|
15
|
+
const skillsRoot = getSkillsRoot();
|
|
16
|
+
if (!fs_1.default.existsSync(skillsRoot)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const entries = fs_1.default.readdirSync(skillsRoot, { withFileTypes: true });
|
|
20
|
+
const skills = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (!entry.isDirectory()) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const skillDir = path_1.default.join(skillsRoot, entry.name);
|
|
26
|
+
const meta = (0, loader_1.loadSkillMetadata)(skillDir);
|
|
27
|
+
if (meta) {
|
|
28
|
+
skills.push(meta);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return skills;
|
|
32
|
+
}
|
|
33
|
+
function getSkillMetadata(skillId) {
|
|
34
|
+
return listSkills().find((skill) => skill.id === skillId);
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ScanResult } from "./vulnerability-detector";
|
|
2
|
+
export interface ScanOptions {
|
|
3
|
+
depth?: number;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
headless?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare class Scanner {
|
|
8
|
+
private browser;
|
|
9
|
+
private detector;
|
|
10
|
+
private visitedUrls;
|
|
11
|
+
private crawledUrls;
|
|
12
|
+
private testedForms;
|
|
13
|
+
private requestsMade;
|
|
14
|
+
constructor();
|
|
15
|
+
initialize(options?: ScanOptions): Promise<void>;
|
|
16
|
+
scan(targetUrl: string, options?: ScanOptions): Promise<ScanResult>;
|
|
17
|
+
private crawl;
|
|
18
|
+
private testForms;
|
|
19
|
+
private testXSS;
|
|
20
|
+
private testSQLi;
|
|
21
|
+
private buildTestUrl;
|
|
22
|
+
private extractLinks;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|