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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-api-security-testing",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
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",
@@ -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
- const CYBER_SUPERVISOR_PROMPT = `你是 API 安全测试的**赛博监工**,代号"P9"。
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
- ### 2. JS 静态分析
97
- 使用 js_parse 解析 JS 文件
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
- ### 3. 目录探测
100
- 常见路径: /api/v1/*, /graphql, /swagger, /.well-known/*`;
41
+ try {
42
+ const content = readFileSync(agentsPath, "utf-8");
43
+ return `
101
44
 
102
- const VULN_VERIFIER_PROMPT = `你是**漏洞验证专家**,专注于验证和确认安全漏洞。
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
- 1. **快速验证** - 确认漏洞是否存在
107
- 2. **风险评估** - 判断实际影响
108
- 3. **PoC 生成** - 提供可执行的证明`;
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
- config: async (config) => {
334
- if (!config.agent) {
335
- config.agent = {};
336
- }
282
+ "chat.message": async (input, output) => {
283
+ const sessionID = input.sessionID;
337
284
 
338
- const agents = config.agent as Record<string, AgentConfig>;
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
- agents["api-cyber-supervisor"] = {
341
- description: "API安全测试编排者。协调完整扫描流程,永不停止。",
342
- mode: "primary",
343
- prompt: CYBER_SUPERVISOR_PROMPT,
344
- };
345
-
346
- agents["api-probing-miner"] = {
347
- description: "漏洞挖掘专家。专注发现和验证 API 漏洞。",
348
- mode: "subagent",
349
- prompt: PROBING_MINER_PROMPT,
350
- };
351
-
352
- agents["api-resource-specialist"] = {
353
- description: "资源探测专家。专注采集和发现 API 端点。",
354
- mode: "subagent",
355
- prompt: RESOURCE_SPECIALIST_PROMPT,
356
- };
357
-
358
- agents["api-vuln-verifier"] = {
359
- description: "漏洞验证专家。验证和确认安全漏洞。",
360
- mode: "subagent",
361
- prompt: VULN_VERIFIER_PROMPT,
362
- };
363
-
364
- console.log("[api-security-testing] Tools registered");
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
- export default ApiSecurityTestingPlugin;
347
+ function resolve(filePath: string): string {
348
+ if (filePath.startsWith("/")) return filePath;
349
+ return join(process.cwd(), filePath);
350
+ }
351
+
352
+ export default ApiSecurityTestingPlugin;