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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/bin/kramscan.js +4 -0
  4. package/bin/openscan.js +4 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +225 -0
  7. package/dist/commands/analyze.d.ts +2 -0
  8. package/dist/commands/analyze.js +115 -0
  9. package/dist/commands/config.d.ts +2 -0
  10. package/dist/commands/config.js +139 -0
  11. package/dist/commands/doctor.d.ts +2 -0
  12. package/dist/commands/doctor.js +234 -0
  13. package/dist/commands/onboard.d.ts +2 -0
  14. package/dist/commands/onboard.js +146 -0
  15. package/dist/commands/report.d.ts +2 -0
  16. package/dist/commands/report.js +225 -0
  17. package/dist/commands/scan.d.ts +2 -0
  18. package/dist/commands/scan.js +125 -0
  19. package/dist/core/ai-client.d.ts +12 -0
  20. package/dist/core/ai-client.js +89 -0
  21. package/dist/core/config.d.ts +45 -0
  22. package/dist/core/config.js +146 -0
  23. package/dist/core/executor.d.ts +2 -0
  24. package/dist/core/executor.js +74 -0
  25. package/dist/core/logger.d.ts +12 -0
  26. package/dist/core/logger.js +51 -0
  27. package/dist/core/registry.d.ts +3 -0
  28. package/dist/core/registry.js +35 -0
  29. package/dist/core/scanner.d.ts +24 -0
  30. package/dist/core/scanner.js +197 -0
  31. package/dist/core/storage.d.ts +4 -0
  32. package/dist/core/storage.js +39 -0
  33. package/dist/core/types.d.ts +24 -0
  34. package/dist/core/types.js +2 -0
  35. package/dist/core/vulnerability-detector.d.ts +47 -0
  36. package/dist/core/vulnerability-detector.js +150 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +7 -0
  39. package/dist/skills/base.d.ts +8 -0
  40. package/dist/skills/base.js +6 -0
  41. package/dist/skills/builtin.d.ts +4 -0
  42. package/dist/skills/builtin.js +71 -0
  43. package/dist/skills/loader.d.ts +2 -0
  44. package/dist/skills/loader.js +27 -0
  45. package/dist/skills/types.d.ts +46 -0
  46. package/dist/skills/types.js +2 -0
  47. package/dist/utils/logger.d.ts +9 -0
  48. package/dist/utils/logger.js +34 -0
  49. package/package.json +62 -0
