opencode-api-security-testing 4.0.1 → 5.0.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 (29) hide show
  1. package/package.json +48 -47
  2. package/postinstall.mjs +69 -40
  3. package/references/references/README.md +72 -0
  4. package/references/references/asset-discovery.md +119 -0
  5. package/references/references/fuzzing-patterns.md +129 -0
  6. package/references/references/graphql-guidance.md +108 -0
  7. package/references/references/intake.md +84 -0
  8. package/references/references/pua-agent.md +192 -0
  9. package/references/references/report-template.md +156 -0
  10. package/references/references/rest-guidance.md +76 -0
  11. package/references/references/severity-model.md +76 -0
  12. package/references/references/test-matrix.md +86 -0
  13. package/references/references/validation.md +78 -0
  14. package/references/references/vulnerabilities/01-sqli-tests.md +1128 -0
  15. package/references/references/vulnerabilities/02-user-enum-tests.md +423 -0
  16. package/references/references/vulnerabilities/03-jwt-tests.md +499 -0
  17. package/references/references/vulnerabilities/04-idor-tests.md +362 -0
  18. package/references/references/vulnerabilities/05-sensitive-data-tests.md +466 -0
  19. package/references/references/vulnerabilities/06-biz-logic-tests.md +501 -0
  20. package/references/references/vulnerabilities/07-security-config-tests.md +511 -0
  21. package/references/references/vulnerabilities/08-brute-force-tests.md +457 -0
  22. package/references/references/vulnerabilities/09-vulnerability-chains.md +465 -0
  23. package/references/references/vulnerabilities/10-auth-tests.md +537 -0
  24. package/references/references/vulnerabilities/11-graphql-tests.md +355 -0
  25. package/references/references/vulnerabilities/12-ssrf-tests.md +396 -0
  26. package/references/references/vulnerabilities/README.md +148 -0
  27. package/references/references/workflows.md +192 -0
  28. package/src/index.ts +153 -25
  29. package/src/src/index.ts +535 -0
