opencode-api-security-testing 5.3.0 → 5.4.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +366 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-api-security-testing",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
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",
package/src/index.ts CHANGED
@@ -1,13 +1,238 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
1
+ import type { Plugin, PluginInput } from "@opencode-ai/plugin";
2
2
  import { tool } from "@opencode-ai/plugin";
3
- import { join } from "path";
4
- import { existsSync, readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "fs";
5
5
  import { exec } from "child_process";
6
6
  import { promisify } from "util";
7
7
 
8
8
  const execAsync = promisify(exec);
9
9
 
10
- // 任务队列管理器
10
+ // ============================================================================
11
+ // OpenCode 原生集成 - 任务持久化与事件系统
12
+ // ============================================================================
13
+
14
+ const TASKS_DIR = ".opencode/tasks";
15
+ const TASK_FILE_PREFIX = "security_scan_";
16
+
17
+ interface PersistedScanTask {
18
+ id: string;
19
+ status: "pending" | "running" | "completed" | "failed" | "cancelled";
20
+ type: string;
21
+ target: string;
22
+ startTime: number;
23
+ endTime?: number;
24
+ result?: string;
25
+ error?: string;
26
+ progress: number;
27
+ ptyId?: string;
28
+ createdAt: string;
29
+ }
30
+
31
+ // 任务管理器 - 支持持久化和 PTY 集成
32
+ class ScanTaskManager {
33
+ private tasks = new Map<string, PersistedScanTask>();
34
+ private directory: string = "";
35
+ private client: PluginInput["client"] | null = null;
36
+ private eventSubscription: ReturnType<typeof this.subscribeToEvents> | null = null;
37
+
38
+ init(directory: string, client: PluginInput["client"]) {
39
+ this.directory = directory;
40
+ this.client = client;
41
+ this.loadPersistedTasks();
42
+ this.subscribeToEvents();
43
+ }
44
+
45
+ private getTasksDir(): string {
46
+ return join(this.directory, TASKS_DIR);
47
+ }
48
+
49
+ private getTaskFilePath(taskId: string): string {
50
+ return join(this.getTasksDir(), `${TASK_FILE_PREFIX}${taskId}.json`);
51
+ }
52
+
53
+ // 持久化任务到文件
54
+ private persistTask(task: PersistedScanTask): void {
55
+ try {
56
+ const tasksDir = this.getTasksDir();
57
+ if (!existsSync(tasksDir)) {
58
+ mkdirSync(tasksDir, { recursive: true });
59
+ }
60
+ writeFileSync(this.getTaskFilePath(task.id), JSON.stringify(task, null, 2));
61
+ } catch (e) {
62
+ console.error(`[api-security-testing] Failed to persist task ${task.id}:`, e);
63
+ }
64
+ }
65
+
66
+ // 删除持久化文件
67
+ private removePersistedTask(taskId: string): void {
68
+ try {
69
+ const filePath = this.getTaskFilePath(taskId);
70
+ if (existsSync(filePath)) {
71
+ unlinkSync(filePath);
72
+ }
73
+ } catch (e) {
74
+ console.error(`[api-security-testing] Failed to remove task file ${taskId}:`, e);
75
+ }
76
+ }
77
+
78
+ // 从文件加载任务
79
+ private loadPersistedTasks(): void {
80
+ try {
81
+ const tasksDir = this.getTasksDir();
82
+ if (!existsSync(tasksDir)) return;
83
+
84
+ const files = readdirSync(tasksDir).filter(f => f.startsWith(TASK_FILE_PREFIX));
85
+ for (const file of files) {
86
+ try {
87
+ const content = readFileSync(join(tasksDir, file), "utf-8");
88
+ const task = JSON.parse(content) as PersistedScanTask;
89
+ // 恢复运行中的任务状态
90
+ if (task.status === "running") {
91
+ task.status = "pending"; // 重置为待处理,等待重新执行
92
+ }
93
+ this.tasks.set(task.id, task);
94
+ } catch (e) {
95
+ console.error(`[api-security-testing] Failed to load task from ${file}:`, e);
96
+ }
97
+ }
98
+ console.log(`[api-security-testing] Loaded ${this.tasks.size} persisted tasks`);
99
+ } catch (e) {
100
+ console.error(`[api-security-testing] Failed to load persisted tasks:`, e);
101
+ }
102
+ }
103
+
104
+ // 订阅 OpenCode 事件
105
+ private subscribeToEvents(): void {
106
+ if (!this.client) return;
107
+
108
+ // 使用 OpenCode 的 PTY 事件来跟踪后台任务
109
+ this.eventSubscription = this.client.event.subscribe({
110
+ body: { types: ["pty.*", "session.*"] }
111
+ });
112
+
113
+ console.log("[api-security-testing] Subscribed to OpenCode events");
114
+ }
115
+
116
+ // 创建新任务
117
+ createTask(type: string, target: string): PersistedScanTask {
118
+ const id = `scan_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
119
+ const task: PersistedScanTask = {
120
+ id,
121
+ status: "pending",
122
+ type,
123
+ target,
124
+ startTime: Date.now(),
125
+ progress: 0,
126
+ createdAt: new Date().toISOString(),
127
+ };
128
+ this.tasks.set(id, task);
129
+ this.persistTask(task);
130
+ return task;
131
+ }
132
+
133
+ // 更新任务状态
134
+ updateTask(taskId: string, updates: Partial<PersistedScanTask>): PersistedScanTask | null {
135
+ const task = this.tasks.get(taskId);
136
+ if (!task) return null;
137
+ Object.assign(task, updates);
138
+ this.persistTask(task);
139
+ return task;
140
+ }
141
+
142
+ // 获取任务
143
+ getTask(taskId: string): PersistedScanTask | undefined {
144
+ return this.tasks.get(taskId);
145
+ }
146
+
147
+ // 列出所有任务
148
+ listTasks(statusFilter?: string): PersistedScanTask[] {
149
+ const tasks = Array.from(this.tasks.values());
150
+ if (statusFilter && statusFilter !== "all") {
151
+ return tasks.filter(t => t.status === statusFilter);
152
+ }
153
+ return tasks.sort((a, b) => b.startTime - a.startTime);
154
+ }
155
+
156
+ // 取消任务
157
+ cancelTask(taskId: string): PersistedScanTask | null {
158
+ const task = this.tasks.get(taskId);
159
+ if (!task || task.status === "completed" || task.status === "failed") {
160
+ return null;
161
+ }
162
+ task.status = "cancelled";
163
+ task.endTime = Date.now();
164
+ task.error = "Cancelled by user";
165
+ this.persistTask(task);
166
+ return task;
167
+ }
168
+
169
+ // 使用 PTY API 创建后台任务
170
+ async createPtyTask(command: string, taskId: string): Promise<string | null> {
171
+ if (!this.client) {
172
+ console.error("[api-security-testing] Client not initialized");
173
+ return null;
174
+ }
175
+
176
+ try {
177
+ const result = await this.client.pty.create({
178
+ body: {
179
+ command,
180
+ cwd: this.directory,
181
+ env: { ...process.env },
182
+ cols: 120,
183
+ rows: 30,
184
+ }
185
+ });
186
+
187
+ if (result.error) {
188
+ console.error(`[api-security-testing] Failed to create PTY:`, result.error);
189
+ return null;
190
+ }
191
+
192
+ const ptyId = (result.data as { id?: string })?.id;
193
+ if (ptyId) {
194
+ this.updateTask(taskId, { ptyId, status: "running" });
195
+ return ptyId;
196
+ }
197
+ return null;
198
+ } catch (e) {
199
+ console.error(`[api-security-testing] PTY creation error:`, e);
200
+ return null;
201
+ }
202
+ }
203
+
204
+ // 获取 PTY 输出
205
+ async getPtyOutput(ptyId: string): Promise<string> {
206
+ if (!this.client) return "";
207
+
208
+ try {
209
+ const result = await this.client.pty.get({
210
+ path: { id: ptyId }
211
+ });
212
+
213
+ if (result.error) return "";
214
+ return (result.data as { output?: string })?.output || "";
215
+ } catch {
216
+ return "";
217
+ }
218
+ }
219
+
220
+ // 清理过期任务
221
+ cleanupOldTasks(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): void {
222
+ const cutoff = Date.now() - maxAgeMs;
223
+ for (const [id, task] of this.tasks) {
224
+ if (task.startTime < cutoff && (task.status === "completed" || task.status === "failed" || task.status === "cancelled")) {
225
+ this.tasks.delete(id);
226
+ this.removePersistedTask(id);
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ // 全局任务管理器实例
233
+ const taskManager = new ScanTaskManager();
234
+
235
+ // 向后兼容的简化接口
11
236
  interface ScanTask {
12
237
  id: string;
13
238
  status: "pending" | "running" | "completed" | "failed" | "cancelled";
@@ -63,6 +288,7 @@ async function processQueue(): Promise<void> {
63
288
  }
64
289
 
65
290
  const SKILL_DIR = "skills/api-security-testing";
291
+ const TASKS_DIR = ".opencode/tasks";
66
292
  const CORE_DIR = `${SKILL_DIR}/core`;
67
293
  const AGENTS_DIR = ".config/opencode/agents";
68
294
  const CONFIG_FILE = "api-security-testing.config.json";
@@ -282,7 +508,13 @@ function detectGiveUpPattern(text: string): boolean {
282
508
 
283
509
  const ApiSecurityTestingPlugin: Plugin = async (ctx) => {
284
510
  const config = loadConfig(ctx);
285
- console.log(`[api-security-testing] Plugin loaded v5.2.2 - collection_mode: ${config.collection_mode}`);
511
+
512
+ // 初始化任务管理器 (OpenCode 原生集成)
513
+ taskManager.init(ctx.directory, ctx.client);
514
+ taskManager.cleanupOldTasks(); // 清理过期任务
515
+
516
+ console.log(`[api-security-testing] Plugin loaded v5.4.0 - collection_mode: ${config.collection_mode}`);
517
+ console.log(`[api-security-testing] Task persistence enabled at ${join(ctx.directory, TASKS_DIR)}`);
286
518
 
287
519
  return {
288
520
  tool: {
@@ -902,6 +1134,135 @@ print(json.dumps(result, ensure_ascii=False))
902
1134
  }, null, 2);
903
1135
  },
904
1136
  }),
1137
+
1138
+ // ========================================
1139
+ // OpenCode 原生集成工具
1140
+ // ========================================
1141
+
1142
+ // PTY 后台扫描 - 使用 OpenCode 原生 PTY API
1143
+ pty_scan: tool({
1144
+ description: "使用 OpenCode PTY 创建后台安全扫描任务。任务在独立 PTY 会话中运行,支持持久化和重连。",
1145
+ args: {
1146
+ command: tool.schema.string(),
1147
+ description: tool.schema.string().optional(),
1148
+ },
1149
+ async execute(args, ctx) {
1150
+ const command = args.command as string;
1151
+ const taskDesc = (args.description as string) || "Background security scan";
1152
+
1153
+ // 创建持久化任务
1154
+ const task = taskManager.createTask("pty_scan", taskDesc);
1155
+
1156
+ // 创建 PTY 会话
1157
+ const ptyId = await taskManager.createPtyTask(command, task.id);
1158
+
1159
+ if (!ptyId) {
1160
+ taskManager.updateTask(task.id, {
1161
+ status: "failed",
1162
+ error: "Failed to create PTY session"
1163
+ });
1164
+ return JSON.stringify({
1165
+ error: "Failed to create PTY session",
1166
+ task_id: task.id
1167
+ }, null, 2);
1168
+ }
1169
+
1170
+ return JSON.stringify({
1171
+ success: true,
1172
+ task_id: task.id,
1173
+ pty_id: ptyId,
1174
+ status: "running",
1175
+ message: "Background scan started. Use pty_output to check progress.",
1176
+ persistence: `Task persisted to ${join(ctx.directory, TASKS_DIR)}`,
1177
+ }, null, 2);
1178
+ },
1179
+ }),
1180
+
1181
+ // PTY 输出读取
1182
+ pty_output: tool({
1183
+ description: "获取 PTY 后台任务的输出和状态。",
1184
+ args: {
1185
+ task_id: tool.schema.string(),
1186
+ },
1187
+ async execute(args) {
1188
+ const taskId = args.task_id as string;
1189
+ const task = taskManager.getTask(taskId);
1190
+
1191
+ if (!task) {
1192
+ return JSON.stringify({ error: `Task ${taskId} not found` }, null, 2);
1193
+ }
1194
+
1195
+ let output = "";
1196
+ if (task.ptyId) {
1197
+ output = await taskManager.getPtyOutput(task.ptyId);
1198
+ }
1199
+
1200
+ return JSON.stringify({
1201
+ task_id: task.id,
1202
+ status: task.status,
1203
+ progress: task.progress,
1204
+ output: output.slice(-5000), // 限制输出长度
1205
+ start_time: task.startTime,
1206
+ end_time: task.endTime,
1207
+ error: task.error,
1208
+ }, null, 2);
1209
+ },
1210
+ }),
1211
+
1212
+ // 持久化任务列表
1213
+ persisted_tasks: tool({
1214
+ description: "列出所有持久化的扫描任务(包括历史任务)。",
1215
+ args: {
1216
+ status_filter: tool.schema.enum(["all", "pending", "running", "completed", "failed", "cancelled"]).optional(),
1217
+ include_output: tool.schema.boolean().optional(),
1218
+ },
1219
+ async execute(args) {
1220
+ const statusFilter = (args.status_filter as string) || "all";
1221
+ const includeOutput = (args.include_output as boolean) || false;
1222
+
1223
+ const tasks = taskManager.listTasks(statusFilter);
1224
+
1225
+ return JSON.stringify({
1226
+ total: tasks.length,
1227
+ tasks: tasks.map(t => ({
1228
+ id: t.id,
1229
+ status: t.status,
1230
+ type: t.type,
1231
+ target: t.target,
1232
+ progress: t.progress,
1233
+ start_time: t.startTime,
1234
+ end_time: t.endTime,
1235
+ created_at: t.createdAt,
1236
+ pty_id: t.ptyId,
1237
+ ...(includeOutput && t.result ? { result: t.result.slice(-1000) } : {}),
1238
+ })),
1239
+ persistence_dir: TASKS_DIR,
1240
+ }, null, 2);
1241
+ },
1242
+ }),
1243
+
1244
+ // 事件订阅状态
1245
+ event_status: tool({
1246
+ description: "检查 OpenCode 事件订阅状态。",
1247
+ args: {},
1248
+ async execute() {
1249
+ return JSON.stringify({
1250
+ event_subscription_active: true,
1251
+ supported_events: [
1252
+ "pty.created",
1253
+ "pty.output",
1254
+ "pty.exited",
1255
+ "session.created",
1256
+ "session.deleted"
1257
+ ],
1258
+ task_persistence: {
1259
+ enabled: true,
1260
+ directory: TASKS_DIR,
1261
+ format: "JSON",
1262
+ },
1263
+ }, null, 2);
1264
+ },
1265
+ }),
905
1266
  },
906
1267
 
907
1268
  // 赛博监工 Hook - chat.message