opencode-api-security-testing 5.4.10 → 5.5.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/agents/api-cyber-supervisor.md +31 -5
- package/package.json +1 -1
- package/src/index.ts +822 -1
|
@@ -33,20 +33,46 @@ color: "#FF5733"
|
|
|
33
33
|
|------|------|------|
|
|
34
34
|
| api_security_scan | 完整扫描 | 全面测试 |
|
|
35
35
|
| api_fuzz_test | 模糊测试 | 发现未知端点 |
|
|
36
|
-
| browser_collect | 浏览器采集 | SPA 应用 |
|
|
36
|
+
| browser_collect | 浏览器采集 | SPA 应用 (**侦察首选**) |
|
|
37
37
|
| js_parse | JS 分析 | 提取 API 模式 |
|
|
38
38
|
| vuln_verify | 漏洞验证 | 确认发现 |
|
|
39
39
|
| graphql_test | GraphQL 测试 | GraphQL 端点 |
|
|
40
40
|
| cloud_storage_test | 云存储测试 | OSS/S3 |
|
|
41
41
|
| idor_test | IDOR 测试 | 越权漏洞 |
|
|
42
42
|
| sqli_test | SQLi 测试 | 注入漏洞 |
|
|
43
|
+
| pentest_planner | 测试规划 | 生成 Todo 清单 |
|
|
44
|
+
|
|
45
|
+
## 任务管理 (关键)
|
|
46
|
+
|
|
47
|
+
**默认行为**: 在开始任何多步测试之前,先使用 `todowrite` 创建 Todo 清单。
|
|
48
|
+
|
|
49
|
+
### 工作流 (不可违背)
|
|
50
|
+
|
|
51
|
+
1. **收到请求后立即**: 使用 `pentest_planner` 或手动用 `todowrite` 规划步骤
|
|
52
|
+
2. **开始每个步骤前**: 标记为 `in_progress` (同时只有一个)
|
|
53
|
+
3. **完成每个步骤后**: 立即标记为 `completed` (绝不批量)
|
|
54
|
+
4. **如果发现新目标**: 先更新 Todo 再继续
|
|
55
|
+
|
|
56
|
+
### 为什么不可违背
|
|
57
|
+
|
|
58
|
+
- **用户可见性**: 用户在右侧面板实时看到进度
|
|
59
|
+
- **防止漂移**: Todo 锚定你到实际请求
|
|
60
|
+
- **自动续写**: 系统在空闲时自动注入未完成任务
|
|
43
61
|
|
|
44
62
|
## 测试流程
|
|
45
63
|
|
|
46
|
-
### Phase 1: 侦察
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
### Phase 1: 侦察 (**必须使用无头浏览器**)
|
|
65
|
+
|
|
66
|
+
**第一步永远是 browser_collect**,不是 curl/fetch!
|
|
67
|
+
|
|
68
|
+
1. **browser_collect(url, mode="dynamic")** - 使用 Playwright 无头浏览器采集动态端点
|
|
69
|
+
2. js_parse 分析 JS 文件 - 提取 API 路由模式
|
|
70
|
+
3. 基于采集结果发现更多隐藏端点
|
|
71
|
+
|
|
72
|
+
> ⚠️ 为什么侦察阶段必须用 browser_collect?
|
|
73
|
+
> - SPA 应用需要执行 JS 才能获取动态路由
|
|
74
|
+
> - curl/fetch 只能获取静态 HTML,会遗漏大量 API 端点
|
|
75
|
+
> - Playwright 可以模拟真实浏览器行为,触发 XHR/Fetch 请求
|
|
50
76
|
|
|
51
77
|
### Phase 2: 分析
|
|
52
78
|
1. 识别技术栈
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -432,7 +432,7 @@ class ContextCollector {
|
|
|
432
432
|
|
|
433
433
|
return entries.sort((a, b) => {
|
|
434
434
|
const priorityDiff = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
|
|
435
|
-
if (priorityDiff !==
|
|
435
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
436
436
|
return a.registrationOrder - b.registrationOrder;
|
|
437
437
|
});
|
|
438
438
|
}
|
|
@@ -479,6 +479,127 @@ function clearSessionState(sessionID: string | undefined): void {
|
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
// ============================================================================
|
|
483
|
+
// P9 内部触发器 + Todo 持续注入系统 (参考 oh-my-opencode)
|
|
484
|
+
// ============================================================================
|
|
485
|
+
|
|
486
|
+
const P9_INTERNAL_INITIATOR = "<!-- P9_INTERNAL_INITIATOR -->";
|
|
487
|
+
|
|
488
|
+
// 创建内部 agent 文本 part (不会被 UI 显示)
|
|
489
|
+
function createP9InternalTextPart(text: string) {
|
|
490
|
+
return {
|
|
491
|
+
type: "text" as const,
|
|
492
|
+
text: `${text}\n${P9_INTERNAL_INITIATOR}`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Todo 持续注入提示
|
|
497
|
+
const P9_CONTINUATION_PROMPT = `[SYSTEM DIRECTIVE: P9 - TODO CONTINUATION]
|
|
498
|
+
|
|
499
|
+
渗透测试任务未完成。继续执行下一个待处理任务。
|
|
500
|
+
|
|
501
|
+
- 直接执行,不要询问用户
|
|
502
|
+
- 完成每个任务后立即标记为 completed
|
|
503
|
+
- 不要停止直到所有任务完成
|
|
504
|
+
- 如果所有测试已完成,生成最终报告`;
|
|
505
|
+
|
|
506
|
+
// 覆盖 todowrite 工具描述 (渗透测试专用)
|
|
507
|
+
const P9_TODOWRITE_DESCRIPTION = `使用此工具创建和管理渗透测试任务清单。
|
|
508
|
+
|
|
509
|
+
## 任务格式 (必须)
|
|
510
|
+
|
|
511
|
+
每个任务标题必须编码: [阶段] [动作] [目标] - 预期 [结果]
|
|
512
|
+
|
|
513
|
+
好的:
|
|
514
|
+
- "[侦察] browser_collect 采集 https://target - 提取 JS 端点"
|
|
515
|
+
- "[认证] sqli_test 测试 /api/login - 检测注入"
|
|
516
|
+
- "[验证] vuln_verify 确认 SQL 注入 - 获取 PoC"
|
|
517
|
+
|
|
518
|
+
坏的:
|
|
519
|
+
- "测试登录" (哪个接口? 怎么测? 预期什么?)
|
|
520
|
+
- "扫描目标" (太笼统)
|
|
521
|
+
|
|
522
|
+
## 测试阶段
|
|
523
|
+
|
|
524
|
+
1. **侦察**: browser_collect → js_parse → 端点发现
|
|
525
|
+
2. **认证**: 登录接口测试 → 认证绕过 → 暴力破解
|
|
526
|
+
3. **漏洞**: SQLi → IDOR → XSS → 信息泄露
|
|
527
|
+
4. **报告**: 生成结构化 Markdown 报告
|
|
528
|
+
|
|
529
|
+
## 管理规则
|
|
530
|
+
- 一次只标记一个 in_progress
|
|
531
|
+
- 完成后立即标记 completed
|
|
532
|
+
- 你的 TODO 会被 HOOK([SYSTEM REMINDER - TODO CONTINUATION]) 自动追踪`;
|
|
533
|
+
|
|
534
|
+
// Todo 持续状态追踪
|
|
535
|
+
const todoContinuationState = new Map<string, {
|
|
536
|
+
lastInjectedAt: number;
|
|
537
|
+
incompleteCount: number;
|
|
538
|
+
stagnationCount: number;
|
|
539
|
+
}>();
|
|
540
|
+
|
|
541
|
+
const CONTINUATION_COOLDOWN_MS = 5000;
|
|
542
|
+
const MAX_STAGNATION_COUNT = 3;
|
|
543
|
+
|
|
544
|
+
function getIncompleteCount(todos: Array<{ status: string }>): number {
|
|
545
|
+
return todos.filter(t =>
|
|
546
|
+
t.status !== "completed" &&
|
|
547
|
+
t.status !== "cancelled" &&
|
|
548
|
+
t.status !== "blocked"
|
|
549
|
+
).length;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 系统提示 - 渗透测试 Todo 管理 (注入到 system prompt, 参考 oh-my-opencode Sisyphus)
|
|
553
|
+
const PENTEST_TODO_SYSTEM_PROMPT = `<Pentest_Task_Management>
|
|
554
|
+
## 渗透测试 Todo 管理 (关键)
|
|
555
|
+
|
|
556
|
+
**默认行为**: 在开始任何渗透测试之前,必须先使用 todowrite 创建 Todo 清单。这是你的首要协调机制。
|
|
557
|
+
|
|
558
|
+
### 何时创建 Todo (必须)
|
|
559
|
+
|
|
560
|
+
- 多步骤测试 (2+ 步骤) → 始终先创建 Todo
|
|
561
|
+
- 用户提出安全测试请求 → 始终
|
|
562
|
+
- 使用 pentest_planner 工具后 → 立即将输出转为 todowrite 调用
|
|
563
|
+
- 复杂单一任务 → 创建 Todo 拆解
|
|
564
|
+
|
|
565
|
+
### 工作流 (不可协商)
|
|
566
|
+
|
|
567
|
+
1. **收到请求后立即**: 使用 todowrite 规划原子步骤
|
|
568
|
+
2. **开始每步前**: 标记 in_progress (同时只有一个)
|
|
569
|
+
3. **完成每步后**: 立即标记 completed (绝不批量)
|
|
570
|
+
4. **范围变化时**: 先更新 Todo 再继续
|
|
571
|
+
|
|
572
|
+
### 侦察阶段 (必须先使用 Playwright 无头浏览器)
|
|
573
|
+
|
|
574
|
+
**侦察阶段必须第一步就使用 browser_collect 工具 (基于 Playwright)!**
|
|
575
|
+
不要用 curl/fetch 做侦察,必须直接上无头浏览器。
|
|
576
|
+
|
|
577
|
+
侦察顺序:
|
|
578
|
+
1. browser_collect(url, mode="dynamic") - 用 Playwright 采集 SPA 动态内容
|
|
579
|
+
2. js_parse(file_path) - 分析 JS 文件中的 API 模式
|
|
580
|
+
3. 根据发现使用专业工具深入
|
|
581
|
+
|
|
582
|
+
### 为什么这是不可协商的
|
|
583
|
+
|
|
584
|
+
- **用户可见性**: 用户在右侧面板实时看到进度
|
|
585
|
+
- **防止漂移**: Todo 锚定你到实际请求
|
|
586
|
+
- **自动继续**: 系统会自动追踪并注入未完成的 Todo,驱动你继续执行
|
|
587
|
+
- **问责制**: 每个 Todo = 明确承诺
|
|
588
|
+
|
|
589
|
+
### 反模式 (禁止)
|
|
590
|
+
|
|
591
|
+
- 多步测试跳过 Todo - 用户无可见性,步骤被遗忘
|
|
592
|
+
- 批量完成多个 Todo - 失去实时追踪意义
|
|
593
|
+
- 不标记 in_progress 就开始 - 无法知道你在做什么
|
|
594
|
+
- 完成但不标记 completed - 任务对用户显示为未完成
|
|
595
|
+
|
|
596
|
+
**未在非琐碎任务上使用 Todo = 不完整的工作。**
|
|
597
|
+
|
|
598
|
+
你的 Todo 创建会被 HOOK([SYSTEM REMINDER - TODO CONTINUATION]) 自动追踪。
|
|
599
|
+
</Pentest_Task_Management>`;
|
|
600
|
+
|
|
601
|
+
const TODO_HOOK_NOTE = `\n[P9 HOOK NOTE] 你的 Todo 创建会被 HOOK([SYSTEM REMINDER - TODO CONTINUATION]) 自动追踪。当会话空闲且有未完成任务时,系统会自动注入续写提示。`;
|
|
602
|
+
|
|
482
603
|
function getConfigPath(ctx: { directory: string }): string {
|
|
483
604
|
return join(ctx.directory, SKILL_DIR, "assets", CONFIG_FILE);
|
|
484
605
|
}
|
|
@@ -651,6 +772,272 @@ function resetFailureCount(sessionID: string): void {
|
|
|
651
772
|
sessionFailures.delete(sessionID);
|
|
652
773
|
}
|
|
653
774
|
|
|
775
|
+
// ============================================================================
|
|
776
|
+
// 渗透测试规划引擎 - 根据侦察结果生成测试计划
|
|
777
|
+
// ============================================================================
|
|
778
|
+
|
|
779
|
+
interface TestPlanItem {
|
|
780
|
+
id: string;
|
|
781
|
+
phase: string;
|
|
782
|
+
task: string;
|
|
783
|
+
tool: string;
|
|
784
|
+
params: Record<string, unknown>;
|
|
785
|
+
priority: "P0" | "P1" | "P2" | "P3";
|
|
786
|
+
status: "pending" | "running" | "done" | "skipped";
|
|
787
|
+
depends_on: string[];
|
|
788
|
+
reason: string;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function generateTestPlan(
|
|
792
|
+
target: string,
|
|
793
|
+
scope: string,
|
|
794
|
+
depth: string,
|
|
795
|
+
reconResults: Record<string, unknown>,
|
|
796
|
+
customRequirements: string,
|
|
797
|
+
): { target: string; scope: string; depth: string; plan: TestPlanItem[]; summary: Record<string, number> } {
|
|
798
|
+
const plan: TestPlanItem[] = [];
|
|
799
|
+
let id = 0;
|
|
800
|
+
|
|
801
|
+
// 分析侦察结果
|
|
802
|
+
const isSPA = reconResults.is_spa === true;
|
|
803
|
+
const apiCount = (reconResults.api_paths_found as number) || 0;
|
|
804
|
+
const framework = reconResults.framework || "Unknown";
|
|
805
|
+
const secChecks = (reconResults.security_checks as Array<{ endpoint: string; status: number }>) || [];
|
|
806
|
+
|
|
807
|
+
// 发现暴露的端点
|
|
808
|
+
const exposedEndpoints = secChecks.filter(c => c.status === 200);
|
|
809
|
+
const hasExposedEndpoints = exposedEndpoints.length > 0;
|
|
810
|
+
|
|
811
|
+
// Phase 1: 侦察 (始终需要)
|
|
812
|
+
plan.push({
|
|
813
|
+
id: `T${++id}`,
|
|
814
|
+
phase: "侦察",
|
|
815
|
+
task: "JS 文件分析 - 提取 API 端点",
|
|
816
|
+
tool: "js_parse",
|
|
817
|
+
params: { file_path: `${target}/js/app.*.js` },
|
|
818
|
+
priority: "P0",
|
|
819
|
+
status: "pending",
|
|
820
|
+
depends_on: [],
|
|
821
|
+
reason: isSPA ? "SPA 应用,需要从 JS 中提取所有 API 路径" : "分析 JS 文件中的 API 调用模式",
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
plan.push({
|
|
825
|
+
id: `T${++id}`,
|
|
826
|
+
phase: "侦察",
|
|
827
|
+
task: "API 端点发现 - 完整扫描",
|
|
828
|
+
tool: "endpoint_batch_test",
|
|
829
|
+
params: { base_url: target, endpoints: ["/api", "/admin", "/v1", "/v2"], methods: ["GET", "POST"] },
|
|
830
|
+
priority: "P0",
|
|
831
|
+
status: "pending",
|
|
832
|
+
depends_on: [],
|
|
833
|
+
reason: "发现所有可访问的 API 端点",
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Phase 2: 认证测试 (如果有登录接口)
|
|
837
|
+
if (scope === "full" || scope === "auth_only") {
|
|
838
|
+
plan.push({
|
|
839
|
+
id: `T${++id}`,
|
|
840
|
+
phase: "认证测试",
|
|
841
|
+
task: "登录接口 SQL 注入测试",
|
|
842
|
+
tool: "sql_injection_test",
|
|
843
|
+
params: { endpoint: `${target}/admin/upms/login/doLogin`, params: ["loginName", "password"], payload_type: "basic" },
|
|
844
|
+
priority: "P0",
|
|
845
|
+
status: "pending",
|
|
846
|
+
depends_on: ["T1"],
|
|
847
|
+
reason: "登录接口是最常见的 SQL 注入入口",
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
plan.push({
|
|
851
|
+
id: `T${++id}`,
|
|
852
|
+
phase: "认证测试",
|
|
853
|
+
task: "认证绕过测试",
|
|
854
|
+
tool: "auth_bypass_test",
|
|
855
|
+
params: { endpoint: `${target}/admin/upms/sysUser/list`, auth_type: "session" },
|
|
856
|
+
priority: "P1",
|
|
857
|
+
status: "pending",
|
|
858
|
+
depends_on: ["T1"],
|
|
859
|
+
reason: "测试是否可以绕过认证访问受保护资源",
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
plan.push({
|
|
863
|
+
id: `T${++id}`,
|
|
864
|
+
phase: "认证测试",
|
|
865
|
+
task: "暴力破解保护检测",
|
|
866
|
+
tool: "endpoint_batch_test",
|
|
867
|
+
params: { base_url: target, endpoints: Array(10).fill("/admin/upms/login/doLogin"), methods: ["POST"] },
|
|
868
|
+
priority: "P1",
|
|
869
|
+
status: "pending",
|
|
870
|
+
depends_on: ["T3"],
|
|
871
|
+
reason: "连续发送登录请求,检测是否有速率限制",
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Phase 3: 深度测试 (根据 depth 决定)
|
|
876
|
+
if (depth === "deep" || depth === "stealth") {
|
|
877
|
+
plan.push({
|
|
878
|
+
id: `T${++id}`,
|
|
879
|
+
phase: "深度测试",
|
|
880
|
+
task: "时间盲注 SQL 注入测试",
|
|
881
|
+
tool: "sql_injection_test",
|
|
882
|
+
params: { endpoint: `${target}/admin/upms/login/doLogin`, params: ["loginName"], payload_type: "time_based" },
|
|
883
|
+
priority: "P1",
|
|
884
|
+
status: "pending",
|
|
885
|
+
depends_on: ["T3"],
|
|
886
|
+
reason: "基础注入未发现时,测试时间盲注",
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
plan.push({
|
|
890
|
+
id: `T${++id}`,
|
|
891
|
+
phase: "深度测试",
|
|
892
|
+
task: "IDOR 越权测试",
|
|
893
|
+
tool: "auth_bypass_test",
|
|
894
|
+
params: { endpoint: `${target}/admin/upms/sysUser/view`, auth_type: "api_key" },
|
|
895
|
+
priority: "P1",
|
|
896
|
+
status: "pending",
|
|
897
|
+
depends_on: ["T4"],
|
|
898
|
+
reason: "测试用户数据接口是否存在 IDOR 越权",
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Phase 4: 如果发现暴露端点
|
|
903
|
+
if (hasExposedEndpoints) {
|
|
904
|
+
for (const ep of exposedEndpoints) {
|
|
905
|
+
plan.push({
|
|
906
|
+
id: `T${++id}`,
|
|
907
|
+
phase: "暴露端点测试",
|
|
908
|
+
task: `测试暴露端点: ${ep.endpoint}`,
|
|
909
|
+
tool: "endpoint_batch_test",
|
|
910
|
+
params: { base_url: target, endpoints: [ep.endpoint], methods: ["GET", "POST"] },
|
|
911
|
+
priority: "P0",
|
|
912
|
+
status: "pending",
|
|
913
|
+
depends_on: [],
|
|
914
|
+
reason: `端点 ${ep.endpoint} 返回 HTTP 200,可能存在信息泄露`,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Phase 5: 报告生成 (始终最后)
|
|
920
|
+
plan.push({
|
|
921
|
+
id: `T${++id}`,
|
|
922
|
+
phase: "报告",
|
|
923
|
+
task: "生成渗透测试报告",
|
|
924
|
+
tool: "generate_pentest_report",
|
|
925
|
+
params: { target },
|
|
926
|
+
priority: "P2",
|
|
927
|
+
status: "pending",
|
|
928
|
+
depends_on: plan.map(t => t.id),
|
|
929
|
+
reason: "所有测试完成后生成最终报告",
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// 统计
|
|
933
|
+
const summary = {
|
|
934
|
+
total_tasks: plan.length,
|
|
935
|
+
p0_tasks: plan.filter(t => t.priority === "P0").length,
|
|
936
|
+
p1_tasks: plan.filter(t => t.priority === "P1").length,
|
|
937
|
+
p2_tasks: plan.filter(t => t.priority === "P2").length,
|
|
938
|
+
phases: [...new Set(plan.map(t => t.phase))].length,
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
target,
|
|
943
|
+
scope,
|
|
944
|
+
depth,
|
|
945
|
+
plan,
|
|
946
|
+
summary,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// ============================================================================
|
|
951
|
+
// 自主决策引擎 - 根据当前状态选择最优下一步
|
|
952
|
+
// ============================================================================
|
|
953
|
+
|
|
954
|
+
interface NextAction {
|
|
955
|
+
name: string;
|
|
956
|
+
tool: string;
|
|
957
|
+
params: Record<string, unknown>;
|
|
958
|
+
reason: string;
|
|
959
|
+
priority: "P0" | "P1" | "P2" | "P3";
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function decideNextActions(state: {
|
|
963
|
+
target: string;
|
|
964
|
+
mode: string;
|
|
965
|
+
focus: string;
|
|
966
|
+
findings: Array<{ type: string; detail: string; severity: string }>;
|
|
967
|
+
completed_tasks: string[];
|
|
968
|
+
api_endpoints: string[];
|
|
969
|
+
auth_required: boolean;
|
|
970
|
+
tech_stack: string[];
|
|
971
|
+
recon_data: Record<string, unknown>;
|
|
972
|
+
}): NextAction[] {
|
|
973
|
+
const actions: NextAction[] = [];
|
|
974
|
+
const target = state.target;
|
|
975
|
+
|
|
976
|
+
// 规则 1: 如果发现 SQL 注入线索,深入测试
|
|
977
|
+
const sqliFindings = state.findings.filter(f => f.type === "sqli");
|
|
978
|
+
if (sqliFindings.length > 0) {
|
|
979
|
+
actions.push({
|
|
980
|
+
name: "深入 SQL 注入测试 (union/error_based)",
|
|
981
|
+
tool: "sql_injection_test",
|
|
982
|
+
params: { endpoint: `${target}/admin/upms/login/doLogin`, params: ["loginName"], payload_type: "union" },
|
|
983
|
+
reason: `已发现 ${sqliFindings.length} 个 SQL 注入线索,需要进一步验证`,
|
|
984
|
+
priority: "P0",
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// 规则 2: 如果认证正常,尝试认证绕过
|
|
989
|
+
if (state.auth_required && !state.completed_tasks.includes("test_auth_bypass")) {
|
|
990
|
+
actions.push({
|
|
991
|
+
name: "认证绕过测试",
|
|
992
|
+
tool: "auth_bypass_test",
|
|
993
|
+
params: { endpoint: `${target}/admin/upms/sysUser/list`, auth_type: "session" },
|
|
994
|
+
reason: "目标需要认证,测试是否可以绕过",
|
|
995
|
+
priority: "P0",
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// 规则 3: 如果有多个 API 端点,批量测试
|
|
1000
|
+
if (state.api_endpoints.length > 5) {
|
|
1001
|
+
const untestedEndpoints = state.api_endpoints.filter((ep: string) =>
|
|
1002
|
+
!ep.includes("login") && !ep.includes("doLogin")
|
|
1003
|
+
).slice(0, 10);
|
|
1004
|
+
|
|
1005
|
+
actions.push({
|
|
1006
|
+
name: "批量 API 端点测试",
|
|
1007
|
+
tool: "endpoint_batch_test",
|
|
1008
|
+
params: { base_url: target, endpoints: untestedEndpoints, methods: ["GET", "POST"] },
|
|
1009
|
+
reason: `发现 ${state.api_endpoints.length} 个 API 端点,需要批量检测认证和响应状态`,
|
|
1010
|
+
priority: "P1",
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// 规则 4: 如果技术栈包含 Java/Spring,测试 Actuator
|
|
1015
|
+
if (state.tech_stack.some(t => t.includes("Spring") || t.includes("Element"))) {
|
|
1016
|
+
actions.push({
|
|
1017
|
+
name: "Spring Boot Actuator 端点测试",
|
|
1018
|
+
tool: "endpoint_batch_test",
|
|
1019
|
+
params: {
|
|
1020
|
+
base_url: target,
|
|
1021
|
+
endpoints: ["/actuator", "/actuator/env", "/actuator/health", "/actuator/mappings", "/actuator/heapdump"],
|
|
1022
|
+
methods: ["GET"],
|
|
1023
|
+
},
|
|
1024
|
+
reason: "检测 Spring Boot Actuator 端点是否暴露",
|
|
1025
|
+
priority: "P1",
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// 规则 5: 生成报告
|
|
1030
|
+
actions.push({
|
|
1031
|
+
name: "生成渗透测试报告",
|
|
1032
|
+
tool: "generate_pentest_report",
|
|
1033
|
+
params: { target, findings: state.findings },
|
|
1034
|
+
reason: "汇总所有测试结果",
|
|
1035
|
+
priority: "P2",
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
return actions;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
654
1041
|
function detectGiveUpPattern(text: string): boolean {
|
|
655
1042
|
return CYBER_SUPERVISOR.give_up_patterns.some(p =>
|
|
656
1043
|
text.toLowerCase().includes(p.toLowerCase())
|
|
@@ -1663,6 +2050,330 @@ ${f.remediation}
|
|
|
1663
2050
|
},
|
|
1664
2051
|
}),
|
|
1665
2052
|
|
|
2053
|
+
// ========================================
|
|
2054
|
+
// 自主任务编排 + 测试规划
|
|
2055
|
+
// ========================================
|
|
2056
|
+
|
|
2057
|
+
// 渗透测试规划器 - 先规划后执行 (使用 OpenCode 内置 todowrite)
|
|
2058
|
+
pentest_planner: tool({
|
|
2059
|
+
description: "渗透测试规划器。侦察目标后输出 todowrite 指令,让你使用 OpenCode 内置 Todo 面板追踪测试进度。参数: target(目标URL), scope(测试范围), depth(测试深度)",
|
|
2060
|
+
args: {
|
|
2061
|
+
target: tool.schema.string(),
|
|
2062
|
+
scope: tool.schema.enum(["full", "auth_only", "api_only", "infra_only", "quick"]).optional(),
|
|
2063
|
+
depth: tool.schema.enum(["surface", "deep", "stealth"]).optional(),
|
|
2064
|
+
custom_requirements: tool.schema.string().optional(),
|
|
2065
|
+
},
|
|
2066
|
+
async execute(args) {
|
|
2067
|
+
const target = args.target;
|
|
2068
|
+
const scope = args.scope || "full";
|
|
2069
|
+
const depth = args.depth || "surface";
|
|
2070
|
+
const requirements = args.custom_requirements || "";
|
|
2071
|
+
|
|
2072
|
+
// Phase 1: 侦察 - 快速探测目标
|
|
2073
|
+
const reconResults: Record<string, unknown> = {};
|
|
2074
|
+
|
|
2075
|
+
try {
|
|
2076
|
+
// 获取首页
|
|
2077
|
+
const homeResponse = await fetch(target, {
|
|
2078
|
+
headers: { "User-Agent": "Mozilla/5.0" },
|
|
2079
|
+
});
|
|
2080
|
+
reconResults.home_status = homeResponse.status;
|
|
2081
|
+
reconResults.content_type = homeResponse.headers.get("content-type");
|
|
2082
|
+
|
|
2083
|
+
// 获取 JS 文件列表
|
|
2084
|
+
const homeText = await homeResponse.text();
|
|
2085
|
+
const jsFiles = homeText.match(/\/js\/[^\s"]+\.js/g) || [];
|
|
2086
|
+
reconResults.js_files_count = jsFiles.length;
|
|
2087
|
+
reconResults.is_spa = homeText.includes("app.js") || homeText.includes("chunk~");
|
|
2088
|
+
reconResults.framework = homeText.includes("vue") ? "Vue.js"
|
|
2089
|
+
: homeText.includes("react") ? "React"
|
|
2090
|
+
: homeText.includes("angular") ? "Angular"
|
|
2091
|
+
: "Unknown";
|
|
2092
|
+
|
|
2093
|
+
// 检测 API 路径
|
|
2094
|
+
const apiPaths = homeText.match(/doUrl\("([^"]+)"/g) || [];
|
|
2095
|
+
reconResults.api_paths_found = apiPaths.length;
|
|
2096
|
+
|
|
2097
|
+
// 检测常见安全端点
|
|
2098
|
+
const securityChecks: Array<{ endpoint: string; status: number }> = [];
|
|
2099
|
+
const checkEndpoints = ["/api", "/swagger-ui.html", "/actuator/health", "/druid", "/.env"];
|
|
2100
|
+
for (const ep of checkEndpoints) {
|
|
2101
|
+
try {
|
|
2102
|
+
const checkUrl = new URL(ep, target).href;
|
|
2103
|
+
const resp = await fetch(checkUrl, { method: "HEAD" });
|
|
2104
|
+
securityChecks.push({ endpoint: ep, status: resp.status });
|
|
2105
|
+
} catch (e) {
|
|
2106
|
+
securityChecks.push({ endpoint: ep, status: 0 });
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
reconResults.security_checks = securityChecks;
|
|
2110
|
+
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
reconResults.error = String(error);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// Phase 2: 根据侦察结果生成测试计划
|
|
2116
|
+
const plan = generateTestPlan(target, scope, depth, reconResults, requirements);
|
|
2117
|
+
|
|
2118
|
+
// Phase 3: 输出 todowrite 指令而非 JSON
|
|
2119
|
+
// 这样 LLM 会调用 OpenCode 内置的 todowrite 工具,显示在右侧面板
|
|
2120
|
+
const todoItems = plan.plan.map(t => {
|
|
2121
|
+
const status = t.priority === "P0" ? "pending" : "pending";
|
|
2122
|
+
return ` { content: "${t.phase}: ${t.task} - 预期${t.reason.includes("泄露") ? "发现/排除信息泄露" : t.reason.includes("注入") ? "确认/排除注入漏洞" : "完成测试"}, 使用 ${t.tool}", status: "${status}", priority: "${t.priority === "P0" ? "high" : t.priority === "P1" ? "medium" : "low"}" }`;
|
|
2123
|
+
}).join(",\n");
|
|
2124
|
+
|
|
2125
|
+
return `## 侦察完成 - 目标分析结果
|
|
2126
|
+
|
|
2127
|
+
**目标**: ${target}
|
|
2128
|
+
**技术栈**: ${reconResults.framework || "Unknown"} | SPA: ${reconResults.is_spa ? "是" : "否"} | JS文件: ${reconResults.js_files_count || 0} | API路径: ${reconResults.api_paths_found || 0}
|
|
2129
|
+
**首页状态**: ${reconResults.home_status || "N/A"}
|
|
2130
|
+
|
|
2131
|
+
### 安全端点探测
|
|
2132
|
+
${JSON.stringify(reconResults.security_checks || [], null, 2)}
|
|
2133
|
+
|
|
2134
|
+
### 测试计划已生成 (${plan.summary.total_tasks} 项任务)
|
|
2135
|
+
|
|
2136
|
+
**⚠️ 请立即使用 \`todowrite\` 工具创建以下 Todo 清单 (会显示在右侧面板):**
|
|
2137
|
+
|
|
2138
|
+
\`\`\`
|
|
2139
|
+
todowrite({
|
|
2140
|
+
todos: [
|
|
2141
|
+
${todoItems}
|
|
2142
|
+
]
|
|
2143
|
+
})
|
|
2144
|
+
\`\`\`
|
|
2145
|
+
|
|
2146
|
+
创建 Todo 后,系统会自动追踪进度并在你完成每个任务后自动继续下一个。
|
|
2147
|
+
从第一个 P0 任务开始执行!`;
|
|
2148
|
+
},
|
|
2149
|
+
}),
|
|
2150
|
+
|
|
2151
|
+
// 自主渗透测试引擎 - 自动 Hook 下一步 + 最优解选择
|
|
2152
|
+
auto_pentest: tool({
|
|
2153
|
+
description: "自主渗透测试引擎。自动执行侦察->规划->测试->验证全流程,根据每步结果自主选择下一步最优操作。参数: target(目标URL), max_steps(最大步骤数), mode(模式)",
|
|
2154
|
+
args: {
|
|
2155
|
+
target: tool.schema.string(),
|
|
2156
|
+
max_steps: tool.schema.number().optional(),
|
|
2157
|
+
mode: tool.schema.enum(["recon", "full", "stealth", "aggressive"]).optional(),
|
|
2158
|
+
focus: tool.schema.enum(["sqli", "auth", "idor", "xss", "all"]).optional(),
|
|
2159
|
+
},
|
|
2160
|
+
async execute(args) {
|
|
2161
|
+
const target = args.target;
|
|
2162
|
+
const maxSteps = args.max_steps || 10;
|
|
2163
|
+
const mode = args.mode || "full";
|
|
2164
|
+
const focus = args.focus || "all";
|
|
2165
|
+
|
|
2166
|
+
// 自主编排引擎状态
|
|
2167
|
+
const engineState = {
|
|
2168
|
+
target,
|
|
2169
|
+
mode,
|
|
2170
|
+
focus,
|
|
2171
|
+
current_phase: "recon" as string,
|
|
2172
|
+
step: 0,
|
|
2173
|
+
max_steps: maxSteps,
|
|
2174
|
+
findings: [] as Array<{ type: string; detail: string; severity: string }>,
|
|
2175
|
+
completed_tasks: [] as string[],
|
|
2176
|
+
next_actions: [] as string[],
|
|
2177
|
+
// 自主决策需要的数据
|
|
2178
|
+
recon_data: {} as Record<string, unknown>,
|
|
2179
|
+
api_endpoints: [] as string[],
|
|
2180
|
+
auth_required: false,
|
|
2181
|
+
tech_stack: [] as string[],
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
// === Phase 1: 侦察 (自动) ===
|
|
2185
|
+
engineState.current_phase = "recon";
|
|
2186
|
+
engineState.step = 1;
|
|
2187
|
+
|
|
2188
|
+
// 1.1 获取首页
|
|
2189
|
+
try {
|
|
2190
|
+
const homeResponse = await fetch(target);
|
|
2191
|
+
const homeText = await homeResponse.text();
|
|
2192
|
+
|
|
2193
|
+
engineState.recon_data.home_status = homeResponse.status;
|
|
2194
|
+
engineState.recon_data.is_spa = homeText.includes("chunk~") || homeText.includes("app.");
|
|
2195
|
+
|
|
2196
|
+
// 检测技术栈
|
|
2197
|
+
if (homeText.includes("vue") || homeText.includes("Vue")) engineState.tech_stack.push("Vue.js");
|
|
2198
|
+
if (homeText.includes("element-ui")) engineState.tech_stack.push("Element UI");
|
|
2199
|
+
if (homeText.includes("axios")) engineState.tech_stack.push("Axios");
|
|
2200
|
+
|
|
2201
|
+
// 提取 API 端点
|
|
2202
|
+
const apiMatches = homeText.match(/doUrl\("([^"]+)"/g) || [];
|
|
2203
|
+
engineState.api_endpoints = apiMatches.map((m: string) => {
|
|
2204
|
+
const match = m.match(/"([^"]+)"/);
|
|
2205
|
+
return match ? match[1] : "";
|
|
2206
|
+
}).filter(Boolean);
|
|
2207
|
+
|
|
2208
|
+
engineState.recon_data.api_count = engineState.api_endpoints.length;
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
engineState.findings.push({
|
|
2211
|
+
type: "recon_error",
|
|
2212
|
+
detail: String(error),
|
|
2213
|
+
severity: "Info",
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// 1.2 提取 JS 中的端点 (如果有主 JS 文件)
|
|
2218
|
+
try {
|
|
2219
|
+
const jsUrl = new URL("/js/app.6ae9dcec.js", target).href;
|
|
2220
|
+
const jsResponse = await fetch(jsUrl);
|
|
2221
|
+
if (jsResponse.status === 200) {
|
|
2222
|
+
const jsText = await jsResponse.text();
|
|
2223
|
+
const doUrlMatches = jsText.match(/doUrl\("([^"]+)"/g) || [];
|
|
2224
|
+
const additionalEndpoints = doUrlMatches.map((m: string) => {
|
|
2225
|
+
const match = m.match(/"([^"]+)"/);
|
|
2226
|
+
return match ? match[1] : "";
|
|
2227
|
+
}).filter(Boolean);
|
|
2228
|
+
|
|
2229
|
+
engineState.api_endpoints = [...new Set([...engineState.api_endpoints, ...additionalEndpoints])];
|
|
2230
|
+
}
|
|
2231
|
+
} catch (e) {
|
|
2232
|
+
// Ignore JS extraction errors
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
engineState.completed_tasks.push("recon_homepage");
|
|
2236
|
+
engineState.completed_tasks.push("recon_js_analysis");
|
|
2237
|
+
engineState.completed_tasks.push("recon_api_discovery");
|
|
2238
|
+
|
|
2239
|
+
// === Phase 2: 根据侦察结果自主决策下一步 ===
|
|
2240
|
+
engineState.current_phase = "analysis";
|
|
2241
|
+
engineState.step = 2;
|
|
2242
|
+
|
|
2243
|
+
// 2.1 检测认证需求
|
|
2244
|
+
const authEndpoints = engineState.api_endpoints.filter((ep: string) =>
|
|
2245
|
+
!ep.includes("login") && !ep.includes("doLogin")
|
|
2246
|
+
);
|
|
2247
|
+
|
|
2248
|
+
if (authEndpoints.length > 0) {
|
|
2249
|
+
const testUrl = new URL(authEndpoints[0], target).href;
|
|
2250
|
+
try {
|
|
2251
|
+
const testResponse = await fetch(testUrl, {
|
|
2252
|
+
method: "POST",
|
|
2253
|
+
headers: { "Content-Type": "application/json" },
|
|
2254
|
+
body: JSON.stringify({}),
|
|
2255
|
+
});
|
|
2256
|
+
engineState.auth_required = testResponse.status === 401 || testResponse.status === 403;
|
|
2257
|
+
} catch (e) {
|
|
2258
|
+
// Ignore
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
engineState.completed_tasks.push("auth_detection");
|
|
2263
|
+
|
|
2264
|
+
// === Phase 3: 自动选择最优解并生成下一步行动 ===
|
|
2265
|
+
engineState.current_phase = "planning";
|
|
2266
|
+
engineState.step = 3;
|
|
2267
|
+
|
|
2268
|
+
const nextActions = decideNextActions(engineState);
|
|
2269
|
+
engineState.next_actions = nextActions;
|
|
2270
|
+
|
|
2271
|
+
// === Phase 4: 执行关键测试 ===
|
|
2272
|
+
engineState.current_phase = "testing";
|
|
2273
|
+
|
|
2274
|
+
// 4.1 测试登录接口 (如果有)
|
|
2275
|
+
const loginEndpoint = engineState.api_endpoints.find((ep: string) =>
|
|
2276
|
+
ep.includes("login") || ep.includes("doLogin")
|
|
2277
|
+
);
|
|
2278
|
+
|
|
2279
|
+
if (loginEndpoint) {
|
|
2280
|
+
engineState.step = 4;
|
|
2281
|
+
const loginUrl = new URL(loginEndpoint, target).href;
|
|
2282
|
+
|
|
2283
|
+
// 测试参数验证
|
|
2284
|
+
try {
|
|
2285
|
+
const emptyResponse = await fetch(loginUrl, {
|
|
2286
|
+
method: "POST",
|
|
2287
|
+
headers: { "Content-Type": "application/json" },
|
|
2288
|
+
body: JSON.stringify({}),
|
|
2289
|
+
});
|
|
2290
|
+
const emptyText = await emptyResponse.text();
|
|
2291
|
+
|
|
2292
|
+
if (emptyText.includes("ARGUMENT_NULL") || emptyText.includes("验证失败")) {
|
|
2293
|
+
engineState.findings.push({
|
|
2294
|
+
type: "info_leakage",
|
|
2295
|
+
detail: `登录接口参数验证泄露: ${emptyText.substring(0, 100)}`,
|
|
2296
|
+
severity: "Low",
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
} catch (e) {
|
|
2300
|
+
// Ignore
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// SQL 注入快速测试
|
|
2304
|
+
try {
|
|
2305
|
+
const sqliPayloads = ["' OR '1'='1", "admin'--", "1' AND SLEEP(3)--"];
|
|
2306
|
+
for (const payload of sqliPayloads) {
|
|
2307
|
+
const sqliResponse = await fetch(loginUrl, {
|
|
2308
|
+
method: "POST",
|
|
2309
|
+
headers: { "Content-Type": "application/json" },
|
|
2310
|
+
body: JSON.stringify({ loginName: payload, password: "test" }),
|
|
2311
|
+
});
|
|
2312
|
+
const sqliText = await sqliResponse.text();
|
|
2313
|
+
|
|
2314
|
+
if (sqliText.toLowerCase().includes("sql") || sqliText.toLowerCase().includes("syntax")) {
|
|
2315
|
+
engineState.findings.push({
|
|
2316
|
+
type: "sqli",
|
|
2317
|
+
detail: `SQL 注入可能存在: payload=${payload}, response=${sqliText.substring(0, 100)}`,
|
|
2318
|
+
severity: "High",
|
|
2319
|
+
});
|
|
2320
|
+
break;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
} catch (e) {
|
|
2324
|
+
// Ignore
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
engineState.completed_tasks.push("test_login_sqli");
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// === Phase 5: 生成最终报告 ===
|
|
2331
|
+
engineState.current_phase = "report";
|
|
2332
|
+
engineState.step = 5;
|
|
2333
|
+
|
|
2334
|
+
const result = {
|
|
2335
|
+
status: "completed",
|
|
2336
|
+
target: engineState.target,
|
|
2337
|
+
mode: engineState.mode,
|
|
2338
|
+
focus: engineState.focus,
|
|
2339
|
+
total_steps: engineState.step,
|
|
2340
|
+
max_steps: engineState.max_steps,
|
|
2341
|
+
|
|
2342
|
+
// 侦察摘要
|
|
2343
|
+
recon_summary: {
|
|
2344
|
+
tech_stack: engineState.tech_stack,
|
|
2345
|
+
api_endpoints_found: engineState.api_endpoints.length,
|
|
2346
|
+
is_spa: engineState.recon_data.is_spa,
|
|
2347
|
+
auth_required: engineState.auth_required,
|
|
2348
|
+
},
|
|
2349
|
+
|
|
2350
|
+
// 发现的漏洞
|
|
2351
|
+
findings: engineState.findings,
|
|
2352
|
+
findings_count: engineState.findings.length,
|
|
2353
|
+
|
|
2354
|
+
// 已完成的任务
|
|
2355
|
+
completed_tasks: engineState.completed_tasks,
|
|
2356
|
+
|
|
2357
|
+
// 推荐的下一步行动 (自主 Hook)
|
|
2358
|
+
recommended_next_actions: nextActions.map((action, i) => ({
|
|
2359
|
+
step: i + 1,
|
|
2360
|
+
action: action.name,
|
|
2361
|
+
tool: action.tool,
|
|
2362
|
+
params: action.params,
|
|
2363
|
+
reason: action.reason,
|
|
2364
|
+
priority: action.priority,
|
|
2365
|
+
})),
|
|
2366
|
+
|
|
2367
|
+
// LLM 应该执行的指令
|
|
2368
|
+
instruction_for_llm: nextActions.length > 0
|
|
2369
|
+
? `根据侦察结果,建议按以下顺序执行:\n${nextActions.map((a, i) => `${i + 1}. [${a.priority}] ${a.name} - 使用 ${a.tool}(${JSON.stringify(a.params)})\n 原因: ${a.reason}`).join("\n")}`
|
|
2370
|
+
: "所有自动化测试已完成,建议手动深入测试。",
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
return JSON.stringify(result, null, 2);
|
|
2374
|
+
},
|
|
2375
|
+
}),
|
|
2376
|
+
|
|
1666
2377
|
// ========================================
|
|
1667
2378
|
// OpenCode 原生集成工具
|
|
1668
2379
|
// ========================================
|
|
@@ -1892,6 +2603,37 @@ ${LEVEL_PROMPTS[level]}
|
|
|
1892
2603
|
return output;
|
|
1893
2604
|
},
|
|
1894
2605
|
|
|
2606
|
+
// Todo 工具描述覆盖 - 参考 oh-my-opencode 的 todo-description-override hook
|
|
2607
|
+
"tool.definition": async (input, output) => {
|
|
2608
|
+
if (input.toolID === "todowrite") {
|
|
2609
|
+
output.description = `使用此工具创建和管理渗透测试任务清单,实时显示在 OpenCode 右侧面板。
|
|
2610
|
+
|
|
2611
|
+
## Todo 格式 (强制)
|
|
2612
|
+
|
|
2613
|
+
每个 todo 标题必须包含四个要素: WHERE, WHY, HOW, 预期结果。
|
|
2614
|
+
|
|
2615
|
+
格式: "[目标/端点] [操作] - 预期 [结果]"
|
|
2616
|
+
|
|
2617
|
+
好的示例:
|
|
2618
|
+
- "https://target/api/login: 使用 sqli_test 测试 SQL 注入 - 预期发现/排除注入漏洞"
|
|
2619
|
+
- "JS 文件分析: 使用 js_parse 提取所有 API 端点 - 预期获得完整端点列表"
|
|
2620
|
+
- "认证绕过测试: 使用 auth_bypass_test 测试 /api/user - 预期确认/排除绕过可能"
|
|
2621
|
+
|
|
2622
|
+
坏的示例:
|
|
2623
|
+
- "测试 SQL 注入" (哪个端点? 什么工具? 什么结果?)
|
|
2624
|
+
- "安全测试" (太笼统)
|
|
2625
|
+
|
|
2626
|
+
## 粒度规则
|
|
2627
|
+
|
|
2628
|
+
每个 todo 必须是单个原子操作,可以在 1-3 次工具调用内完成。如果需要更多步骤,拆分它。
|
|
2629
|
+
|
|
2630
|
+
## 任务管理
|
|
2631
|
+
- 一次只标记一个 in_progress。完成后再开始下一个。
|
|
2632
|
+
- 每完成一项立即标记 completed。
|
|
2633
|
+
- 单步简单任务跳过此工具。`;
|
|
2634
|
+
}
|
|
2635
|
+
},
|
|
2636
|
+
|
|
1895
2637
|
// 会话事件处理
|
|
1896
2638
|
event: async (input) => {
|
|
1897
2639
|
const { event } = input
|
|
@@ -1908,6 +2650,77 @@ ${LEVEL_PROMPTS[level]}
|
|
|
1908
2650
|
}
|
|
1909
2651
|
}
|
|
1910
2652
|
|
|
2653
|
+
// 会话空闲 - 触发 Todo 持续注入 (参考 oh-my-opencode 的 todo-continuation-enforcer)
|
|
2654
|
+
if (event.type === "session.idle") {
|
|
2655
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
2656
|
+
const sessionID = (props?.info as { id?: string })?.id;
|
|
2657
|
+
|
|
2658
|
+
if (sessionID) {
|
|
2659
|
+
try {
|
|
2660
|
+
// 获取当前 todo 列表
|
|
2661
|
+
const response = await ctx.client.session.todo({ path: { id: sessionID } });
|
|
2662
|
+
const todos = (response as any)?.data || response || [];
|
|
2663
|
+
const incompleteCount = getIncompleteCount(Array.isArray(todos) ? todos : []);
|
|
2664
|
+
|
|
2665
|
+
if (incompleteCount === 0) {
|
|
2666
|
+
return; // 没有未完成的 todo,不需要持续注入
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// 检查冷却时间和停滞状态
|
|
2670
|
+
const state = todoContinuationState.get(sessionID);
|
|
2671
|
+
const now = Date.now();
|
|
2672
|
+
|
|
2673
|
+
if (state) {
|
|
2674
|
+
// 冷却期内跳过
|
|
2675
|
+
if (now - state.lastInjectedAt < CONTINUATION_COOLDOWN_MS) {
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
// 停滞检测 (连续未进展)
|
|
2679
|
+
if (state.stagnationCount >= MAX_STAGNATION_COUNT) {
|
|
2680
|
+
console.log(`[api-security-testing] Todo continuation stagnated for session ${sessionID}`);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
// 更新停滞计数
|
|
2684
|
+
if (state.incompleteCount === incompleteCount) {
|
|
2685
|
+
state.stagnationCount++;
|
|
2686
|
+
} else {
|
|
2687
|
+
state.stagnationCount = 0; // 有进展,重置
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// 构建持续注入 prompt
|
|
2692
|
+
const incompleteTodos = Array.isArray(todos)
|
|
2693
|
+
? todos.filter((t: any) => t.status !== "completed" && t.status !== "cancelled")
|
|
2694
|
+
: [];
|
|
2695
|
+
const todoList = incompleteTodos
|
|
2696
|
+
.map((t: any) => `- [${t.status || "pending"}] ${t.content || t.text || "Unknown task"}`)
|
|
2697
|
+
.join("\n");
|
|
2698
|
+
|
|
2699
|
+
const continuationPrompt = `${CONTINUATION_PROMPT}\n\n[状态: ${Array.isArray(todos) ? todos.length - incompleteCount : "?"}/${Array.isArray(todos) ? todos.length : "?"} 已完成, ${incompleteCount} 剩余]\n\n剩余任务:\n${todoList}`;
|
|
2700
|
+
|
|
2701
|
+
// 注入持续 prompt (参考 oh-my-opencode 的 injectContinuation)
|
|
2702
|
+
await ctx.client.session.promptAsync({
|
|
2703
|
+
path: { id: sessionID },
|
|
2704
|
+
body: {
|
|
2705
|
+
parts: [createP9InternalTextPart(continuationPrompt)],
|
|
2706
|
+
},
|
|
2707
|
+
query: { directory: ctx.directory },
|
|
2708
|
+
} as any);
|
|
2709
|
+
|
|
2710
|
+
// 更新状态
|
|
2711
|
+
todoContinuationState.set(sessionID, {
|
|
2712
|
+
lastInjectedAt: now,
|
|
2713
|
+
incompleteCount,
|
|
2714
|
+
stagnationCount: state?.stagnationCount || 0,
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
console.log(`[api-security-testing] Injected todo continuation for session ${sessionID}, ${incompleteCount} tasks remaining`);
|
|
2718
|
+
} catch (err) {
|
|
2719
|
+
console.error(`[api-security-testing] Todo continuation injection failed:`, err);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
1911
2724
|
// 会话删除或压缩 - 清理状态
|
|
1912
2725
|
if (event.type === "session.deleted" || event.type === "session.compacted") {
|
|
1913
2726
|
const props = event.properties as Record<string, unknown> | undefined;
|
|
@@ -1923,6 +2736,7 @@ ${LEVEL_PROMPTS[level]}
|
|
|
1923
2736
|
clearSessionState(sessionID);
|
|
1924
2737
|
resetFailureCount(sessionID);
|
|
1925
2738
|
resetModelFailures(sessionID);
|
|
2739
|
+
todoContinuationState.delete(sessionID);
|
|
1926
2740
|
}
|
|
1927
2741
|
}
|
|
1928
2742
|
},
|
|
@@ -1993,6 +2807,13 @@ ${LEVEL_PROMPTS[level]}
|
|
|
1993
2807
|
|
|
1994
2808
|
console.log(`[api-security-testing] Injected context via synthetic part, session=${sessionID}, length=${pending.merged.length}`);
|
|
1995
2809
|
},
|
|
2810
|
+
|
|
2811
|
+
// 系统 Prompt 注入 - Todo 管理指令 (参考 oh-my-opencode Sisyphus agent)
|
|
2812
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
2813
|
+
output.system.push(PENTEST_TODO_SYSTEM_PROMPT);
|
|
2814
|
+
output.system.push(TODO_HOOK_NOTE);
|
|
2815
|
+
console.log(`[api-security-testing] Injected pentest todo management into system prompt`);
|
|
2816
|
+
},
|
|
1996
2817
|
};
|
|
1997
2818
|
};
|
|
1998
2819
|
|