@@ -0,0 +1,192 @@
1
+ # 完整扫描流程
2
+
3
+ ## 场景化扫描流程
4
+
5
+ **【强制】所有扫描深度都必须使用Playwright进行JS动态采集,禁止降级**
6
+
7
+ ```
8
+ 【quick_scan】5分钟快速扫描
9
+ ├─ 目标:快速发现明显漏洞
10
+ └─ 流程:
11
+ ├─ HTTP/HTTPS探测 + 技术栈识别
12
+ ├─ Playwright全流量采集(必须)
13
+ ├─ 关键路径探测
14
+ └─ 快速漏洞测试
15
+
16
+ 【normal_scan】20分钟标准扫描
17
+ ├─ 目标:全面发现漏洞
18
+ └─ 流程:
19
+ ├─ 所有基础探测
20
+ ├─ Playwright全流量采集(必须)
21
+ ├─ JS深度分析
22
+ ├─ 关键端点测试
23
+ └─ 漏洞验证 + 报告输出
24
+
25
+ 【deep_scan】1小时深度扫描
26
+ ├─ 目标:完整渗透测试
27
+ └─ 流程:
28
+ ├─ Playwright全流量采集(必须)
29
+ ├─ JS深度分析(AST+正则+路径推断)
30
+ ├─ 全量API端点测试
31
+ ├─ 认证绕过测试矩阵
32
+ ├─ SQL注入深入利用(判断类型+可利用性)
33
+ ├─ 漏洞链构造
34
+ └─ 完整报告输出
35
+ ```
36
+
37
+ ## SPA应用完整采集流程
38
+
39
+ ### 流程图(循环迭代模型)
40
+
41
+ ```mermaid
42
+ flowchart TD
43
+ A["开始: 确定测试目标"] --> B["基础探测"]
44
+ B --> C{"目标可访问?"}
45
+ C -->|否| D["记录并尝试其他路径"]
46
+ D --> E{"还有未探测路径?"}
47
+ E -->|是| B
48
+ E -->|否| Z["报告: 目标不可达"]
49
+ C -->|是| F["识别技术栈"]
50
+ F --> G{"发现新资产?"}
51
+ G -->|是| H["创建新测试目标"]
52
+ H --> B
53
+ G -->|否| I["SPA应用?"]
54
+ I -->|否| J["直接探测API"]
55
+ J --> K["API测试"]
56
+ K --> L{"发现新接口?"}
57
+ L -->|是| M["深入分析新接口"]
58
+ M --> K
59
+ L -->|否| N{"发现新资产?"}
60
+ N -->|是| H
61
+ N -->|否| O{"发现漏洞?"}
62
+ I -->|是| P["全流量采集"]
63
+ P --> Q["JS采集"]
64
+ Q --> R{"发现新URL?"}
65
+ R -->|是| H
66
+ R -->|否| S["拦截XHR/Fetch"]
67
+ S --> T{"发现新域名?"}
68
+ T -->|是| H
69
+ T -->|否| U["深度分析JS"]
70
+ U --> V{"发现新API路径?"}
71
+ V -->|是| M
72
+ V -->|否| W{"发现新资产?"}
73
+ W -->|是| H
74
+ W -->|否| K
75
+ O -->|是| X["漏洞验证"]
76
+ X --> Y["输出漏洞报告"]
77
+ Y --> K
78
+ O -->|否| K
79
+ ```
80
+
81
+ **核心原则**:渗透测试是**循环迭代**过程,不是线性流程!
82
+
83
+ ## 阶段1:基础探测
84
+
85
+ ```
86
+ 1. HTTP探测目标可访问性
87
+ curl -I http://target.com
88
+
89
+ 2. 技术栈识别
90
+ - 检查响应头Server字段
91
+ - 检查HTML中是否包含Vue/React/Angular关键词
92
+ - 检查是否包含webpack chunk引用
93
+
94
+ 3. 判断是否是SPA应用
95
+ - /api/* 返回HTML → SPA
96
+ - HTML包含JS chunk路径 → Vue/React应用
97
+
98
+ 4. 全流量监听
99
+ - 捕获所有出站请求(不只是JS)
100
+ - 记录所有第三方域名(CDN、分析服务等)
101
+ - 记录所有静态资源加载
102
+ ```
103
+
104
+ ## 阶段2:JS采集【强制·禁止降级】
105
+
106
+ **【禁止降级采集阶段】必须使用Playwright,不允许降级到Selenium/requests**
107
+
108
+ **【允许】分析阶段可使用curl进行补充**
109
+
110
+ ```
111
+ 【核心目标】捕获所有流量,不只分析JS文件!
112
+
113
+ 1. Playwright全流量采集(必须)
114
+ - 使用ignore_https_errors=True处理证书问题
115
+ - 拦截所有请求/响应
116
+ - 记录流量类型(xhr/fetch/document/script/websocket)
117
+
118
+ 2. 模拟用户操作(必须)
119
+ - 点击页面触发加载
120
+ - 滚动触发懒加载
121
+ - 填写登录表单
122
+ - 导航到其他页面
123
+
124
+ 3. 多目标队列管理
125
+ TEST_QUEUE = [] # 待测试目标
126
+ TESTED = set() # 已测试目标
127
+ ```
128
+
129
+ ## 阶段3:JS深度分析
130
+
131
+ ```
132
+ 1. 提取baseURL配置(最优先!)
133
+ - r'baseURL\s*[:=]\s*["\']([^"\']+)["\']'
134
+ - r'VUE_APP_\w+["\s]*[:=]["\s]*["\']([^"\']*)["\']'
135
+
136
+ 2. API端点提取
137
+ - r'["\'](/(?:user|auth|admin|login|logout|api|v\d)[^"\']*)["\']'
138
+ - r'axios\.[a-z]+\(["\']([^"\']+)["\']'
139
+ - r'fetch\(["\']([^"\']+)["\']'
140
+
141
+ 3. 敏感信息提取
142
+ - IP地址、内网地址
143
+ - Token、API Key
144
+ - 外部域名
145
+ ```
146
+
147
+ ## 阶段4:API测试
148
+
149
+ ```
150
+ base_path获取优先级:
151
+ 1. 配置文件: /{app}/_app.config.js 中的 VITE_GLOB_API_URL
152
+ 2. baseURL配置
153
+ 3. Swagger/OpenAPI文档
154
+ 4. nginx反向代理推测
155
+ 5. JS路径反推
156
+ 6. 多API共同前缀
157
+ 7. 字典fallback
158
+
159
+ 【重要】多base_path场景:
160
+ - 一个站点可能存在多个base_path
161
+ - 需要分别测试每个base_path下的端点
162
+ ```
163
+
164
+ ## 阶段5:漏洞验证
165
+
166
+ ```
167
+ 【多维度误报判断框架】
168
+
169
+ 判断逻辑优先级:
170
+ 1. 先看响应内容是否符合漏洞特征
171
+ 2. 再看是否为预期响应
172
+ 3. 最后验证利用可能性
173
+
174
+ 必须先分析"正常响应应该是什么样的"
175
+ ```
176
+
177
+ ## 阶段6:报告输出
178
+
179
+ ```
180
+ 1. 收集测试结果
181
+ - 汇总所有发现的漏洞(高/中/低危)
182
+ - 整理API端点清单
183
+ - 记录安全优点
184
+
185
+ 2. 利用链分析
186
+ - 分析独立漏洞的关联性
187
+ - 构建完整攻击链
188
+
189
+ 3. 修复建议
190
+ - 按优先级排序
191
+ - 提供具体修复方案
192
+ ```
package/src/index.ts CHANGED
@@ -6,18 +6,127 @@ import { existsSync, readFileSync } from "fs";
6
6
  const SKILL_DIR = "skills/api-security-testing";
