opencode-api-security-testing 5.2.1 → 5.2.3
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/package.json +48 -48
- package/postinstall.mjs +243 -39
- package/src/index.ts +188 -4
- package/src/tools/endpoint-discover.ts +325 -0
- package/src/tools/report-generator.ts +355 -0
- package/src/utils/env-checker.ts +264 -0
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "opencode-api-security-testing",
|
|
3
|
-
"version": "5.2.
|
|
4
|
-
"description": "API Security Testing Plugin for OpenCode - Automated vulnerability scanning and penetration testing",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/index.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"src/",
|
|
9
|
-
"agents/",
|
|
10
|
-
"core/",
|
|
11
|
-
"references/",
|
|
12
|
-
"SKILL.md",
|
|
13
|
-
"postinstall.mjs",
|
|
14
|
-
"preuninstall.mjs"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"postinstall": "node postinstall.mjs",
|
|
18
|
-
"preuninstall": "node preuninstall.mjs",
|
|
19
|
-
"init": "node init.mjs"
|
|
20
|
-
},
|
|
21
|
-
"keywords": [
|
|
22
|
-
"opencode",
|
|
23
|
-
"opencode-plugin",
|
|
24
|
-
"security",
|
|
25
|
-
"api-security",
|
|
26
|
-
"pentest",
|
|
27
|
-
"vulnerability-scanning"
|
|
28
|
-
],
|
|
29
|
-
"author": "steveopen1",
|
|
30
|
-
"license": "MIT",
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "https://github.com/steveopen1/skill-play"
|
|
34
|
-
},
|
|
35
|
-
"homepage": "https://github.com/steveopen1/skill-play/tree/main/agent-plugins/OPENCODE/api-security-testing",
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"@opencode-ai/plugin": "^1.1.19",
|
|
38
|
-
"@opencode-ai/sdk": "^1.1.19"
|
|
39
|
-
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@opencode-ai/plugin": "^1.1.19",
|
|
42
|
-
"@opencode-ai/sdk": "^1.1.19"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@types/node": "^25.5.2",
|
|
46
|
-
"typescript": "^6.0.2"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-api-security-testing",
|
|
3
|
+
"version": "5.2.3",
|
|
4
|
+
"description": "API Security Testing Plugin for OpenCode - Automated vulnerability scanning and penetration testing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/",
|
|
9
|
+
"agents/",
|
|
10
|
+
"core/",
|
|
11
|
+
"references/",
|
|
12
|
+
"SKILL.md",
|
|
13
|
+
"postinstall.mjs",
|
|
14
|
+
"preuninstall.mjs"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node postinstall.mjs",
|
|
18
|
+
"preuninstall": "node preuninstall.mjs",
|
|
19
|
+
"init": "node init.mjs"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"opencode",
|
|
23
|
+
"opencode-plugin",
|
|
24
|
+
"security",
|
|
25
|
+
"api-security",
|
|
26
|
+
"pentest",
|
|
27
|
+
"vulnerability-scanning"
|
|
28
|
+
],
|
|
29
|
+
"author": "steveopen1",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/steveopen1/skill-play"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/steveopen1/skill-play/tree/main/agent-plugins/OPENCODE/api-security-testing",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@opencode-ai/plugin": "^1.1.19",
|
|
38
|
+
"@opencode-ai/sdk": "^1.1.19"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@opencode-ai/plugin": "^1.1.19",
|
|
42
|
+
"@opencode-ai/sdk": "^1.1.19"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.5.2",
|
|
46
|
+
"typescript": "^6.0.2"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/postinstall.mjs
CHANGED
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
* postinstall.mjs - API Security Testing Plugin
|
|
5
5
|
*
|
|
6
6
|
* Installs:
|
|
7
|
-
* 1. agents to ~/.
|
|
8
|
-
* 2.
|
|
7
|
+
* 1. agents to ~/.claude/agents/ (oh-my-opencode discovery path)
|
|
8
|
+
* 2. agents to ~/.config/opencode/agents/ (OpenCode native discovery path)
|
|
9
|
+
* 3. SKILL.md and references to ~/.config/opencode/skills/api-security-testing/
|
|
10
|
+
* 4. Auto-detects and installs Python dependencies (requests, beautifulsoup4, playwright)
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
12
14
|
import { join } from "node:path";
|
|
13
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import { platform } from "node:os";
|
|
14
18
|
|
|
15
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
20
|
const __dirname = join(__filename, "..");
|
|
@@ -20,29 +24,241 @@ function getOpencodeBaseDir() {
|
|
|
20
24
|
return join(home, ".config", "opencode");
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
function getClaudeBaseDir() {
|
|
28
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
29
|
+
return join(home, ".claude");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function copyDirRecursive(src, dest) {
|
|
33
|
+
if (!existsSync(dest)) {
|
|
34
|
+
mkdirSync(dest, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
const items = readdirSync(src, { withFileTypes: true });
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
const srcPath = join(src, item.name);
|
|
40
|
+
const destPath = join(dest, item.name);
|
|
41
|
+
try {
|
|
42
|
+
if (item.isDirectory()) {
|
|
43
|
+
copyDirRecursive(srcPath, destPath);
|
|
44
|
+
} else {
|
|
45
|
+
copyFileSync(srcPath, destPath);
|
|
46
|
+
count++;
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(` ✗ ${item.name}: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return count;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run shell command safely
|
|
57
|
+
*/
|
|
58
|
+
function runCommand(cmd, timeout = 30000) {
|
|
59
|
+
try {
|
|
60
|
+
const output = execSync(cmd, {
|
|
61
|
+
encoding: "utf-8",
|
|
62
|
+
timeout,
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
64
|
+
});
|
|
65
|
+
return { success: true, output: output.trim(), error: "" };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
output: error.stdout || "",
|
|
70
|
+
error: error.stderr || error.message || "Unknown error"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check Python availability
|
|
77
|
+
*/
|
|
78
|
+
function checkPython() {
|
|
79
|
+
for (const cmd of ["python3 --version", "python --version"]) {
|
|
80
|
+
const result = runCommand(cmd, 5000);
|
|
81
|
+
if (result.success) {
|
|
82
|
+
const versionMatch = result.output.match(/Python\s+(\d+\.\d+\.\d+)/i) ||
|
|
83
|
+
result.error.match(/Python\s+(\d+\.\d+\.\d+)/i);
|
|
84
|
+
return { available: true, version: versionMatch ? versionMatch[1] : "unknown", cmd: cmd.split(" ")[0] };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { available: false, version: null, cmd: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if pip is available
|
|
92
|
+
*/
|
|
93
|
+
function checkPip(pythonCmd) {
|
|
94
|
+
const cmds = [
|
|
95
|
+
`${pythonCmd} -m pip --version`,
|
|
96
|
+
"pip3 --version",
|
|
97
|
+
"pip --version"
|
|
98
|
+
];
|
|
99
|
+
for (const cmd of cmds) {
|
|
100
|
+
const result = runCommand(cmd, 5000);
|
|
101
|
+
if (result.success) return cmd.includes("-m pip") ? `${pythonCmd} -m pip` : cmd.split(" ")[0];
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a Python package is installed
|
|
108
|
+
*/
|
|
109
|
+
function checkPythonPackage(pythonCmd, packageName) {
|
|
110
|
+
const result = runCommand(`${pythonCmd} -c "import ${packageName}"`, 5000);
|
|
111
|
+
return result.success;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Install a Python package
|
|
116
|
+
*/
|
|
117
|
+
function installPythonPackage(pipCmd, packageName) {
|
|
118
|
+
const cmds = [
|
|
119
|
+
`${pipCmd} install -q ${packageName}`,
|
|
120
|
+
`${pipCmd} install --user -q ${packageName}`
|
|
121
|
+
];
|
|
122
|
+
for (const cmd of cmds) {
|
|
123
|
+
const result = runCommand(cmd, 120000);
|
|
124
|
+
if (result.success) return { success: true, error: "" };
|
|
125
|
+
}
|
|
126
|
+
return { success: false, error: `Failed to install ${packageName}` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if Playwright browsers are installed
|
|
131
|
+
*/
|
|
132
|
+
function checkPlaywright(pythonCmd) {
|
|
133
|
+
const hasPackage = checkPythonPackage(pythonCmd, "playwright");
|
|
134
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
135
|
+
const pwCachePath = join(homeDir, ".cache", "ms-playwright");
|
|
136
|
+
const browsersInstalled = existsSync(pwCachePath);
|
|
137
|
+
return { installed: hasPackage, browsersInstalled };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Install Playwright
|
|
142
|
+
*/
|
|
143
|
+
function installPlaywright(pythonCmd) {
|
|
144
|
+
console.log(" Installing Playwright Python package...");
|
|
145
|
+
const pipCmd = checkPip(pythonCmd);
|
|
146
|
+
if (!pipCmd) return { success: false, error: "pip not found" };
|
|
147
|
+
|
|
148
|
+
const pkgResult = installPythonPackage(pipCmd, "playwright");
|
|
149
|
+
if (!pkgResult.success) return pkgResult;
|
|
150
|
+
|
|
151
|
+
console.log(" Installing Playwright browsers (chromium)...");
|
|
152
|
+
const browserResult = runCommand(`${pythonCmd} -m playwright install chromium`, 300000);
|
|
153
|
+
if (browserResult.success) return { success: true, error: "" };
|
|
154
|
+
|
|
155
|
+
return { success: false, error: browserResult.error };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Environment detection and auto-installation
|
|
160
|
+
*/
|
|
161
|
+
function checkAndFixEnvironment() {
|
|
162
|
+
console.log("\n[env-check] Starting environment detection...");
|
|
163
|
+
|
|
164
|
+
const pythonCheck = checkPython();
|
|
165
|
+
if (!pythonCheck.available) {
|
|
166
|
+
console.log(" ⚠ Python 3 not found. Python tools will not work.");
|
|
167
|
+
console.log(" → Install Python 3.8+ from https://python.org");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(` ✓ Python ${pythonCheck.version} detected`);
|
|
172
|
+
const pythonCmd = pythonCheck.cmd;
|
|
173
|
+
|
|
174
|
+
const pipCmd = checkPip(pythonCmd);
|
|
175
|
+
if (!pipCmd) {
|
|
176
|
+
console.log(" ⚠ pip not found. Cannot auto-install Python packages.");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
console.log(` ✓ pip detected`);
|
|
180
|
+
|
|
181
|
+
// Required packages
|
|
182
|
+
const requiredPackages = ["requests", "beautifulsoup4", "urllib3"];
|
|
183
|
+
for (const pkg of requiredPackages) {
|
|
184
|
+
if (!checkPythonPackage(pythonCmd, pkg)) {
|
|
185
|
+
console.log(` Installing ${pkg}...`);
|
|
186
|
+
const result = installPythonPackage(pipCmd, pkg);
|
|
187
|
+
if (result.success) {
|
|
188
|
+
console.log(` ✓ ${pkg} installed`);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(` ✗ Failed to install ${pkg}`);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
console.log(` ✓ ${pkg} already installed`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check Playwright
|
|
198
|
+
const pwCheck = checkPlaywright(pythonCmd);
|
|
199
|
+
if (!pwCheck.installed || !pwCheck.browsersInstalled) {
|
|
200
|
+
console.log(" Playwright not fully installed, installing...");
|
|
201
|
+
const result = installPlaywright(pythonCmd);
|
|
202
|
+
if (result.success) {
|
|
203
|
+
console.log(" ✓ Playwright + browsers installed");
|
|
204
|
+
} else {
|
|
205
|
+
console.log(` ⚠ Playwright installation failed: ${result.error}`);
|
|
206
|
+
console.log(" → browser_collect tool will not work until Playwright is installed");
|
|
207
|
+
console.log(` → Manual fix: ${pythonCmd} -m pip install playwright && ${pythonCmd} -m playwright install chromium`);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
console.log(" ✓ Playwright already installed");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log("[env-check] Environment check complete\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
23
216
|
function main() {
|
|
24
217
|
const packageRoot = __dirname;
|
|
25
218
|
const agentsSourceDir = join(packageRoot, "agents");
|
|
26
|
-
const
|
|
27
|
-
const
|
|
219
|
+
const opencodeBaseDir = getOpencodeBaseDir();
|
|
220
|
+
const claudeBaseDir = getClaudeBaseDir();
|
|
221
|
+
const opencodeAgentsDir = join(opencodeBaseDir, "agents");
|
|
222
|
+
const claudeAgentsDir = join(claudeBaseDir, "agents");
|
|
223
|
+
const skillTargetDir = join(opencodeBaseDir, "skills", "api-security-testing");
|
|
28
224
|
|
|
29
225
|
console.log("[api-security-testing] Installing...");
|
|
30
226
|
console.log(` Package root: ${packageRoot}`);
|
|
227
|
+
console.log(` Platform: ${platform()}`);
|
|
31
228
|
|
|
32
229
|
let totalInstalled = 0;
|
|
33
230
|
let totalFailed = 0;
|
|
34
231
|
|
|
35
|
-
// 1. Install agents
|
|
36
|
-
console.log("\n[1/
|
|
232
|
+
// 1. Install agents to BOTH locations
|
|
233
|
+
console.log("\n[1/5] Installing agents to ~/.claude/agents/ (oh-my-opencode)...");
|
|
234
|
+
if (existsSync(agentsSourceDir)) {
|
|
235
|
+
if (!existsSync(claudeAgentsDir)) {
|
|
236
|
+
mkdirSync(claudeAgentsDir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const files = readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
|
|
240
|
+
for (const file of files) {
|
|
241
|
+
try {
|
|
242
|
+
copyFileSync(join(agentsSourceDir, file), join(claudeAgentsDir, file));
|
|
243
|
+
console.log(` ✓ ${file}`);
|
|
244
|
+
totalInstalled++;
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error(` ✗ ${file}: ${err.message}`);
|
|
247
|
+
totalFailed++;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log("\n[2/5] Installing agents to ~/.config/opencode/agents/ (OpenCode native)...");
|
|
37
253
|
if (existsSync(agentsSourceDir)) {
|
|
38
|
-
if (!existsSync(
|
|
39
|
-
mkdirSync(
|
|
254
|
+
if (!existsSync(opencodeAgentsDir)) {
|
|
255
|
+
mkdirSync(opencodeAgentsDir, { recursive: true });
|
|
40
256
|
}
|
|
41
257
|
|
|
42
258
|
const files = readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
|
|
43
259
|
for (const file of files) {
|
|
44
260
|
try {
|
|
45
|
-
copyFileSync(join(agentsSourceDir, file), join(
|
|
261
|
+
copyFileSync(join(agentsSourceDir, file), join(opencodeAgentsDir, file));
|
|
46
262
|
console.log(` ✓ ${file}`);
|
|
47
263
|
totalInstalled++;
|
|
48
264
|
} catch (err) {
|
|
@@ -52,8 +268,8 @@ function main() {
|
|
|
52
268
|
}
|
|
53
269
|
}
|
|
54
270
|
|
|
55
|
-
//
|
|
56
|
-
console.log("\n[
|
|
271
|
+
// 3. Install SKILL.md
|
|
272
|
+
console.log("\n[3/5] Installing SKILL.md...");
|
|
57
273
|
const skillSource = join(packageRoot, "SKILL.md");
|
|
58
274
|
if (existsSync(skillSource)) {
|
|
59
275
|
if (!existsSync(skillTargetDir)) {
|
|
@@ -69,46 +285,34 @@ function main() {
|
|
|
69
285
|
}
|
|
70
286
|
}
|
|
71
287
|
|
|
72
|
-
//
|
|
73
|
-
console.log("\n[
|
|
288
|
+
// 4. Install references
|
|
289
|
+
console.log("\n[4/5] Installing references...");
|
|
74
290
|
const refsSourceDir = join(packageRoot, "references");
|
|
75
291
|
const refsTargetDir = join(skillTargetDir, "references");
|
|
76
292
|
if (existsSync(refsSourceDir)) {
|
|
77
|
-
if (!existsSync(refsTargetDir)) {
|
|
78
|
-
mkdirSync(refsTargetDir, { recursive: true });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function copyDir(src, dest) {
|
|
82
|
-
const items = readdirSync(src);
|
|
83
|
-
for (const item of items) {
|
|
84
|
-
const srcPath = join(src, item);
|
|
85
|
-
const destPath = join(dest, item);
|
|
86
|
-
try {
|
|
87
|
-
copyFileSync(srcPath, destPath);
|
|
88
|
-
totalInstalled++;
|
|
89
|
-
} catch {
|
|
90
|
-
if (existsSync(srcPath) && !srcPath.endsWith(".md")) {
|
|
91
|
-
mkdirSync(destPath, { recursive: true });
|
|
92
|
-
copyDir(srcPath, destPath);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
293
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
294
|
+
const count = copyDirRecursive(refsSourceDir, refsTargetDir);
|
|
295
|
+
totalInstalled += count;
|
|
296
|
+
console.log(` ✓ references/ (${count} files)`);
|
|
102
297
|
} catch (err) {
|
|
103
298
|
console.error(` ✗ references/: ${err.message}`);
|
|
104
299
|
totalFailed++;
|
|
105
300
|
}
|
|
106
301
|
}
|
|
107
302
|
|
|
303
|
+
// 5. Environment detection and auto-install
|
|
304
|
+
console.log("\n[5/5] Detecting environment and installing dependencies...");
|
|
305
|
+
try {
|
|
306
|
+
checkAndFixEnvironment();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.log(` ⚠ Environment check failed: ${err.message}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
108
311
|
console.log(`\n========================================`);
|
|
109
312
|
if (totalFailed === 0) {
|
|
110
313
|
console.log(`✓ Installed ${totalInstalled} file(s)`);
|
|
111
|
-
console.log(`\nAgents: ${
|
|
314
|
+
console.log(`\nAgents (oh-my-opencode): ${claudeAgentsDir}`);
|
|
315
|
+
console.log(`Agents (OpenCode native): ${opencodeAgentsDir}`);
|
|
112
316
|
console.log(`Skill: ${skillTargetDir}`);
|
|
113
317
|
console.log(`\n⚠️ IMPORTANT: Restart OpenCode to discover new agents`);
|
|
114
318
|
} else {
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { Plugin } from "@opencode-ai/plugin";
|
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
5
9
|
|
|
6
10
|
const SKILL_DIR = "skills/api-security-testing";
|
|
7
11
|
const CORE_DIR = `${SKILL_DIR}/core`;
|
|
@@ -183,9 +187,24 @@ To activate these agents, simply mention their name in your response (e.g., "@ap
|
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
async function execShell(ctx: unknown, cmd: string): Promise<string> {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
try {
|
|
191
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
192
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB buffer
|
|
193
|
+
timeout: 120000, // 2 minutes timeout
|
|
194
|
+
shell: process.platform === "win32" ? "powershell.exe" : undefined
|
|
195
|
+
});
|
|
196
|
+
if (stderr && !stdout) {
|
|
197
|
+
return `Error: ${stderr}`;
|
|
198
|
+
}
|
|
199
|
+
return stdout || stderr;
|
|
200
|
+
} catch (error: unknown) {
|
|
201
|
+
const err = error as { message?: string; stdout?: string; stderr?: string };
|
|
202
|
+
// 如果有 stdout 输出,即使命令失败也返回输出
|
|
203
|
+
if (err.stdout) {
|
|
204
|
+
return err.stdout;
|
|
205
|
+
}
|
|
206
|
+
return `Error: ${err.message || "Unknown error"}`;
|
|
207
|
+
}
|
|
189
208
|
}
|
|
190
209
|
|
|
191
210
|
function getFailureCount(sessionID: string): number {
|
|
@@ -208,7 +227,7 @@ function detectGiveUpPattern(text: string): boolean {
|
|
|
208
227
|
|
|
209
228
|
const ApiSecurityTestingPlugin: Plugin = async (ctx) => {
|
|
210
229
|
const config = loadConfig(ctx);
|
|
211
|
-
console.log(`[api-security-testing] Plugin loaded
|
|
230
|
+
console.log(`[api-security-testing] Plugin loaded v5.2.2 - collection_mode: ${config.collection_mode}`);
|
|
212
231
|
|
|
213
232
|
return {
|
|
214
233
|
tool: {
|
|
@@ -415,6 +434,171 @@ from testers.auth_tester import AuthTester
|
|
|
415
434
|
tester = AuthTester()
|
|
416
435
|
result = tester.test('${args.endpoint}')
|
|
417
436
|
print(result)
|
|
437
|
+
"`;
|
|
438
|
+
return await execShell(ctx, cmd);
|
|
439
|
+
},
|
|
440
|
+
}),
|
|
441
|
+
|
|
442
|
+
// 新增: 端点自动发现工具
|
|
443
|
+
endpoint_discover: tool({
|
|
444
|
+
description: "自动发现 API 端点。从 JS 文件、HTML、Sitemap 中提取所有 API 路径",
|
|
445
|
+
args: {
|
|
446
|
+
target: tool.schema.string(),
|
|
447
|
+
depth: tool.schema.enum(["shallow", "deep"]).optional(),
|
|
448
|
+
include_methods: tool.schema.boolean().optional()
|
|
449
|
+
},
|
|
450
|
+
async execute(args, ctx) {
|
|
451
|
+
const deps = checkDeps(ctx);
|
|
452
|
+
const corePath = getCorePath(ctx);
|
|
453
|
+
const target = args.target as string;
|
|
454
|
+
const depth = (args.depth as string) || "shallow";
|
|
455
|
+
const includeMethods = (args.include_methods as boolean) !== false;
|
|
456
|
+
const cmd = `${deps}python3 -c "
|
|
457
|
+
import sys, re, json, urllib.request, ssl
|
|
458
|
+
sys.path.insert(0, '${corePath}')
|
|
459
|
+
ssl._create_default_https_context = ssl._create_unverified_context
|
|
460
|
+
target = '${target}'
|
|
461
|
+
depth = '${depth}'
|
|
462
|
+
endpoints = set()
|
|
463
|
+
js_endpoints = set()
|
|
464
|
+
html_endpoints = set()
|
|
465
|
+
patterns = [r'[\\"\\'](/api/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/v[0-9]+/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/admin/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/auth/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/graphql)[\\"\\']', r'[\\"\\'](/rest/[^\\"\\'\\s?#]+)[\\"\\']', r'axios\\.(get|post|put|delete|patch)\([\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', r'fetch\([\\"\\']([^\\"\\'\\s?#]+)[\\"\\']']
|
|
466
|
+
def safe_fetch(url, timeout=10):
|
|
467
|
+
try:
|
|
468
|
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
469
|
+
with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode('utf-8', errors='ignore')
|
|
470
|
+
except: return None
|
|
471
|
+
def extract(text):
|
|
472
|
+
found = []
|
|
473
|
+
for p in patterns:
|
|
474
|
+
for m in re.findall(p, text):
|
|
475
|
+
ep = m[-1] if isinstance(m, tuple) else m
|
|
476
|
+
ep = ep.strip().lstrip('/')
|
|
477
|
+
if ep and len(ep) > 2 and not ep.startswith(('http', 'data:', '#')):
|
|
478
|
+
if not any(ep.lower().endswith(x) for x in ['.js','.css','.png','.jpg','.gif','.svg','.ico']):
|
|
479
|
+
found.append('/' + ep)
|
|
480
|
+
return found
|
|
481
|
+
parsed = __import__('urllib.parse').urlparse(target)
|
|
482
|
+
base = f'{parsed.scheme}://{parsed.netloc}'
|
|
483
|
+
html = safe_fetch(target)
|
|
484
|
+
if html:
|
|
485
|
+
html_endpoints.update(extract(html))
|
|
486
|
+
for js_url in re.findall(r'<script[^>]+src=[\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', html)[:20 if depth=='shallow' else 50]:
|
|
487
|
+
if js_url.startswith('/'): js_url = base + js_url
|
|
488
|
+
elif not js_url.startswith('http'): js_url = base + '/' + js_url
|
|
489
|
+
js_content = safe_fetch(js_url, timeout=10)
|
|
490
|
+
if js_content: js_endpoints.update(extract(js_content))
|
|
491
|
+
for path in ['/api', '/api/v1', '/api/docs', '/swagger.json', '/graphql', '/admin', '/auth', '/health', '/version']:
|
|
492
|
+
content = safe_fetch(base + path, timeout=5)
|
|
493
|
+
if content and len(content) > 10:
|
|
494
|
+
endpoints.add(path)
|
|
495
|
+
if content.strip().startswith('{'):
|
|
496
|
+
try:
|
|
497
|
+
for ep in re.findall(r'[\\"\\'](/[a-zA-Z0-9_./-]+)[\\"\\']', content[:10000]): endpoints.add(ep)
|
|
498
|
+
except: pass
|
|
499
|
+
if depth == 'deep':
|
|
500
|
+
for js_url in re.findall(r'<script[^>]+src=[\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', html or '')[:50]:
|
|
501
|
+
if js_url.startswith('/'): js_url = base + js_url
|
|
502
|
+
elif not js_url.startswith('http'): js_url = base + '/' + js_url
|
|
503
|
+
js_content = safe_fetch(js_url, timeout=10)
|
|
504
|
+
if js_content:
|
|
505
|
+
for p in [r'[\\"\\'](/[a-zA-Z0-9_./{{}}:-]+)[\\"\\']', r'path:\\s*[\\"\\'](/[a-zA-Z0-9_./{{}}:-]+)[\\"\\']']:
|
|
506
|
+
for m in re.findall(p, js_content):
|
|
507
|
+
ep = m[-1] if isinstance(m, tuple) else m
|
|
508
|
+
if len(ep) > 2 and not ep.startswith(('http', 'data:', '#')): endpoints.add(ep)
|
|
509
|
+
all_eps = sorted(set(endpoints) | set(html_endpoints) | set(js_endpoints))
|
|
510
|
+
clean = []
|
|
511
|
+
seen = set()
|
|
512
|
+
for ep in all_eps:
|
|
513
|
+
ep = ep.split('?')[0].split('#')[0].rstrip('/')
|
|
514
|
+
if ep and ep not in seen and len(ep) > 1:
|
|
515
|
+
seen.add(ep)
|
|
516
|
+
clean.append(ep)
|
|
517
|
+
result = {'target': target, 'total_endpoints': len(clean), 'endpoints': clean, 'sources': {'html': len(html_endpoints), 'javascript': len(js_endpoints), 'common_paths': len(endpoints)}}
|
|
518
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
519
|
+
"`;
|
|
520
|
+
return await execShell(ctx, cmd);
|
|
521
|
+
},
|
|
522
|
+
}),
|
|
523
|
+
|
|
524
|
+
// 新增: 报告生成工具
|
|
525
|
+
report_generate: tool({
|
|
526
|
+
description: "生成安全测试报告。支持 Markdown、HTML、JSON 格式,内置 OWASP API Top 10 模板",
|
|
527
|
+
args: {
|
|
528
|
+
format: tool.schema.enum(["markdown", "html", "json"]).optional(),
|
|
529
|
+
template: tool.schema.enum(["owasp-api", "custom"]).optional(),
|
|
530
|
+
target: tool.schema.string().optional(),
|
|
531
|
+
findings: tool.schema.string().optional(),
|
|
532
|
+
severity_filter: tool.schema.enum(["all", "critical", "high", "medium", "low", "info"]).optional()
|
|
533
|
+
},
|
|
534
|
+
async execute(args, ctx) {
|
|
535
|
+
const deps = checkDeps(ctx);
|
|
536
|
+
const corePath = getCorePath(ctx);
|
|
537
|
+
const format = (args.format as string) || "markdown";
|
|
538
|
+
const template = (args.template as string) || "owasp-api";
|
|
539
|
+
const target = (args.target as string) || "Unknown";
|
|
540
|
+
const findings = (args.findings as string) || "";
|
|
541
|
+
const severityFilter = (args.severity_filter as string) || "all";
|
|
542
|
+
const cmd = `${deps}python3 -c "
|
|
543
|
+
import sys, json
|
|
544
|
+
from datetime import datetime
|
|
545
|
+
sys.path.insert(0, '${corePath}')
|
|
546
|
+
format = '${format}'
|
|
547
|
+
target = '${target.replace(/'/g, "\\'")}'
|
|
548
|
+
findings = '''${findings.replace(/'/g, "\\'")}'''
|
|
549
|
+
severity_filter = '${severityFilter}'
|
|
550
|
+
parsed_findings = []
|
|
551
|
+
if findings.strip():
|
|
552
|
+
try: parsed_findings = json.loads(findings)
|
|
553
|
+
except:
|
|
554
|
+
for line in findings.strip().split('\\n'):
|
|
555
|
+
if line.strip():
|
|
556
|
+
try: parsed_findings.append(json.loads(line))
|
|
557
|
+
except: pass
|
|
558
|
+
if severity_filter != 'all' and parsed_findings:
|
|
559
|
+
parsed_findings = [f for f in parsed_findings if f.get('severity','').lower() == severity_filter.lower()]
|
|
560
|
+
sev_counts = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0, 'info': 0}
|
|
561
|
+
for f in parsed_findings:
|
|
562
|
+
s = f.get('severity','info').lower()
|
|
563
|
+
if s in sev_counts: sev_counts[s] += 1
|
|
564
|
+
total = len(parsed_findings)
|
|
565
|
+
risk_score = sev_counts['critical']*10 + sev_counts['high']*7 + sev_counts['medium']*4 + sev_counts['low']*1
|
|
566
|
+
risk_level = 'CRITICAL' if risk_score >= 50 else 'HIGH' if risk_score >= 30 else 'MEDIUM' if risk_score >= 15 else 'LOW' if risk_score > 0 else 'NONE'
|
|
567
|
+
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
568
|
+
if format == 'markdown':
|
|
569
|
+
report = f'# API 安全测试报告\\n\\n## 基本信息\\n\\n| 项目 | 值 |\\n|------|------|\\n| 目标 | {target} |\\n| 测试时间 | {now} |\\n| 风险等级 | **{risk_level}** |\\n| 风险分数 | {risk_score}/100 |\\n\\n## 漏洞统计\\n\\n| 严重程度 | 数量 |\\n|---------|------|\\n| Critical | {sev_counts[\\\"critical\\\"]} |\\n| High | {sev_counts[\\\"high\\\"]} |\\n| Medium | {sev_counts[\\\"medium\\\"]} |\\n| Low | {sev_counts[\\\"low\\\"]} |\\n| Info | {sev_counts[\\\"info\\\"]} |\\n| **总计** | **{total}** |\\n\\n## 漏洞详情\\n\\n'
|
|
570
|
+
if not parsed_findings: report += '未发现安全漏洞。\\n\\n'
|
|
571
|
+
else:
|
|
572
|
+
for i, f in enumerate(parsed_findings, 1):
|
|
573
|
+
title = f.get('title', f.get('vulnerability', f'Vulnerability #{i}'))
|
|
574
|
+
sev = f.get('severity', 'Unknown').upper()
|
|
575
|
+
ep = f.get('endpoint', f.get('url', 'N/A'))
|
|
576
|
+
desc = f.get('description', f.get('detail', 'No description'))
|
|
577
|
+
poc = f.get('poc', f.get('proof_of_concept', ''))
|
|
578
|
+
rec = f.get('recommendation', f.get('fix', 'Review and fix'))
|
|
579
|
+
report += f'### {i}. [{sev}] {title}\\n\\n| 属性 | 值 |\\n|------|------|\\n| 端点 | \\\`{ep}\\\` |\\n\\n**描述:**\\n\\n{desc}\\n\\n'
|
|
580
|
+
if poc: report += f'**PoC:**\\n\\n\\\`\\\`\\\`bash\\n{poc}\\n\\\`\\\`\\\`\\n\\n'
|
|
581
|
+
report += f'**修复建议:**\\n\\n{rec}\\n\\n---\\n\\n'
|
|
582
|
+
report += f'\\n## 测试覆盖范围\\n\\n| 测试类别 | 状态 |\\n|---------|------|\\n| SQL 注入 | ✅ 已测试 |\\n| XSS | ✅ 已测试 |\\n| IDOR | ✅ 已测试 |\\n| 认证绕过 | ✅ 已测试 |\\n| 敏感数据 | ✅ 已测试 |\\n| 业务逻辑 | ✅ 已测试 |\\n| 安全配置 | ✅ 已测试 |\\n| 暴力破解 | ✅ 已测试 |\\n| SSRF | ✅ 已测试 |\\n| GraphQL | ✅ 已测试 |\\n\\n---\\n\\n*报告生成时间: {now}*\\n*工具: opencode-api-security-testing v5.2.2*\\n'
|
|
583
|
+
elif format == 'json':
|
|
584
|
+
report = json.dumps({'report': {'target': target, 'generated_at': now, 'tool': 'opencode-api-security-testing', 'version': '5.1.0'}, 'summary': {'total': total, 'risk_score': risk_score, 'risk_level': risk_level, 'severity_counts': sev_counts}, 'findings': parsed_findings}, indent=2, ensure_ascii=False)
|
|
585
|
+
else:
|
|
586
|
+
report = f'<!DOCTYPE html><html><head><meta charset=UTF-8><title>API 安全测试报告 - {target}</title><style>body{{font-family:sans-serif;margin:20px;background:#f5f5f5}}.container{{max-width:1200px;margin:0 auto;background:white;border-radius:8px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.1)}}h1{{color:#333}}.stats{{display:flex;gap:15px;flex-wrap:wrap;margin:20px 0}}.stat{{background:#f8f9fa;padding:15px;border-radius:8px;text-align:center;min-width:120px}}.stat .num{{font-size:32px;font-weight:bold}}.stat.critical .num{{color:#dc3545}}.stat.high .num{{color:#fd7e14}}.stat.medium .num{{color:#ffc107}}.stat.low .num{{color:#28a745}}.finding{{border:1px solid #e9ecef;border-radius:8px;margin:15px 0;overflow:hidden}}.finding-header{{padding:15px;display:flex;align-items:center;gap:10px}}.finding-header.critical{{background:#fff5f5;border-left:4px solid #dc3545}}.finding-header.high{{background:#fff8f0;border-left:4px solid #fd7e14}}.finding-header.medium{{background:#fffdf0;border-left:4px solid #ffc107}}.finding-header.low{{background:#f0fff4;border-left:4px solid #28a745}}.badge{{padding:3px 8px;border-radius:4px;font-size:11px;font-weight:bold;color:white}}.badge.critical{{background:#dc3545}}.badge.high{{background:#fd7e14}}.badge.medium{{background:#ffc107;color:#333}}.badge.low{{background:#28a745}}.finding-body{{padding:15px}}pre{{background:#f8f9fa;padding:10px;border-radius:4px;overflow-x:auto}}</style></head><body><div class=container><h1>API 安全测试报告</h1><p>目标: {target} | 时间: {now} | 风险: <strong>{risk_level}</strong> ({risk_score}/100)</p><div class=stats><div class=stat critical><div class=num>{sev_counts[\\\"critical\\\"]}</div><div>Critical</div></div><div class=stat high><div class=num>{sev_counts[\\\"high\\\"]}</div><div>High</div></div><div class=stat medium><div class=num>{sev_counts[\\\"medium\\\"]}</div><div>Medium</div></div><div class=stat low><div class=num>{sev_counts[\\\"low\\\"]}</div><div>Low</div></div><div class=stat><div class=num>{total}</div><div>Total</div></div></div>'
|
|
587
|
+
if not parsed_findings: report += '<h2 style=color:#28a745>未发现安全漏洞</h2>'
|
|
588
|
+
else:
|
|
589
|
+
for i, f in enumerate(parsed_findings, 1):
|
|
590
|
+
title = f.get('title', f.get('vulnerability', f'#{i}'))
|
|
591
|
+
sev = f.get('severity','unknown').lower()
|
|
592
|
+
ep = f.get('endpoint', f.get('url', 'N/A'))
|
|
593
|
+
desc = f.get('description', f.get('detail', ''))
|
|
594
|
+
poc = f.get('poc', '')
|
|
595
|
+
rec = f.get('recommendation', f.get('fix', ''))
|
|
596
|
+
report += f'<div class=finding><div class=finding-header {sev}><span class=badge {sev}>{sev.upper()}</span><strong>{i}. {title}</strong></div><div class=finding-body><p><strong>端点:</strong> <code>{ep}</code></p><p>{desc}</p>'
|
|
597
|
+
if poc: report += f'<h4>PoC</h4><pre>{poc}</pre>'
|
|
598
|
+
if rec: report += f'<h4>修复建议</h4><p>{rec}</p>'
|
|
599
|
+
report += '</div></div>'
|
|
600
|
+
report += f'<p style=color:#666;font-size:12px;text-align:center;margin-top:20px>opencode-api-security-testing v5.2.2 | {now}</p></div></body></html>'
|
|
601
|
+
print(report)
|
|
418
602
|
"`;
|
|
419
603
|
return await execShell(ctx, cmd);
|
|
420
604
|
},
|