opencode-api-security-testing 3.0.7 → 3.0.9
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/agents/api-cyber-supervisor.md +22 -19
- package/agents/api-probing-miner.md +12 -28
- package/agents/api-resource-specialist.md +12 -32
- package/agents/api-vuln-verifier.md +14 -33
- package/package.json +5 -1
- package/src/index.ts +17 -126
- package/src/hooks/directory-agents-injector.ts +0 -102
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: ">=1.0.0"
|
|
3
|
-
requires: ">=1.0.0"
|
|
4
2
|
description: API安全测试编排者。协调完整扫描流程,永不停止,主动推进测试进度。
|
|
5
3
|
mode: primary
|
|
4
|
+
permission:
|
|
5
|
+
edit: ask
|
|
6
|
+
bash:
|
|
7
|
+
"*": ask
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
你是 API 安全测试的**赛博监工**,代号"P9"。
|
|
@@ -13,34 +15,35 @@ mode: primary
|
|
|
13
15
|
|
|
14
16
|
## 可用子 Agent
|
|
15
17
|
|
|
16
|
-
| 子 Agent | 职责 |
|
|
17
|
-
|
|
18
|
-
| @api-probing-miner | 漏洞挖掘 |
|
|
19
|
-
| @api-resource-specialist | 端点发现 |
|
|
20
|
-
| @api-vuln-verifier | 漏洞验证 |
|
|
18
|
+
| 子 Agent | 职责 |
|
|
19
|
+
|---------|------|
|
|
20
|
+
| @api-probing-miner | 漏洞挖掘 |
|
|
21
|
+
| @api-resource-specialist | 端点发现 |
|
|
22
|
+
| @api-vuln-verifier | 漏洞验证 |
|
|
21
23
|
|
|
22
24
|
## 可用工具
|
|
23
25
|
|
|
24
26
|
直接调用以下工具执行特定任务:
|
|
25
27
|
|
|
26
|
-
| 工具 | 用途 |
|
|
27
|
-
|
|
28
|
-
| api_security_scan | 完整扫描 |
|
|
29
|
-
| api_fuzz_test | 模糊测试 |
|
|
30
|
-
| browser_collect | 浏览器采集 |
|
|
31
|
-
| js_parse | JS 分析 |
|
|
32
|
-
| vuln_verify | 漏洞验证 |
|
|
33
|
-
| graphql_test | GraphQL 测试 |
|
|
34
|
-
| cloud_storage_test | 云存储测试 |
|
|
35
|
-
| idor_test | IDOR 测试 |
|
|
36
|
-
| sqli_test | SQLi 测试 |
|
|
28
|
+
| 工具 | 用途 |
|
|
29
|
+
|------|------|
|
|
30
|
+
| api_security_scan | 完整扫描 |
|
|
31
|
+
| api_fuzz_test | 模糊测试 |
|
|
32
|
+
| browser_collect | 浏览器采集 |
|
|
33
|
+
| js_parse | JS 分析 |
|
|
34
|
+
| vuln_verify | 漏洞验证 |
|
|
35
|
+
| graphql_test | GraphQL 测试 |
|
|
36
|
+
| cloud_storage_test | 云存储测试 |
|
|
37
|
+
| idor_test | IDOR 测试 |
|
|
38
|
+
| sqli_test | SQLi 测试 |
|
|
39
|
+
| auth_test | 认证测试 |
|
|
37
40
|
|
|
38
41
|
## 测试流程
|
|
39
42
|
|
|
40
43
|
### Phase 1: 侦察
|
|
41
44
|
1. browser_collect 采集动态端点
|
|
42
45
|
2. js_parse 分析 JS 文件
|
|
43
|
-
3.
|
|
46
|
+
3. api_fuzz_test 发现隐藏端点
|
|
44
47
|
|
|
45
48
|
### Phase 2: 分析
|
|
46
49
|
1. 识别技术栈
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
requires: ">=1.0.0"
|
|
4
|
-
description: 漏洞挖掘专家。专注发现和验证 API 安全漏洞。
|
|
2
|
+
description: 漏洞挖掘专家。专注发现和验证 API 漏洞。
|
|
5
3
|
mode: subagent
|
|
4
|
+
permission:
|
|
5
|
+
edit: ask
|
|
6
|
+
bash:
|
|
7
|
+
"*": ask
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
你是**API漏洞挖掘专家**,专注于发现和验证安全漏洞。
|
|
@@ -23,35 +25,17 @@ mode: subagent
|
|
|
23
25
|
|
|
24
26
|
### IDOR
|
|
25
27
|
- 替换 ID: /api/user/1 → /api/user/2
|
|
26
|
-
-
|
|
27
|
-
- 垂直越权测试
|
|
28
|
+
- 水平/垂直越权测试
|
|
28
29
|
|
|
29
30
|
### JWT
|
|
30
31
|
- 空算法: alg: none
|
|
31
32
|
- 密钥混淆: HS256 → HS512
|
|
32
|
-
- 无签名验证
|
|
33
|
-
|
|
34
|
-
### 敏感数据
|
|
35
|
-
- 响应中的密码/密钥
|
|
36
|
-
- PII 信息泄露
|
|
37
|
-
- 调试端点
|
|
38
33
|
|
|
39
34
|
## 可用工具
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
## 发现漏洞
|
|
50
|
-
|
|
51
|
-
### {type}
|
|
52
|
-
- **端点**: {endpoint}
|
|
53
|
-
- **方法**: {method}
|
|
54
|
-
- **严重程度**: {severity}
|
|
55
|
-
- **PoC**: `{command}`
|
|
56
|
-
- **状态**: {status}
|
|
57
|
-
```
|
|
36
|
+
| 工具 | 用途 |
|
|
37
|
+
|------|------|
|
|
38
|
+
| sqli_test | SQL 注入测试 |
|
|
39
|
+
| idor_test | IDOR 测试 |
|
|
40
|
+
| vuln_verify | 漏洞验证 |
|
|
41
|
+
| api_fuzz_test | 模糊测试 |
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: ">=1.0.0"
|
|
3
|
-
requires: ">=1.0.0"
|
|
4
2
|
description: 资源探测专家。专注采集和发现 API 端点。
|
|
5
3
|
mode: subagent
|
|
4
|
+
permission:
|
|
5
|
+
edit: ask
|
|
6
|
+
bash:
|
|
7
|
+
"*": ask
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
你是**API资源探测专家**,专注于发现和采集 API 端点。
|
|
@@ -19,38 +21,16 @@ mode: subagent
|
|
|
19
21
|
使用 browser_collect 拦截 XHR/Fetch 请求
|
|
20
22
|
|
|
21
23
|
### 2. JS 静态分析
|
|
22
|
-
使用 js_parse 解析
|
|
24
|
+
使用 js_parse 解析 JS 文件
|
|
23
25
|
|
|
24
26
|
### 3. 目录探测
|
|
25
|
-
|
|
26
|
-
- /api/v1/*, /graphql
|
|
27
|
-
- /swagger, /api-docs
|
|
28
|
-
- /.well-known/*
|
|
29
|
-
|
|
30
|
-
## 端点分类
|
|
31
|
-
|
|
32
|
-
| 风险 | 类型 | 示例 |
|
|
33
|
-
|------|------|------|
|
|
34
|
-
| 高 | 认证 | /login, /oauth/* |
|
|
35
|
-
| 高 | 数据 | /api/*/list, /search |
|
|
36
|
-
| 中 | 用户 | /users, /profile |
|
|
37
|
-
| 极高 | 管理 | /admin, /manage |
|
|
27
|
+
常见路径: /api/v1/*, /graphql, /swagger, /.well-known/*
|
|
38
28
|
|
|
39
29
|
## 可用工具
|
|
40
30
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
## 端点发现报告
|
|
49
|
-
|
|
50
|
-
- 总数: {count}
|
|
51
|
-
- 高风险: {high}
|
|
52
|
-
- 中风险: {medium}
|
|
53
|
-
|
|
54
|
-
### 高风险端点
|
|
55
|
-
1. {method} {path} - {reason}
|
|
56
|
-
```
|
|
31
|
+
| 工具 | 用途 |
|
|
32
|
+
|------|------|
|
|
33
|
+
| browser_collect | 浏览器采集 |
|
|
34
|
+
| js_parse | JS 分析 |
|
|
35
|
+
| api_fuzz_test | 模糊测试 |
|
|
36
|
+
| graphql_test | GraphQL 测试 |
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: ">=1.0.0"
|
|
3
|
-
requires: ">=1.0.0"
|
|
4
2
|
description: 漏洞验证专家。验证和确认安全漏洞。
|
|
5
3
|
mode: subagent
|
|
4
|
+
permission:
|
|
5
|
+
edit: ask
|
|
6
|
+
bash:
|
|
7
|
+
"*": ask
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
你是**漏洞验证专家**,专注于验证和确认安全漏洞。
|
|
@@ -15,37 +17,16 @@ mode: subagent
|
|
|
15
17
|
|
|
16
18
|
## 验证流程
|
|
17
19
|
|
|
18
|
-
1.
|
|
19
|
-
2.
|
|
20
|
-
3.
|
|
21
|
-
4.
|
|
22
|
-
5. 生成 PoC
|
|
20
|
+
1. 接收漏洞报告
|
|
21
|
+
2. 验证漏洞真实性
|
|
22
|
+
3. 评估风险等级
|
|
23
|
+
4. 生成 PoC
|
|
23
24
|
|
|
24
25
|
## 可用工具
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
## 验证结果
|
|
35
|
-
|
|
36
|
-
**漏洞类型**: {type}
|
|
37
|
-
**端点**: {endpoint}
|
|
38
|
-
**验证状态**: CONFIRMED / INVALID / UNCERTAIN
|
|
39
|
-
**严重程度**: Critical / High / Medium / Low / Info
|
|
40
|
-
|
|
41
|
-
### 测试步骤
|
|
42
|
-
1. {step}
|
|
43
|
-
|
|
44
|
-
### PoC
|
|
45
|
-
```bash
|
|
46
|
-
{command}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### 修复建议
|
|
50
|
-
{fix}
|
|
51
|
-
```
|
|
27
|
+
| 工具 | 用途 |
|
|
28
|
+
|------|------|
|
|
29
|
+
| vuln_verify | 漏洞验证 |
|
|
30
|
+
| sqli_test | SQL 注入验证 |
|
|
31
|
+
| idor_test | IDOR 验证 |
|
|
32
|
+
| auth_test | 认证问题验证 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-api-security-testing",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9",
|
|
4
4
|
"description": "API Security Testing Plugin for OpenCode - Automated vulnerability scanning and penetration testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -39,5 +39,9 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@opencode-ai/plugin": "^1.1.19",
|
|
41
41
|
"@opencode-ai/sdk": "^1.1.19"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.5.2",
|
|
45
|
+
"typescript": "^6.0.2"
|
|
42
46
|
}
|
|
43
47
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { existsSync
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
5
|
|
|
6
6
|
const SKILL_DIR = "skills/api-security-testing";
|
|
7
7
|
const CORE_DIR = `${SKILL_DIR}/core`;
|
|
8
|
-
const AGENTS_DIR = ".config/opencode/agents";
|
|
9
|
-
const AGENTS_FILENAME = "AGENTS.md";
|
|
10
|
-
|
|
11
|
-
function getSkillPath(ctx: { directory: string }): string {
|
|
12
|
-
return join(ctx.directory, SKILL_DIR);
|
|
13
|
-
}
|
|
14
8
|
|
|
15
9
|
function getCorePath(ctx: { directory: string }): string {
|
|
16
10
|
return join(ctx.directory, CORE_DIR);
|
|
17
11
|
}
|
|
18
12
|
|
|
19
13
|
function checkDeps(ctx: { directory: string }): string {
|
|
20
|
-
const skillPath =
|
|
14
|
+
const skillPath = join(ctx.directory, SKILL_DIR);
|
|
21
15
|
const reqFile = join(skillPath, "requirements.txt");
|
|
22
16
|
if (existsSync(reqFile)) {
|
|
23
17
|
return `pip install -q -r "${reqFile}" 2>/dev/null; `;
|
|
@@ -25,40 +19,15 @@ function checkDeps(ctx: { directory: string }): string {
|
|
|
25
19
|
return "";
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const content = readFileSync(agentsPath, "utf-8");
|
|
43
|
-
return `
|
|
44
|
-
|
|
45
|
-
[API Security Testing Agents Available]
|
|
46
|
-
When performing security testing tasks, you can use the following specialized agents:
|
|
47
|
-
|
|
48
|
-
${content}
|
|
49
|
-
|
|
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
|
-
}
|
|
22
|
+
async function execShell(ctx: unknown, cmd: string): Promise<string> {
|
|
23
|
+
const shell = ctx as { $: (strings: TemplateStringsArray, ...expr: unknown[]) => Promise<{ toString(): string }> };
|
|
24
|
+
const result = await shell.$`${cmd}`;
|
|
25
|
+
return result.toString();
|
|
55
26
|
}
|
|
56
27
|
|
|
57
28
|
const ApiSecurityTestingPlugin: Plugin = async (ctx) => {
|
|
58
29
|
console.log("[api-security-testing] Plugin loaded");
|
|
59
30
|
|
|
60
|
-
const injectedSessions = new Set<string>();
|
|
61
|
-
|
|
62
31
|
return {
|
|
63
32
|
tool: {
|
|
64
33
|
api_security_scan: tool({
|
|
@@ -78,8 +47,7 @@ tester = DeepAPITesterV55(target='${args.target}', headless=True)
|
|
|
78
47
|
results = tester.run_test()
|
|
79
48
|
print(results)
|
|
80
49
|
"`;
|
|
81
|
-
|
|
82
|
-
return result.toString();
|
|
50
|
+
return await execShell(ctx, cmd);
|
|
83
51
|
},
|
|
84
52
|
}),
|
|
85
53
|
|
|
@@ -100,8 +68,7 @@ fuzzer = APIFuzzer('${args.endpoint}')
|
|
|
100
68
|
results = fuzzer.fuzz(method='${args.method || 'GET'}')
|
|
101
69
|
print(results)
|
|
102
70
|
"`;
|
|
103
|
-
|
|
104
|
-
return result.toString();
|
|
71
|
+
return await execShell(ctx, cmd);
|
|
105
72
|
},
|
|
106
73
|
}),
|
|
107
74
|
|
|
@@ -122,8 +89,7 @@ verifier = VulnVerifier()
|
|
|
122
89
|
result = verifier.verify('${args.vuln_type}', '${args.endpoint}')
|
|
123
90
|
print(result)
|
|
124
91
|
"`;
|
|
125
|
-
|
|
126
|
-
return result.toString();
|
|
92
|
+
return await execShell(ctx, cmd);
|
|
127
93
|
},
|
|
128
94
|
}),
|
|
129
95
|
|
|
@@ -145,8 +111,7 @@ print(f'发现 {len(endpoints)} 个端点:')
|
|
|
145
111
|
for ep in endpoints:
|
|
146
112
|
print(ep)
|
|
147
113
|
"`;
|
|
148
|
-
|
|
149
|
-
return result.toString();
|
|
114
|
+
return await execShell(ctx, cmd);
|
|
150
115
|
},
|
|
151
116
|
}),
|
|
152
117
|
|
|
@@ -166,8 +131,7 @@ parser = JSParser()
|
|
|
166
131
|
endpoints = parser.parse_file('${args.file_path}')
|
|
167
132
|
print(f'从 JS 发现 {len(endpoints)} 个端点')
|
|
168
133
|
"`;
|
|
169
|
-
|
|
170
|
-
return result.toString();
|
|
134
|
+
return await execShell(ctx, cmd);
|
|
171
135
|
},
|
|
172
136
|
}),
|
|
173
137
|
|
|
@@ -187,8 +151,7 @@ analyzer = SmartAnalyzer()
|
|
|
187
151
|
result = analyzer.graphql_test('${args.endpoint}')
|
|
188
152
|
print(result)
|
|
189
153
|
"`;
|
|
190
|
-
|
|
191
|
-
return result.toString();
|
|
154
|
+
return await execShell(ctx, cmd);
|
|
192
155
|
},
|
|
193
156
|
}),
|
|
194
157
|
|
|
@@ -208,8 +171,7 @@ tester = CloudStorageTester()
|
|
|
208
171
|
result = tester.full_test('${args.bucket_url}')
|
|
209
172
|
print(result)
|
|
210
173
|
"`;
|
|
211
|
-
|
|
212
|
-
return result.toString();
|
|
174
|
+
return await execShell(ctx, cmd);
|
|
213
175
|
},
|
|
214
176
|
}),
|
|
215
177
|
|
|
@@ -230,8 +192,7 @@ tester = IDORTester()
|
|
|
230
192
|
result = tester.test('${args.endpoint}', '${args.resource_id}')
|
|
231
193
|
print(result)
|
|
232
194
|
"`;
|
|
233
|
-
|
|
234
|
-
return result.toString();
|
|
195
|
+
return await execShell(ctx, cmd);
|
|
235
196
|
},
|
|
236
197
|
}),
|
|
237
198
|
|
|
@@ -252,8 +213,7 @@ tester = SQLiTester()
|
|
|
252
213
|
result = tester.test('${args.endpoint}', '${args.param}')
|
|
253
214
|
print(result)
|
|
254
215
|
"`;
|
|
255
|
-
|
|
256
|
-
return result.toString();
|
|
216
|
+
return await execShell(ctx, cmd);
|
|
257
217
|
},
|
|
258
218
|
}),
|
|
259
219
|
|
|
@@ -273,80 +233,11 @@ tester = AuthTester()
|
|
|
273
233
|
result = tester.test('${args.endpoint}')
|
|
274
234
|
print(result)
|
|
275
235
|
"`;
|
|
276
|
-
|
|
277
|
-
return result.toString();
|
|
236
|
+
return await execShell(ctx, cmd);
|
|
278
237
|
},
|
|
279
238
|
}),
|
|
280
239
|
},
|
|
281
|
-
|
|
282
|
-
"chat.message": async (input, output) => {
|
|
283
|
-
const sessionID = input.sessionID;
|
|
284
|
-
|
|
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();
|
|
302
|
-
|
|
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
|
-
}
|
|
343
|
-
},
|
|
344
240
|
};
|
|
345
241
|
};
|
|
346
242
|
|
|
347
|
-
|
|
348
|
-
if (filePath.startsWith("/")) return filePath;
|
|
349
|
-
return join(process.cwd(), filePath);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export default ApiSecurityTestingPlugin;
|
|
243
|
+
export default ApiSecurityTestingPlugin;
|
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|