7
7
  const CORE_DIR = `${SKILL_DIR}/core`;
8
8
  const AGENTS_DIR = ".config/opencode/agents";
9
+ const CONFIG_FILE = "api-security-testing.config.json";
10
+
11
+ // 默认配置
12
+ const DEFAULT_CONFIG = {
13
+ agents: {
14
+ "api-cyber-supervisor": { model: "anthropic/claude-sonnet-4-20250514", temperature: 0.3 },
15
+ "api-probing-miner": { model: "anthropic/claude-haiku-4-20250514", temperature: 0.5 },
16
+ "api-resource-specialist": { model: "anthropic/claude-haiku-4-20250514", temperature: 0.4 },
17
+ "api-vuln-verifier": { model: "anthropic/claude-haiku-4-20250514", temperature: 0.2 }
18
+ },
19
+ model_fallback: {
20
+ supervisor: [
21
+ "anthropic/claude-sonnet-4-20250514",
22
+ "openai/gpt-4o",
23
+ "google/gemini-2.0-flash",
24
+ "anthropic/claude-haiku-4-20250514"
25
+ ],
26
+ subagent: [
27
+ "anthropic/claude-haiku-4-20250514",
28
+ "openai/gpt-4o-mini",
29
+ "google/gemini-2.0-flash-lite"
30
+ ]
31
+ },
32
+ cyber_supervisor: {
33
+ enabled: true,
34
+ auto_trigger: true,
35
+ max_retries: 5,
36
+ give_up_patterns: [
37
+ "无法解决", "不能", "需要手动", "环境问题",
38
+ "超出范围", "建议用户", "I cannot", "I'm unable",
39
+ "out of scope", "manual"
40
+ ]
41
+ },
42
+ collection_mode: "auto" // auto | static | dynamic
43
+ };
9
44
 
10
45
  // 赛博监工配置
