opencode-api-security-testing 3.0.4 → 3.0.6
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 +1 -1
- package/src/hooks/directory-agents-injector.ts +102 -0
- package/src/index.ts +93 -110
package/package.json
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const AGENTS_FILENAME = "AGENTS.md";
|
|
6
|
+
const AGENTS_DIR = ".config/opencode/agents";
|
|
7
|
+
|
|
8
|
+
export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
|
9
|
+
function resolveAgentsDir(): string | null {
|
|
10
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
11
|
+
if (!home) return null;
|
|
12
|
+
return join(home, AGENTS_DIR);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findAgentsMdUp(startDir: string, agentsDir: string): string | null {
|
|
16
|
+
let current = startDir;
|
|
17
|
+
|
|
18
|
+
while (true) {
|
|
19
|
+
const agentsPath = join(current, AGENTS_FILENAME);
|
|
20
|
+
if (existsSync(agentsPath)) {
|
|
21
|
+
return agentsPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (current === agentsDir) break;
|
|
25
|
+
const parent = dirname(current);
|
|
26
|
+
if (parent === current) break;
|
|
27
|
+
if (parent === "/" || parent === home) break;
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getSessionKey(sessionID: string): string {
|
|
35
|
+
return `api-sec-inject-${sessionID}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const injectedPaths = new Set<string>();
|
|
39
|
+
|
|
40
|
+
const toolExecuteAfter = async (
|
|
41
|
+
input: { tool: string; sessionID: string; callID: string },
|
|
42
|
+
output: { title: string; output: string; metadata: unknown }
|
|
43
|
+
) => {
|
|
44
|
+
const toolName = input.tool.toLowerCase();
|
|
45
|
+
const agentsDir = resolveAgentsDir();
|
|
46
|
+
|
|
47
|
+
if (!agentsDir || !existsSync(agentsDir)) return;
|
|
48
|
+
|
|
49
|
+
if (toolName === "read") {
|
|
50
|
+
const filePath = output.title;
|
|
51
|
+
if (!filePath) return;
|
|
52
|
+
|
|
53
|
+
const resolved = resolve(filePath);
|
|
54
|
+
const dir = dirname(resolved);
|
|
55
|
+
|
|
56
|
+
if (!dir.includes(agentsDir)) return;
|
|
57
|
+
|
|
58
|
+
const cacheKey = getSessionKey(input.sessionID);
|
|
59
|
+
if (injectedPaths.has(cacheKey + resolved)) return;
|
|
60
|
+
|
|
61
|
+
const agentsPath = findAgentsMdUp(dir, agentsDir);
|
|
62
|
+
if (!agentsPath) return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const content = readFileSync(agentsPath, "utf-8");
|
|
66
|
+
output.output += `\n\n[Auto-injected from ${AGENTS_FILENAME}]\n${content}`;
|
|
67
|
+
injectedPaths.add(cacheKey + resolved);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("[api-security-testing] Failed to inject agents:", err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const eventHandler = async (input: { event: { type: string; properties?: unknown } }) => {
|
|
75
|
+
const { event } = input;
|
|
76
|
+
|
|
77
|
+
if (event.type === "session.deleted" || event.type === "session.compacted") {
|
|
78
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
79
|
+
let sessionID: string | undefined;
|
|
80
|
+
|
|
81
|
+
if (event.type === "session.deleted") {
|
|
82
|
+
sessionID = (props?.info as { id?: string })?.id;
|
|
83
|
+
} else {
|
|
84
|
+
sessionID = (props?.sessionID ?? (props?.info as { id?: string })?.id) as string | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (sessionID) {
|
|
88
|
+
const cacheKey = getSessionKey(sessionID);
|
|
89
|
+
for (const key of injectedPaths.keys()) {
|
|
90
|
+
if (key.startsWith(cacheKey)) {
|
|
91
|
+
injectedPaths.delete(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"tool.execute.after": toolExecuteAfter,
|
|
100
|
+
event: eventHandler,
|
|
101
|
+
};
|
|
102
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
|
-
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
4
3
|
import { join } from "path";
|
|
5
|
-
import { existsSync } from "fs";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
5
|
|
|
7
6
|
const SKILL_DIR = "skills/api-security-testing";
|
|
8
7
|
const CORE_DIR = `${SKILL_DIR}/core`;
|
|
8
|
+
const AGENTS_DIR = ".config/opencode/agents";
|
|
9
|
+
const AGENTS_FILENAME = "AGENTS.md";
|
|
9
10
|
|
|
10
11
|
function getSkillPath(ctx: { directory: string }): string {
|
|
11
12
|
return join(ctx.directory, SKILL_DIR);
|
|
@@ -24,92 +25,40 @@ function checkDeps(ctx: { directory: string }): string {
|
|
|
24
25
|
return "";
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
1. **永不停止** - 任何线索都要追到底
|
|
32
|
-
2. **自动化编排** - 不等待用户,主动推进
|
|
33
|
-
3. **智能委派** - 识别任务类型,委派给最合适的子 agent
|
|
34
|
-
4. **压力升级** - 遇到失败自动换方法 (L1-L4)
|
|
35
|
-
|
|
36
|
-
## 可用子 Agent
|
|
37
|
-
|
|
38
|
-
| 子 Agent | 职责 |
|
|
39
|
-
|---------|------|
|
|
40
|
-
| @api-probing-miner | 漏洞挖掘 |
|
|
41
|
-
| @api-resource-specialist | 端点发现 |
|
|
42
|
-
| @api-vuln-verifier | 漏洞验证 |
|
|
43
|
-
|
|
44
|
-
## 可用工具
|
|
45
|
-
|
|
46
|
-
| 工具 | 用途 |
|
|
47
|
-
|------|------|
|
|
48
|
-
| api_security_scan | 完整扫描 |
|
|
49
|
-
| api_fuzz_test | 模糊测试 |
|
|
50
|
-
| browser_collect | 浏览器采集 |
|
|
51
|
-
| js_parse | JS分析 |
|
|
52
|
-
| graphql_test | GraphQL测试 |
|
|
53
|
-
| cloud_storage_test | 云存储测试 |
|
|
54
|
-
| vuln_verify | 漏洞验证 |
|
|
55
|
-
| sqli_test | SQL注入测试 |
|
|
56
|
-
| idor_test | IDOR测试 |
|
|
57
|
-
| auth_test | 认证测试`;
|
|
58
|
-
|
|
59
|
-
const PROBING_MINER_PROMPT = `你是**API漏洞挖掘专家**,专注于发现和验证安全漏洞。
|
|
60
|
-
|
|
61
|
-
## 职责
|
|
62
|
-
|
|
63
|
-
1. **针对性测试** - 根据端点特征选择最佳测试方法
|
|
64
|
-
2. **快速验证** - 确认漏洞存在
|
|
65
|
-
3. **PoC 生成** - 提供可执行的测试命令
|
|
66
|
-
|
|
67
|
-
## 测试方法库
|
|
68
|
-
|
|
69
|
-
### SQL 注入
|
|
70
|
-
- 布尔盲注: ' OR 1=1 --
|
|
71
|
-
- 联合查询: ' UNION SELECT NULL--
|
|
72
|
-
- 错误注入: ' AND 1=CONVERT(int,...)--
|
|
73
|
-
- 时间盲注: '; WAITFOR DELAY '00:00:05'--
|
|
74
|
-
|
|
75
|
-
### IDOR
|
|
76
|
-
- 替换 ID: /api/user/1 → /api/user/2
|
|
77
|
-
- 水平/垂直越权测试
|
|
78
|
-
|
|
79
|
-
### JWT
|
|
80
|
-
- 空算法: alg: none
|
|
81
|
-
- 密钥混淆: HS256 → HS512`;
|
|
82
|
-
|
|
83
|
-
const RESOURCE_SPECIALIST_PROMPT = `你是**API资源探测专家**,专注于发现和采集 API 端点。
|
|
84
|
-
|
|
85
|
-
## 职责
|
|
86
|
-
|
|
87
|
-
1. **全面发现** - 不遗漏任何端点
|
|
88
|
-
2. **动态采集** - 拦截真实请求
|
|
89
|
-
3. **静态分析** - 提取 API 模式
|
|
90
|
-
|
|
91
|
-
## 采集技术
|
|
92
|
-
|
|
93
|
-
### 1. 浏览器动态采集
|
|
94
|
-
使用 browser_collect 拦截 XHR/Fetch 请求
|
|
28
|
+
function getAgentsDir(): string {
|
|
29
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
30
|
+
return join(home, AGENTS_DIR);
|
|
31
|
+
}
|
|
95
32
|
|
|
96
|
-
|
|
97
|
-
|
|
33
|
+
function getInjectedAgentsPrompt(): string {
|
|
34
|
+
const agentsDir = getAgentsDir();
|
|
35
|
+
const agentsPath = join(agentsDir, "api-cyber-supervisor.md");
|
|
36
|
+
|
|
37
|
+
if (!existsSync(agentsPath)) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
98
40
|
|
|
99
|
-
|
|
100
|
-
|
|
41
|
+
try {
|
|
42
|
+
const content = readFileSync(agentsPath, "utf-8");
|
|
43
|
+
return `
|
|
101
44
|
|
|
102
|
-
|
|
45
|
+
[API Security Testing Agents Available]
|
|
46
|
+
When performing security testing tasks, you can use the following specialized agents:
|
|
103
47
|
|
|
104
|
-
|
|
48
|
+
${content}
|
|
105
49
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
50
|
+
To activate these agents, simply mention their name in your response (e.g., "@api-cyber-supervisor" to coordinate security testing).
|
|
51
|
+
`;
|
|
52
|
+
} catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
109
56
|
|
|
110
57
|
const ApiSecurityTestingPlugin: Plugin = async (ctx) => {
|
|
111
58
|
console.log("[api-security-testing] Plugin loaded");
|
|
112
59
|
|
|
60
|
+
const injectedSessions = new Set<string>();
|
|
61
|
+
|
|
113
62
|
return {
|
|
114
63
|
tool: {
|
|
115
64
|
api_security_scan: tool({
|
|
@@ -330,40 +279,74 @@ print(result)
|
|
|
330
279
|
}),
|
|
331
280
|
},
|
|
332
281
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
config.agent = {};
|
|
336
|
-
}
|
|
282
|
+
"chat.message": async (input, output) => {
|
|
283
|
+
const sessionID = input.sessionID;
|
|
337
284
|
|
|
338
|
-
|
|
285
|
+
if (!injectedSessions.has(sessionID)) {
|
|
286
|
+
injectedSessions.add(sessionID);
|
|
287
|
+
|
|
288
|
+
const agentsPrompt = getInjectedAgentsPrompt();
|
|
289
|
+
if (agentsPrompt) {
|
|
290
|
+
const parts = output.parts as Array<{ type: string; text?: string }>;
|
|
291
|
+
const textPart = parts.find(p => p.type === "text");
|
|
292
|
+
if (textPart && textPart.text) {
|
|
293
|
+
textPart.text += agentsPrompt;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
"tool.execute.after": async (input, output) => {
|
|
300
|
+
const toolName = input.tool.toLowerCase();
|
|
301
|
+
const agentsDir = getAgentsDir();
|
|
339
302
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
303
|
+
if (!existsSync(agentsDir)) return;
|
|
304
|
+
|
|
305
|
+
if (toolName === "read") {
|
|
306
|
+
const filePath = output.title;
|
|
307
|
+
if (!filePath) return;
|
|
308
|
+
|
|
309
|
+
const resolved = resolve(filePath);
|
|
310
|
+
const dir = dirname(resolved);
|
|
311
|
+
|
|
312
|
+
if (!dir.includes(agentsDir)) return;
|
|
313
|
+
|
|
314
|
+
const agentsPath = join(agentsDir, AGENTS_FILENAME);
|
|
315
|
+
if (!existsSync(agentsPath)) return;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const content = readFileSync(agentsPath, "utf-8");
|
|
319
|
+
output.output += `\n\n[Agents Definition]\n${content}`;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.error("[api-security-testing] Failed to inject agents:", err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
event: async (input) => {
|
|
327
|
+
const { event } = input;
|
|
328
|
+
|
|
329
|
+
if (event.type === "session.deleted" || event.type === "session.compacted") {
|
|
330
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
331
|
+
let sessionID: string | undefined;
|
|
332
|
+
|
|
333
|
+
if (event.type === "session.deleted") {
|
|
334
|
+
sessionID = (props?.info as { id?: string })?.id;
|
|
335
|
+
} else {
|
|
336
|
+
sessionID = (props?.sessionID ?? (props?.info as { id?: string })?.id) as string | undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (sessionID) {
|
|
340
|
+
injectedSessions.delete(sessionID);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
365
343
|
},
|
|
366
344
|
};
|
|
367
345
|
};
|
|
368
346
|
|
|
369
|
-
|
|
347
|
+
function resolve(filePath: string): string {
|
|
348
|
+
if (filePath.startsWith("/")) return filePath;
|
|
349
|
+
return join(process.cwd(), filePath);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export default ApiSecurityTestingPlugin;
|