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.
@@ -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
- 1. browser_collect 采集动态端点
48
- 2. js_parse 分析 JS 文件
49
- 3. url_discover 发现隐藏端点
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-api-security-testing",
3
- "version": "5.4.10",
3
+ "version": "5.5.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
@@ -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 !== 00 return 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