11
- const CYBER_SUPERVISOR = {
12
- enabled: true,
13
- auto_trigger: true,
14
- max_retries: 5,
15
- give_up_patterns: [
16
- "无法解决", "不能", "需要手动", "环境问题",
17
- "超出范围", "建议用户", "I cannot", "I'm unable",
18
- "out of scope", "manual"
19
- ]
20
- };
46
+ const CYBER_SUPERVISOR = DEFAULT_CONFIG.cyber_supervisor;
47
+
48
+ // 模型回退状态追踪
49
+ const modelFailureCounts = new Map<string, Map<string, number>>();
50
+ const sessionFailures = new Map<string, number>();
51
+
52
+ function getConfigPath(ctx: { directory: string }): string {
53
+ return join(ctx.directory, SKILL_DIR, "assets", CONFIG_FILE);
54
+ }
55
+
56
+ function loadConfig(ctx: { directory: string }): typeof DEFAULT_CONFIG {
57
+ const configPath = getConfigPath(ctx);
58
+ if (existsSync(configPath)) {
59
+ try {
60
+ const content = readFileSync(configPath, "utf-8");
61
+ const loaded = JSON.parse(content);
62
+ return { ...DEFAULT_CONFIG, ...loaded };
63
+ } catch (e) {
64
+ console.log(`[api-security-testing] Failed to load config: ${e}`);
65
+ }
66
+ }
67
+ return DEFAULT_CONFIG;
68
+ }
69
+
70
+ function getModelFailureCount(sessionID: string, modelID: string): number {
71
+ const sessionMap = modelFailureCounts.get(sessionID);
72
+ if (!sessionMap) return 0;
73
+ return sessionMap.get(modelID) || 0;
74
+ }
75
+
76
+ function incrementModelFailure(sessionID: string, modelID: string): void {
77
+ if (!modelFailureCounts.has(sessionID)) {
78
+ modelFailureCounts.set(sessionID, new Map());
79
+ }
80
+ const sessionMap = modelFailureCounts.get(sessionID)!;
81
+ sessionMap.set(modelID, (sessionMap.get(modelID) || 0) + 1);
82
+ }
83
+
84
+ function resetModelFailures(sessionID: string): void {
85
+ modelFailureCounts.delete(sessionID);
86
+ }
87
+
88
+ function getNextFallbackModel(
89
+ config: typeof DEFAULT_CONFIG,
90
+ sessionID: string,
91
+ currentModel: string,
92
+ isSupervisor: boolean
93
+ ): string | null {
94
+ const chain = isSupervisor
95
+ ? config.model_fallback.supervisor
96
+ : config.model_fallback.subagent;
97
+
98
+ const currentIndex = chain.indexOf(currentModel);
99
+ if (currentIndex === -1 || currentIndex >= chain.length - 1) {
100
+ return null; // No fallback available
101
+ }
102
+
103
+ // 检查下一个模型是否也已失败过多
104
+ const nextModel = chain[currentIndex + 1];
105
+ const failures = getModelFailureCount(sessionID, nextModel);
106
+ if (failures >= 3) {
107
+ // 递归查找下一个可用模型
108
+ return getNextFallbackModel(config, sessionID, nextModel, isSupervisor);
109
+ }
110
+
111
+ return nextModel;
112
+ }
113
+
114
+ function detectModelError(output: string): { isModelError: boolean; modelID?: string } {
115
+ const errorPatterns = [
116
+ /model (.+?) (is overloaded|is not available|rate limit|timeout|failed)/i,
117
+ /(.+?) (rate limit exceeded|context length exceeded)/i,
118
+ /API error.*model[:\s]*(.+)/i,
119
+ ];
120
+
121
+ for (const pattern of errorPatterns) {
122
+ const match = output.match(pattern);
123
+ if (match) {
124
+ return { isModelError: true, modelID: match[1]?.trim() };
125
+ }
126
+ }
127
+
128
+ return { isModelError: false };
129
+ }
21
130
 
22
131
  // 压力升级提示词
