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,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,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,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;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -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,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,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,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
|
+
};
|