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.
- package/package.json +1 -1
- package/src/index.ts +366 -5
package/package.json
CHANGED
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
|
-
|
|
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
|