@@ -0,0 +1,197 @@
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.Scanner = void 0;
7
+ const puppeteer_1 = __importDefault(require("puppeteer"));
8
+ const vulnerability_detector_1 = require("./vulnerability-detector");
9
+ const config_1 = require("./config");
10
+ const logger_1 = require("../utils/logger");
11
+ class Scanner {
12
+ browser = null;
13
+ detector;
14
+ visitedUrls = new Set();
15
+ crawledUrls = 0;
16
+ testedForms = 0;
17
+ requestsMade = 0;
18
+ constructor() {
19
+ this.detector = new vulnerability_detector_1.VulnerabilityDetector();
20
+ }
21
+ async initialize(options = {}) {
22
+ const config = (0, config_1.getConfig)();
23
+ const headless = options.headless ?? true;
24
+ this.browser = await puppeteer_1.default.launch({
25
+ headless,
26
+ args: [
27
+ "--no-sandbox",
28
+ "--disable-setuid-sandbox",
29
+ "--disable-web-security",
30
+ "--disable-features=IsolateOrigins,site-per-process",
31
+ ],
32
+ });
33
+ logger_1.logger.debug("Browser initialized");
34
+ }
35
+ async scan(targetUrl, options = {}) {
36
+ const startTime = Date.now();
37
+ const depth = options.depth ?? 2;
38
+ const timeout = options.timeout ?? 30000;
39
+ if (!this.browser) {
40
+ await this.initialize(options);
41
+ }
42
+ logger_1.logger.info(`Starting scan of ${targetUrl} (depth: ${depth}, timeout: ${timeout}ms)`);
43
+ // Start crawling
44
+ await this.crawl(targetUrl, depth, timeout);
45
+ // Close browser
46
+ await this.close();
47
+ const duration = Date.now() - startTime;
48
+ const result = {
49
+ target: targetUrl,
50
+ timestamp: new Date().toISOString(),
51
+ duration,
52
+ vulnerabilities: this.detector.getVulnerabilities(),
53
+ summary: this.detector.getSummary(),
54
+ metadata: {
55
+ crawledUrls: this.crawledUrls,
56
+ testedForms: this.testedForms,
57
+ requestsMade: this.requestsMade,
58
+ },
59
+ };
60
+ return result;
61
+ }
62
+ async crawl(url, depth, timeout) {
63
+ if (depth === 0 || this.visitedUrls.has(url)) {
64
+ return;
65
+ }
66
+ this.visitedUrls.add(url);
67
+ this.crawledUrls++;
68
+ const page = await this.browser.newPage();
69
+ try {
70
+ // Set up request interception
71
+ await page.setRequestInterception(true);
72
+ page.on("request", (request) => {
73
+ this.requestsMade++;
74
+ request.continue();
75
+ });
76
+ // Navigate to page
77
+ const response = await page.goto(url, {
78
+ waitUntil: "networkidle2",
79
+ timeout,
80
+ });
81
+ if (!response) {
82
+ logger_1.logger.warn(`No response from ${url}`);
83
+ return;
84
+ }
85
+ // Get response headers
86
+ const headers = response.headers();
87
+ this.detector.analyzeSecurityHeaders(url, headers);
88
+ // Get page content
89
+ const content = await page.content();
90
+ // Check for sensitive data
91
+ this.detector.detectSensitiveData(url, content);
92
+ // Find and test forms
93
+ await this.testForms(page, url, timeout);
94
+ // Find links and crawl deeper
95
+ if (depth > 1) {
96
+ const links = await this.extractLinks(page, url);
97
+ for (const link of links.slice(0, 10)) {
98
+ // Limit to 10 links per page
99
+ await this.crawl(link, depth - 1, timeout);
100
+ }
101
+ }
102
+ }
103
+ catch (error) {
104
+ logger_1.logger.error(`Error crawling ${url}: ${error.message}`);
105
+ }
106
+ finally {
107
+ await page.close();
108
+ }
109
+ }
110
+ async testForms(page, url, timeout) {
111
+ const forms = await page.$$("form");
112
+ for (const form of forms) {
113
+ this.testedForms++;
114
+ // Get form HTML for CSRF detection
115
+ const formHtml = await form.evaluate((el) => el.outerHTML);
116
+ this.detector.detectCSRF(url, formHtml);
117
+ // Find input fields
118
+ const inputs = await form.$$("input, textarea");
119
+ for (const input of inputs) {
120
+ const name = await input.evaluate((el) => el.getAttribute("name"));
121
+ const type = await input.evaluate((el) => el.getAttribute("type"));
122
+ if (!name || type === "hidden" || type === "submit") {
123
+ continue;
124
+ }
125
+ // Test for XSS
126
+ await this.testXSS(page, url, name, timeout);
127
+ // Test for SQLi
128
+ await this.testSQLi(page, url, name, timeout);
129
+ }
130
+ }
131
+ }
132
+ async testXSS(page, url, param, timeout) {
133
+ const payloads = [
134
+ "<script>alert('XSS')</script>",
135
+ '"><script>alert(1)</script>',
136
+ "<img src=x onerror=alert(1)>",
137
+ ];
138
+ for (const payload of payloads) {
139
+ try {
140
+ // Try to inject payload
141
+ const testUrl = this.buildTestUrl(url, param, payload);
142
+ await page.goto(testUrl, { waitUntil: "networkidle2", timeout });
143
+ const content = await page.content();
144
+ this.detector.detectXSS(url, param, payload, content);
145
+ }
146
+ catch (error) {
147
+ logger_1.logger.debug(`XSS test failed for ${param}: ${error.message}`);
148
+ }
149
+ }
150
+ }
151
+ async testSQLi(page, url, param, timeout) {
152
+ const payloads = ["'", "1' OR '1'='1", "1; DROP TABLE users--", "' OR 1=1--"];
153
+ for (const payload of payloads) {
154
+ try {
155
+ const testUrl = this.buildTestUrl(url, param, payload);
156
+ await page.goto(testUrl, { waitUntil: "networkidle2", timeout });
157
+ const content = await page.content();
158
+ this.detector.detectSQLi(url, param, content);
159
+ }
160
+ catch (error) {
161
+ logger_1.logger.debug(`SQLi test failed for ${param}: ${error.message}`);
162
+ }
163
+ }
164
+ }
165
+ buildTestUrl(baseUrl, param, value) {
166
+ const url = new URL(baseUrl);
167
+ url.searchParams.set(param, value);
168
+ return url.toString();
169
+ }
170
+ async extractLinks(page, baseUrl) {
171
+ const links = await page.$$eval("a[href]", (anchors) => anchors.map((a) => a.getAttribute("href")).filter(Boolean));
172
+ const base = new URL(baseUrl);
173
+ const absoluteLinks = [];
174
+ for (const link of links) {
175
+ if (!link)
176
+ continue;
177
+ try {
178
+ const absolute = new URL(link, baseUrl);
179
+ // Only crawl same-origin links
180
+ if (absolute.origin === base.origin) {
181
+ absoluteLinks.push(absolute.toString());
182
+ }
183
+ }
184
+ catch {
185
+ // Invalid URL, skip
186
+ }
187
+ }
188
+ return [...new Set(absoluteLinks)]; // Remove duplicates
189
+ }
190
+ async close() {
191
+ if (this.browser) {
192
+ await this.browser.close();
193
+ this.browser = null;
194
+ }
195
+ }
196
+ }
197
+ exports.Scanner = Scanner;
@@ -0,0 +1,4 @@
1
+ import { ScanResult } from "./types";
2
+ export declare function saveScanResult(result: ScanResult): string;
3
+ export declare function loadLastScanResult(): ScanResult | null;
4
+ export declare function loadScanResultFromFile(filePath: string): ScanResult;
@@ -0,0 +1,39 @@
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.saveScanResult = saveScanResult;
7
+ exports.loadLastScanResult = loadLastScanResult;
8
+ exports.loadScanResultFromFile = loadScanResultFromFile;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const ROOT_DIR = path_1.default.join(os_1.default.homedir(), ".openscan");
13
+ const SCANS_DIR = path_1.default.join(ROOT_DIR, "scans");
14
+ const LAST_SCAN_PATH = path_1.default.join(SCANS_DIR, "last.json");
15
+ function ensureDir(dirPath) {
16
+ if (!fs_1.default.existsSync(dirPath)) {
17
+ fs_1.default.mkdirSync(dirPath, { recursive: true });
18
+ }
19
+ }
20
+ function saveScanResult(result) {
21
+ ensureDir(SCANS_DIR);
22
+ const filename = `${result.id}.json`;
23
+ const filePath = path_1.default.join(SCANS_DIR, filename);
24
+ const payload = JSON.stringify(result, null, 2);
25
+ fs_1.default.writeFileSync(filePath, payload, "utf-8");
26
+ fs_1.default.writeFileSync(LAST_SCAN_PATH, payload, "utf-8");
27
+ return filePath;
28
+ }
29
+ function loadLastScanResult() {
30
+ if (!fs_1.default.existsSync(LAST_SCAN_PATH)) {
31
+ return null;
32
+ }
33
+ const raw = fs_1.default.readFileSync(LAST_SCAN_PATH, "utf-8");
34
+ return JSON.parse(raw);
35
+ }
36
+ function loadScanResultFromFile(filePath) {
37
+ const raw = fs_1.default.readFileSync(filePath, "utf-8");
38
+ return JSON.parse(raw);
39
+ }
@@ -0,0 +1,24 @@
1
+ import { Finding } from "../skills/types";
2
+ export interface ScanTarget {
3
+ url: string;
4
+ }
5
+ export interface ScanOptions {
6
+ deep: boolean;
7
+ timeoutSeconds: number;
8
+ threads: number;
9
+ skills?: string[];
10
+ excludeSkills?: string[];
11
+ proxy?: string;
12
+ aiEnabled?: boolean;
13
+ aiModel?: string;
14
+ }
15
+ export interface ScanResult {
16
+ id: string;
17
+ startedAt: string;
18
+ finishedAt: string;
19
+ target: ScanTarget;
20
+ options: ScanOptions;
21
+ skillsRun: string[];
22
+ findings: Finding[];
23
+ aiSummary?: string;
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,47 @@
1
+ export interface Vulnerability {
2
+ type: "xss" | "sqli" | "csrf" | "header" | "sensitive_data" | "other";
3
+ severity: "critical" | "high" | "medium" | "low" | "info";
4
+ title: string;
5
+ description: string;
6
+ url: string;
7
+ evidence?: string;
8
+ remediation?: string;
9
+ cwe?: string;
10
+ }
11
+ export interface ScanResult {
12
+ target: string;
13
+ timestamp: string;
14
+ duration: number;
15
+ vulnerabilities: Vulnerability[];
16
+ summary: {
17
+ total: number;
18
+ critical: number;
19
+ high: number;
20
+ medium: number;
21
+ low: number;
22
+ info: number;
23
+ };
24
+ metadata: {
25
+ crawledUrls: number;
26
+ testedForms: number;
27
+ requestsMade: number;
28
+ };
29
+ }
30
+ export declare class VulnerabilityDetector {
31
+ private vulnerabilities;
32
+ detectXSS(url: string, param: string, payload: string, response: string): void;
33
+ detectSQLi(url: string, param: string, errorResponse: string): void;
34
+ detectCSRF(url: string, formHtml: string): void;
35
+ analyzeSecurityHeaders(url: string, headers: Record<string, string>): void;
36
+ detectSensitiveData(url: string, response: string): void;
37
+ getVulnerabilities(): Vulnerability[];
38
+ getSummary(): {
39
+ total: number;
40
+ critical: number;
41
+ high: number;
42
+ medium: number;
43
+ low: number;
44
+ info: number;
45
+ };
46
+ clear(): void;
47
+ }
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VulnerabilityDetector = void 0;
4
+ class VulnerabilityDetector {
5
+ vulnerabilities = [];
6
+ // XSS Detection
7
+ detectXSS(url, param, payload, response) {
8
+ // Check if payload is reflected in response
9
+ if (response.includes(payload)) {
10
+ this.vulnerabilities.push({
11
+ type: "xss",
12
+ severity: "high",
13
+ title: "Reflected Cross-Site Scripting (XSS)",
14
+ description: `The parameter '${param}' appears to be vulnerable to XSS. The payload was reflected in the response without proper encoding.`,
15
+ url,
16
+ evidence: `Payload: ${payload}`,
17
+ remediation: "Implement proper output encoding/escaping for user-controlled data. Use Content Security Policy (CSP) headers.",
18
+ cwe: "CWE-79",
19
+ });
20
+ }
21
+ }
22
+ // SQL Injection Detection
23
+ detectSQLi(url, param, errorResponse) {
24
+ const sqlErrors = [
25
+ "SQL syntax",
26
+ "mysql_fetch",
27
+ "ORA-",
28
+ "PostgreSQL",
29
+ "SQLite",
30
+ "ODBC",
31
+ "JET Database",
32
+ "Microsoft Access Driver",
33
+ ];
34
+ for (const error of sqlErrors) {
35
+ if (errorResponse.includes(error)) {
36
+ this.vulnerabilities.push({
37
+ type: "sqli",
38
+ severity: "critical",
39
+ title: "SQL Injection",
40
+ description: `The parameter '${param}' may be vulnerable to SQL injection. Database error messages were detected in the response.`,
41
+ url,
42
+ evidence: `Error pattern: ${error}`,
43
+ remediation: "Use parameterized queries or prepared statements. Never concatenate user input directly into SQL queries.",
44
+ cwe: "CWE-89",
45
+ });
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ // CSRF Detection
51
+ detectCSRF(url, formHtml) {
52
+ const hasCSRFToken = formHtml.includes('name="csrf') ||
53
+ formHtml.includes('name="_token') ||
54
+ formHtml.includes('name="authenticity_token');
55
+ if (!hasCSRFToken) {
56
+ this.vulnerabilities.push({
57
+ type: "csrf",
58
+ severity: "medium",
59
+ title: "Missing CSRF Protection",
60
+ description: "The form does not appear to have CSRF token protection. This could allow attackers to perform unauthorized actions on behalf of authenticated users.",
61
+ url,
62
+ remediation: "Implement CSRF tokens for all state-changing operations. Use SameSite cookie attribute.",
63
+ cwe: "CWE-352",
64
+ });
65
+ }
66
+ }
67
+ // Security Headers Analysis
68
+ analyzeSecurityHeaders(url, headers) {
69
+ const requiredHeaders = {
70
+ "content-security-policy": {
71
+ title: "Missing Content-Security-Policy",
72
+ severity: "medium",
73
+ remediation: "Implement a strict Content-Security-Policy to prevent XSS and data injection attacks.",
74
+ },
75
+ "x-frame-options": {
76
+ title: "Missing X-Frame-Options",
77
+ severity: "medium",
78
+ remediation: "Set X-Frame-Options to DENY or SAMEORIGIN to prevent clickjacking attacks.",
79
+ },
80
+ "strict-transport-security": {
81
+ title: "Missing Strict-Transport-Security",
82
+ severity: "medium",
83
+ remediation: "Enable HSTS to force HTTPS connections and prevent protocol downgrade attacks.",
84
+ },
85
+ "x-content-type-options": {
86
+ title: "Missing X-Content-Type-Options",
87
+ severity: "low",
88
+ remediation: "Set X-Content-Type-Options to 'nosniff' to prevent MIME-sniffing attacks.",
89
+ },
90
+ };
91
+ for (const [header, config] of Object.entries(requiredHeaders)) {
92
+ if (!headers[header.toLowerCase()]) {
93
+ this.vulnerabilities.push({
94
+ type: "header",
95
+ severity: config.severity,
96
+ title: config.title,
97
+ description: `The ${header} security header is not set.`,
98
+ url,
99
+ remediation: config.remediation,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ // Sensitive Data Detection
105
+ detectSensitiveData(url, response) {
106
+ const patterns = [
107
+ { regex: /api[_-]?key["\s:=]+[\w-]{20,}/gi, name: "API Key" },
108
+ { regex: /sk-[a-zA-Z0-9]{48}/g, name: "OpenAI API Key" },
109
+ { regex: /ghp_[a-zA-Z0-9]{36}/g, name: "GitHub Token" },
110
+ { regex: /AKIA[0-9A-Z]{16}/g, name: "AWS Access Key" },
111
+ { regex: /password["\s:=]+[^\s"]{8,}/gi, name: "Password" },
112
+ ];
113
+ for (const pattern of patterns) {
114
+ const matches = response.match(pattern.regex);
115
+ if (matches && matches.length > 0) {
116
+ this.vulnerabilities.push({
117
+ type: "sensitive_data",
118
+ severity: "high",
119
+ title: `Exposed ${pattern.name}`,
120
+ description: `Potential ${pattern.name} found in the response. Sensitive data should never be exposed in client-side code.`,
121
+ url,
122
+ evidence: `Pattern matched: ${matches[0].substring(0, 20)}...`,
123
+ remediation: "Remove sensitive data from responses. Use environment variables and secure secret management.",
124
+ cwe: "CWE-200",
125
+ });
126
+ }
127
+ }
128
+ }
129
+ getVulnerabilities() {
130
+ return this.vulnerabilities;
131
+ }
132
+ getSummary() {
133
+ const summary = {
134
+ total: this.vulnerabilities.length,
135
+ critical: 0,
136
+ high: 0,
137
+ medium: 0,
138
+ low: 0,
139
+ info: 0,
140
+ };
141
+ for (const vuln of this.vulnerabilities) {
142
+ summary[vuln.severity]++;
143
+ }
144
+ return summary;
145
+ }
146
+ clear() {
147
+ this.vulnerabilities = [];
148
+ }
149
+ }
150
+ exports.VulnerabilityDetector = VulnerabilityDetector;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const cli_1 = require("./cli");
4
+ (0, cli_1.run)().catch((err) => {
5
+ console.error("Fatal error:", err);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,8 @@
1
+ import { Skill, SkillContext, SkillResult } from "./types";
2
+ export declare abstract class BaseSkill implements Skill {
3
+ abstract id: string;
4
+ abstract name: string;
5
+ abstract description: string;
6
+ abstract tags: string[];
7
+ abstract run(context: SkillContext): Promise<SkillResult>;
8
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseSkill = void 0;
4
+ class BaseSkill {
5
+ }
6
+ exports.BaseSkill = BaseSkill;
@@ -0,0 +1,4 @@
1
+ import { SkillContext, SkillResult } from "./types";
2
+ type SkillRunner = (context: SkillContext) => Promise<SkillResult>;
3
+ export declare const builtinSkillRunners: Record<string, SkillRunner>;
4
+ export {};
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.builtinSkillRunners = void 0;
4
+ async function runHeadersSkill(context) {
5
+ const response = await context.http.get(context.targetUrl);
6
+ const headers = {};
7
+ for (const [key, value] of Object.entries(response.headers)) {
8
+ headers[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : String(value);
9
+ }
10
+ const checks = [
11
+ {
12
+ key: "strict-transport-security",
13
+ title: "Missing HSTS header",
14
+ recommendation: "Enable Strict-Transport-Security with a long max-age and includeSubDomains."
15
+ },
16
+ {
17
+ key: "content-security-policy",
18
+ title: "Missing Content-Security-Policy header",
19
+ recommendation: "Define a CSP to restrict scripts, styles, and frames."
20
+ },
21
+ {
22
+ key: "x-frame-options",
23
+ title: "Missing X-Frame-Options header",
24
+ recommendation: "Set X-Frame-Options to DENY or SAMEORIGIN."
25
+ },
26
+ {
27
+ key: "x-content-type-options",
28
+ title: "Missing X-Content-Type-Options header",
29
+ recommendation: "Set X-Content-Type-Options to nosniff."
30
+ },
31
+ {
32
+ key: "referrer-policy",
33
+ title: "Missing Referrer-Policy header",
34
+ recommendation: "Set Referrer-Policy to strict-origin-when-cross-origin or similar."
35
+ },
36
+ {
37
+ key: "permissions-policy",
38
+ title: "Missing Permissions-Policy header",
39
+ recommendation: "Set Permissions-Policy to restrict sensitive browser APIs."
40
+ }
41
+ ];
42
+ const findings = checks
43
+ .filter((check) => !headers[check.key])
44
+ .map((check) => ({
45
+ id: `headers-${check.key}`,
46
+ skillId: "headers",
47
+ title: check.title,
48
+ severity: "low",
49
+ description: `The response did not include the ${check.key} header.`,
50
+ recommendation: check.recommendation
51
+ }));
52
+ return {
53
+ skillId: "headers",
54
+ findings
55
+ };
56
+ }
57
+ async function runPlaceholderSkill(skillId, context) {
58
+ context.logger.warn(`${skillId} is not implemented yet. Returning no findings.`);
59
+ return {
60
+ skillId,
61
+ findings: []
62
+ };
63
+ }
64
+ exports.builtinSkillRunners = {
65
+ headers: runHeadersSkill,
66
+ sqli: (context) => runPlaceholderSkill("sqli", context),
67
+ xss: (context) => runPlaceholderSkill("xss", context),
68
+ csrf: (context) => runPlaceholderSkill("csrf", context),
69
+ idor: (context) => runPlaceholderSkill("idor", context),
70
+ jwt: (context) => runPlaceholderSkill("jwt", context)
71
+ };
@@ -0,0 +1,2 @@
1
+ import { SkillMetadata } from "./types";
2
+ export declare function loadSkillMetadata(skillDir: string): SkillMetadata | null;
@@ -0,0 +1,27 @@
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.loadSkillMetadata = loadSkillMetadata;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ function loadSkillMetadata(skillDir) {
11
+ const skillPath = path_1.default.join(skillDir, "skill.yaml");
12
+ if (!fs_1.default.existsSync(skillPath)) {
13
+ return null;
14
+ }
15
+ const raw = fs_1.default.readFileSync(skillPath, "utf-8");
16
+ const doc = js_yaml_1.default.load(raw);
17
+ if (!doc || !doc.id || !doc.name) {
18
+ return null;
19
+ }
20
+ return {
21
+ id: doc.id,
22
+ name: doc.name,
23
+ description: doc.description ?? "",
24
+ tags: doc.tags ?? [],
25
+ category: doc.category
26
+ };
27
+ }
@@ -0,0 +1,46 @@
1
+ export type Severity = "info" | "low" | "medium" | "high" | "critical";
2
+ export interface Finding {
3
+ id: string;
4
+ skillId: string;
5
+ title: string;
6
+ severity: Severity;
7
+ description: string;
8
+ evidence?: string;
9
+ recommendation?: string;
10
+ references?: string[];
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+ export interface SkillMetadata {
14
+ id: string;
15
+ name: string;
16
+ description: string;
17
+ tags: string[];
18
+ category?: string;
19
+ }
20
+ export interface SkillContext {
21
+ targetUrl: string;
22
+ timeoutSeconds: number;
23
+ logger: {
24
+ info(message: string): void;
25
+ warn(message: string): void;
26
+ error(message: string): void;
27
+ };
28
+ http: {
29
+ get: <T = unknown>(url: string) => Promise<{
30
+ data: T;
31
+ headers: Record<string, string>;
32
+ }>;
33
+ };
34
+ }
35
+ export interface SkillResult {
36
+ skillId: string;
37
+ findings: Finding[];
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+ export interface Skill {
41
+ id: string;
42
+ name: string;
43
+ description: string;
44
+ tags: string[];
45
+ run(context: SkillContext): Promise<SkillResult>;
46
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ import { Ora } from "ora";
2
+ export declare const logger: {
3
+ info: (message: string) => void;
4
+ success: (message: string) => void;
5
+ warn: (message: string) => void;
6
+ error: (message: string) => void;
7
+ debug: (message: string) => void;
8
+ spinner: (text: string) => Ora;
9
+ };