23
132
  const LEVEL_PROMPTS = [
@@ -79,9 +188,6 @@ async function execShell(ctx: unknown, cmd: string): Promise<string> {
79
188
  return result.toString();
80
189
  }
81
190
 
82
- // 赛博监工状态追踪
83
- const sessionFailures = new Map<string, number>();
84
-
85
191
  function getFailureCount(sessionID: string): number {
86
192
  return sessionFailures.get(sessionID) || 0;
87
193
  }
@@ -101,7 +207,8 @@ function detectGiveUpPattern(text: string): boolean {
101
207
  }
102
208
 
103
209
  const ApiSecurityTestingPlugin: Plugin = async (ctx) => {
104
- console.log("[api-security-testing] Plugin loaded v4.0.0");
210
+ const config = loadConfig(ctx);
211
+ console.log(`[api-security-testing] Plugin loaded v4.0.2 - collection_mode: ${config.collection_mode}`);
105
212
 
106
213
  return {
107
214
  tool: {
@@ -169,22 +276,23 @@ print(result)
169
276
  }),
170
277
 
171
278
  browser_collect: tool({
172
- description: "浏览器采集动态内容。参数: url(目标URL)",
279
+ description: "浏览器采集动态内容。参数: url(目标URL), mode(auto/static/dynamic)",
173
280
  args: {
174
281
  url: tool.schema.string(),
282
+ mode: tool.schema.enum(["auto", "static", "dynamic"]).optional(),
175
283
  },
176
284
  async execute(args, ctx) {
177
285
  const deps = checkDeps(ctx);
178
286
  const corePath = getCorePath(ctx);
287
+ const collectionMode = args.mode || config.collection_mode;
179
288
  const cmd = `${deps}python3 -c "
180
289
  import sys
181
290
  sys.path.insert(0, '${corePath}')
182
- from collectors.browser_collect import BrowserCollector
183
- collector = BrowserCollector(headless=True)
184
- endpoints = collector.collect('${args.url}')
185
- print(f'发现 {len(endpoints)} 个端点:')
186
- for ep in endpoints:
187
- print(ep)
291
+ from collectors.browser_collector import BrowserCollectorFacade
292
+ facade = BrowserCollectorFacade(headless=True)
293
+ result = facade.collect_all('${args.url}', {'mode': '${collectionMode}'})
294
+ import json
295
+ print(json.dumps(result, indent=2))
188
296
  "`;
189
297
  return await execShell(ctx, cmd);
190
298
  },
@@ -328,14 +436,14 @@ print(result)
328
436
  }
329
437
 
330
438
  // 赛博监工压力注入
331
- if (CYBER_SUPERVISOR.enabled && CYBER_SUPERVISOR.auto_trigger) {
439
+ if (config.cyber_supervisor.enabled && config.cyber_supervisor.auto_trigger) {
332
440
  const failures = getFailureCount(sessionID);
333
- if (failures > 0 && failures <= CYBER_SUPERVISOR.max_retries) {
441
+ if (failures > 0 && failures <= config.cyber_supervisor.max_retries) {
334
442
  const level = Math.min(failures - 1, 4);
335
443
  const supervisorPrompt = `
336
444
 
337
445
  [赛博监工 P9 - 压力等级 L${level}]
338
- 当前失败次数: ${failures}/${CYBER_SUPERVISOR.max_retries}
446
+ 当前失败次数: ${failures}/${config.cyber_supervisor.max_retries}
339
447
  ${LEVEL_PROMPTS[level]}
340
448
 
341
449
  记住三条红线:
@@ -355,6 +463,26 @@ ${LEVEL_PROMPTS[level]}
355
463
  // 赛博监工 Hook - tool.execute.after
356
464
  "tool.execute.after": async (input, output) => {
357
465
  const sessionID = input.sessionID;
466
+ const outputText = output.output || "";
467
+
468
+ // 检测模型错误并触发回退
469
+ const modelError = detectModelError(outputText);
470
+ if (modelError.isModelError && modelError.modelID) {
471
+ incrementModelFailure(sessionID, modelError.modelID);
472
+
473
+ // 获取下一个可用模型
474
+ const isSupervisor = input.toolName?.includes("supervisor") ||
475
+ input.toolName?.includes("scan");
476
+ const nextModel = getNextFallbackModel(config, sessionID, modelError.modelID, isSupervisor);
477
+
478
+ if (nextModel) {
479
+ return {
480
+ ...output,
481
+ output: outputText + `\n\n[模型回退] ${modelError.modelID} 失败,已切换到 ${nextModel}`,
482
+ _fallbackModel: nextModel
483
+ };
484
+ }
485
+ }
358
486
 
359
487
  // 检测工具失败
360
488
  if (output.error) {
@@ -362,7 +490,6 @@ ${LEVEL_PROMPTS[level]}
362
490
  }
363
491
 
364
492
  // 检测放弃模式
365
- const outputText = output.output || "";
366
493
  if (detectGiveUpPattern(outputText)) {
367
494
  incrementFailureCount(sessionID);
368
495
  const failures = getFailureCount(sessionID);
@@ -398,6 +525,7 @@ ${LEVEL_PROMPTS[level]}
398
525
 
399
526
  if (sessionID) {
400
527
  resetFailureCount(sessionID);
528
+ resetModelFailures(sessionID);
401
529
  }
402
530
  }
403
531
  },