opencode-api-security-testing 5.0.0 → 5.2.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 (34) hide show
  1. package/SKILL.md +6 -0
  2. package/agents/api-cyber-supervisor.md +5 -3
  3. package/package.json +1 -1
  4. package/postinstall.mjs +181 -6
  5. package/src/index.ts +165 -0
  6. package/src/tools/endpoint-discover.ts +325 -0
  7. package/src/tools/report-generator.ts +355 -0
  8. package/src/utils/env-checker.ts +264 -0
  9. package/references/references/README.md +0 -72
  10. package/references/references/asset-discovery.md +0 -119
  11. package/references/references/fuzzing-patterns.md +0 -129
  12. package/references/references/graphql-guidance.md +0 -108
  13. package/references/references/intake.md +0 -84
  14. package/references/references/pua-agent.md +0 -192
  15. package/references/references/report-template.md +0 -156
  16. package/references/references/rest-guidance.md +0 -76
  17. package/references/references/severity-model.md +0 -76
  18. package/references/references/test-matrix.md +0 -86
  19. package/references/references/validation.md +0 -78
  20. package/references/references/vulnerabilities/01-sqli-tests.md +0 -1128
  21. package/references/references/vulnerabilities/02-user-enum-tests.md +0 -423
  22. package/references/references/vulnerabilities/03-jwt-tests.md +0 -499
  23. package/references/references/vulnerabilities/04-idor-tests.md +0 -362
  24. package/references/references/vulnerabilities/05-sensitive-data-tests.md +0 -466
  25. package/references/references/vulnerabilities/06-biz-logic-tests.md +0 -501
  26. package/references/references/vulnerabilities/07-security-config-tests.md +0 -511
  27. package/references/references/vulnerabilities/08-brute-force-tests.md +0 -457
  28. package/references/references/vulnerabilities/09-vulnerability-chains.md +0 -465
  29. package/references/references/vulnerabilities/10-auth-tests.md +0 -537
  30. package/references/references/vulnerabilities/11-graphql-tests.md +0 -355
  31. package/references/references/vulnerabilities/12-ssrf-tests.md +0 -396
  32. package/references/references/vulnerabilities/README.md +0 -148
  33. package/references/references/workflows.md +0 -192
  34. package/src/src/index.ts +0 -535
package/SKILL.md CHANGED
@@ -69,6 +69,12 @@ tools:
69
69
  - name: report_generator
70
70
  description: "Compile evidence-based security report."
71
71
  usage: "During 报告 to generate deliverables."
72
+ - name: endpoint_discover
73
+ description: "Auto-discover API endpoints from JS, HTML, sitemap, robots.txt, and common paths."
74
+ usage: "During 侦察 to automatically map the attack surface without manual input."
75
+ - name: env_checker
76
+ description: "Auto-detect and install missing dependencies (Python, pip, Playwright, etc.)."
77
+ usage: "Runs automatically during postinstall; can be triggered manually to fix environment issues."
72
78
  notes:
73
79
  - "All tools must be used within their defined phases; avoid cross-phase misuse."
74
80
  - "Preserve evidence with timestamps; ensure traceability for audits."
@@ -31,6 +31,7 @@ color: "#FF5733"
31
31
 
32
32
  | 工具 | 用途 | 场景 |
33
33
  |------|------|------|
34
+ | endpoint_discover | 端点自动发现 | 从 JS/HTML/Sitemap 提取 |
34
35
  | api_security_scan | 完整扫描 | 全面测试 |
35
36
  | api_fuzz_test | 模糊测试 | 发现未知端点 |
36
37
  | browser_collect | 浏览器采集 | SPA 应用 |
@@ -40,13 +41,14 @@ color: "#FF5733"
40
41
  | cloud_storage_test | 云存储测试 | OSS/S3 |
41
42
  | idor_test | IDOR 测试 | 越权漏洞 |
42
43
  | sqli_test | SQLi 测试 | 注入漏洞 |
44
+ | report_generate | 报告生成 | Markdown/HTML/JSON 格式 |
43
45
 
44
46
  ## 测试流程
45
47
 
46
48
  ### Phase 1: 侦察
47
- 1. browser_collect 采集动态端点
48
- 2. js_parse 分析 JS 文件
49
- 3. url_discover 发现隐藏端点
49
+ 1. endpoint_discover 自动发现所有 API 端点
50
+ 2. browser_collect 采集动态端点
51
+ 3. js_parse 分析 JS 文件
50
52
 
51
53
  ### Phase 2: 分析
52
54
  1. 识别技术栈
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-api-security-testing",
3
- "version": "5.0.0",
3
+ "version": "5.2.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/postinstall.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  /**
2
4
  * postinstall.mjs - API Security Testing Plugin
3
5
  *
@@ -5,11 +7,14 @@
5
7
  * 1. agents to ~/.claude/agents/ (oh-my-opencode discovery path)
6
8
  * 2. agents to ~/.config/opencode/agents/ (OpenCode native discovery path)
7
9
  * 3. SKILL.md and references to ~/.config/opencode/skills/api-security-testing/
10
+ * 4. Auto-detects and installs Python dependencies (requests, beautifulsoup4, playwright)
8
11
  */
9
12
 
10
- import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
13
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
11
14
  import { join } from "node:path";
12
15
  import { fileURLToPath } from "node:url";
16
+ import { execSync } from "node:child_process";
17
+ import { platform } from "node:os";
13
18
 
14
19
  const __filename = fileURLToPath(import.meta.url);
15
20
  const __dirname = join(__filename, "..");
@@ -47,6 +52,167 @@ function copyDirRecursive(src, dest) {
47
52
  return count;
48
53
  }
49
54
 
55
+ /**
56
+ * Run shell command safely
57
+ */
58
+ function runCommand(cmd, timeout = 30000) {
59
+ try {
60
+ const output = execSync(cmd, {
61
+ encoding: "utf-8",
62
+ timeout,
63
+ stdio: ["pipe", "pipe", "pipe"]
64
+ });
65
+ return { success: true, output: output.trim(), error: "" };
66
+ } catch (error) {
67
+ return {
68
+ success: false,
69
+ output: error.stdout || "",
70
+ error: error.stderr || error.message || "Unknown error"
71
+ };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check Python availability
77
+ */
78
+ function checkPython() {
79
+ for (const cmd of ["python3 --version", "python --version"]) {
80
+ const result = runCommand(cmd, 5000);
81
+ if (result.success) {
82
+ const versionMatch = result.output.match(/Python\s+(\d+\.\d+\.\d+)/i) ||
83
+ result.error.match(/Python\s+(\d+\.\d+\.\d+)/i);
84
+ return { available: true, version: versionMatch ? versionMatch[1] : "unknown", cmd: cmd.split(" ")[0] };
85
+ }
86
+ }
87
+ return { available: false, version: null, cmd: null };
88
+ }
89
+
90
+ /**
91
+ * Check if pip is available
92
+ */
93
+ function checkPip(pythonCmd) {
94
+ const cmds = [
95
+ `${pythonCmd} -m pip --version`,
96
+ "pip3 --version",
97
+ "pip --version"
98
+ ];
99
+ for (const cmd of cmds) {
100
+ const result = runCommand(cmd, 5000);
101
+ if (result.success) return cmd.includes("-m pip") ? `${pythonCmd} -m pip` : cmd.split(" ")[0];
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /**
107
+ * Check if a Python package is installed
108
+ */
109
+ function checkPythonPackage(pythonCmd, packageName) {
110
+ const result = runCommand(`${pythonCmd} -c "import ${packageName}"`, 5000);
111
+ return result.success;
112
+ }
113
+
114
+ /**
115
+ * Install a Python package
116
+ */
117
+ function installPythonPackage(pipCmd, packageName) {
118
+ const cmds = [
119
+ `${pipCmd} install -q ${packageName}`,
120
+ `${pipCmd} install --user -q ${packageName}`
121
+ ];
122
+ for (const cmd of cmds) {
123
+ const result = runCommand(cmd, 120000);
124
+ if (result.success) return { success: true, error: "" };
125
+ }
126
+ return { success: false, error: `Failed to install ${packageName}` };
127
+ }
128
+
129
+ /**
130
+ * Check if Playwright browsers are installed
131
+ */
132
+ function checkPlaywright(pythonCmd) {
133
+ const hasPackage = checkPythonPackage(pythonCmd, "playwright");
134
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "/root";
135
+ const pwCachePath = join(homeDir, ".cache", "ms-playwright");
136
+ const browsersInstalled = existsSync(pwCachePath);
137
+ return { installed: hasPackage, browsersInstalled };
138
+ }
139
+
140
+ /**
141
+ * Install Playwright
142
+ */
143
+ function installPlaywright(pythonCmd) {
144
+ console.log(" Installing Playwright Python package...");
145
+ const pipCmd = checkPip(pythonCmd);
146
+ if (!pipCmd) return { success: false, error: "pip not found" };
147
+
148
+ const pkgResult = installPythonPackage(pipCmd, "playwright");
149
+ if (!pkgResult.success) return pkgResult;
150
+
151
+ console.log(" Installing Playwright browsers (chromium)...");
152
+ const browserResult = runCommand(`${pythonCmd} -m playwright install chromium`, 300000);
153
+ if (browserResult.success) return { success: true, error: "" };
154
+
155
+ return { success: false, error: browserResult.error };
156
+ }
157
+
158
+ /**
159
+ * Environment detection and auto-installation
160
+ */
161
+ function checkAndFixEnvironment() {
162
+ console.log("\n[env-check] Starting environment detection...");
163
+
164
+ const pythonCheck = checkPython();
165
+ if (!pythonCheck.available) {
166
+ console.log(" ⚠ Python 3 not found. Python tools will not work.");
167
+ console.log(" → Install Python 3.8+ from https://python.org");
168
+ return;
169
+ }
170
+
171
+ console.log(` ✓ Python ${pythonCheck.version} detected`);
172
+ const pythonCmd = pythonCheck.cmd;
173
+
174
+ const pipCmd = checkPip(pythonCmd);
175
+ if (!pipCmd) {
176
+ console.log(" ⚠ pip not found. Cannot auto-install Python packages.");
177
+ return;
178
+ }
179
+ console.log(` ✓ pip detected`);
180
+
181
+ // Required packages
182
+ const requiredPackages = ["requests", "beautifulsoup4", "urllib3"];
183
+ for (const pkg of requiredPackages) {
184
+ if (!checkPythonPackage(pythonCmd, pkg)) {
185
+ console.log(` Installing ${pkg}...`);
186
+ const result = installPythonPackage(pipCmd, pkg);
187
+ if (result.success) {
188
+ console.log(` ✓ ${pkg} installed`);
189
+ } else {
190
+ console.log(` ✗ Failed to install ${pkg}`);
191
+ }
192
+ } else {
193
+ console.log(` ✓ ${pkg} already installed`);
194
+ }
195
+ }
196
+
197
+ // Check Playwright
198
+ const pwCheck = checkPlaywright(pythonCmd);
199
+ if (!pwCheck.installed || !pwCheck.browsersInstalled) {
200
+ console.log(" Playwright not fully installed, installing...");
201
+ const result = installPlaywright(pythonCmd);
202
+ if (result.success) {
203
+ console.log(" ✓ Playwright + browsers installed");
204
+ } else {
205
+ console.log(` ⚠ Playwright installation failed: ${result.error}`);
206
+ console.log(" → browser_collect tool will not work until Playwright is installed");
207
+ console.log(` → Manual fix: ${pythonCmd} -m pip install playwright && ${pythonCmd} -m playwright install chromium`);
208
+ }
209
+ } else {
210
+ console.log(" ✓ Playwright already installed");
211
+ }
212
+
213
+ console.log("[env-check] Environment check complete\n");
214
+ }
215
+
50
216
  function main() {
51
217
  const packageRoot = __dirname;
52
218
  const agentsSourceDir = join(packageRoot, "agents");
@@ -58,12 +224,13 @@ function main() {
58
224
 
59
225
  console.log("[api-security-testing] Installing...");
60
226
  console.log(` Package root: ${packageRoot}`);
227
+ console.log(` Platform: ${platform()}`);
61
228
 
62
229
  let totalInstalled = 0;
63
230
  let totalFailed = 0;
64
231
 
65
- // 1. Install agents to BOTH locations (oh-my-opencode + OpenCode native)
66
- console.log("\n[1/4] Installing agents to ~/.claude/agents/ (oh-my-opencode)...");
232
+ // 1. Install agents to BOTH locations
233
+ console.log("\n[1/5] Installing agents to ~/.claude/agents/ (oh-my-opencode)...");
67
234
  if (existsSync(agentsSourceDir)) {
68
235
  if (!existsSync(claudeAgentsDir)) {
69
236
  mkdirSync(claudeAgentsDir, { recursive: true });
@@ -82,7 +249,7 @@ function main() {
82
249
  }
83
250
  }
84
251
 
85
- console.log("\n[2/4] Installing agents to ~/.config/opencode/agents/ (OpenCode native)...");
252
+ console.log("\n[2/5] Installing agents to ~/.config/opencode/agents/ (OpenCode native)...");
86
253
  if (existsSync(agentsSourceDir)) {
87
254
  if (!existsSync(opencodeAgentsDir)) {
88
255
  mkdirSync(opencodeAgentsDir, { recursive: true });
@@ -102,7 +269,7 @@ function main() {
102
269
  }
103
270
 
104
271
  // 3. Install SKILL.md
105
- console.log("\n[3/4] Installing SKILL.md...");
272
+ console.log("\n[3/5] Installing SKILL.md...");
106
273
  const skillSource = join(packageRoot, "SKILL.md");
107
274
  if (existsSync(skillSource)) {
108
275
  if (!existsSync(skillTargetDir)) {
@@ -119,7 +286,7 @@ function main() {
119
286
  }
120
287
 
121
288
  // 4. Install references
122
- console.log("\n[4/4] Installing references...");
289
+ console.log("\n[4/5] Installing references...");
123
290
  const refsSourceDir = join(packageRoot, "references");
124
291
  const refsTargetDir = join(skillTargetDir, "references");
125
292
  if (existsSync(refsSourceDir)) {
@@ -133,6 +300,14 @@ function main() {
133
300
  }
134
301
  }
135
302
 
303
+ // 5. Environment detection and auto-install
304
+ console.log("\n[5/5] Detecting environment and installing dependencies...");
305
+ try {
306
+ checkAndFixEnvironment();
307
+ } catch (err) {
308
+ console.log(` ⚠ Environment check failed: ${err.message}`);
309
+ }
310
+
136
311
  console.log(`\n========================================`);
137
312
  if (totalFailed === 0) {
138
313
  console.log(`✓ Installed ${totalInstalled} file(s)`);
package/src/index.ts CHANGED
@@ -415,6 +415,171 @@ from testers.auth_tester import AuthTester
415
415
  tester = AuthTester()
416
416
  result = tester.test('${args.endpoint}')
417
417
  print(result)
418
+ "`;
419
+ return await execShell(ctx, cmd);
420
+ },
421
+ }),
422
+
423
+ // 新增: 端点自动发现工具
424
+ endpoint_discover: tool({
425
+ description: "自动发现 API 端点。从 JS 文件、HTML、Sitemap 中提取所有 API 路径",
426
+ args: {
427
+ target: tool.schema.string(),
428
+ depth: tool.schema.enum(["shallow", "deep"]).optional(),
429
+ include_methods: tool.schema.boolean().optional()
430
+ },
431
+ async execute(args, ctx) {
432
+ const deps = checkDeps(ctx);
433
+ const corePath = getCorePath(ctx);
434
+ const target = args.target as string;
435
+ const depth = (args.depth as string) || "shallow";
436
+ const includeMethods = (args.include_methods as boolean) !== false;
437
+ const cmd = `${deps}python3 -c "
438
+ import sys, re, json, urllib.request, ssl
439
+ sys.path.insert(0, '${corePath}')
440
+ ssl._create_default_https_context = ssl._create_unverified_context
441
+ target = '${target}'
442
+ depth = '${depth}'
443
+ endpoints = set()
444
+ js_endpoints = set()
445
+ html_endpoints = set()
446
+ patterns = [r'[\\"\\'](/api/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/v[0-9]+/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/admin/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/auth/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/graphql)[\\"\\']', r'[\\"\\'](/rest/[^\\"\\'\\s?#]+)[\\"\\']', r'axios\\.(get|post|put|delete|patch)\([\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', r'fetch\([\\"\\']([^\\"\\'\\s?#]+)[\\"\\']']
447
+ def safe_fetch(url, timeout=10):
448
+ try:
449
+ req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
450
+ with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode('utf-8', errors='ignore')
451
+ except: return None
452
+ def extract(text):
453
+ found = []
454
+ for p in patterns:
455
+ for m in re.findall(p, text):
456
+ ep = m[-1] if isinstance(m, tuple) else m
457
+ ep = ep.strip().lstrip('/')
458
+ if ep and len(ep) > 2 and not ep.startswith(('http', 'data:', '#')):
459
+ if not any(ep.lower().endswith(x) for x in ['.js','.css','.png','.jpg','.gif','.svg','.ico']):
460
+ found.append('/' + ep)
461
+ return found
462
+ parsed = __import__('urllib.parse').urlparse(target)
463
+ base = f'{parsed.scheme}://{parsed.netloc}'
464
+ html = safe_fetch(target)
465
+ if html:
466
+ html_endpoints.update(extract(html))
467
+ for js_url in re.findall(r'<script[^>]+src=[\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', html)[:20 if depth=='shallow' else 50]:
468
+ if js_url.startswith('/'): js_url = base + js_url
469
+ elif not js_url.startswith('http'): js_url = base + '/' + js_url
470
+ js_content = safe_fetch(js_url, timeout=10)
471
+ if js_content: js_endpoints.update(extract(js_content))
472
+ for path in ['/api', '/api/v1', '/api/docs', '/swagger.json', '/graphql', '/admin', '/auth', '/health', '/version']:
473
+ content = safe_fetch(base + path, timeout=5)
474
+ if content and len(content) > 10:
475
+ endpoints.add(path)
476
+ if content.strip().startswith('{'):
477
+ try:
478
+ for ep in re.findall(r'[\\"\\'](/[a-zA-Z0-9_./-]+)[\\"\\']', content[:10000]): endpoints.add(ep)
479
+ except: pass
480
+ if depth == 'deep':
481
+ for js_url in re.findall(r'<script[^>]+src=[\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', html or '')[:50]:
482
+ if js_url.startswith('/'): js_url = base + js_url
483
+ elif not js_url.startswith('http'): js_url = base + '/' + js_url
484
+ js_content = safe_fetch(js_url, timeout=10)
485
+ if js_content:
486
+ for p in [r'[\\"\\'](/[a-zA-Z0-9_./{{}}:-]+)[\\"\\']', r'path:\\s*[\\"\\'](/[a-zA-Z0-9_./{{}}:-]+)[\\"\\']']:
487
+ for m in re.findall(p, js_content):
488
+ ep = m[-1] if isinstance(m, tuple) else m
489
+ if len(ep) > 2 and not ep.startswith(('http', 'data:', '#')): endpoints.add(ep)
490
+ all_eps = sorted(set(endpoints) | set(html_endpoints) | set(js_endpoints))
491
+ clean = []
492
+ seen = set()
493
+ for ep in all_eps:
494
+ ep = ep.split('?')[0].split('#')[0].rstrip('/')
495
+ if ep and ep not in seen and len(ep) > 1:
496
+ seen.add(ep)
497
+ clean.append(ep)
498
+ result = {'target': target, 'total_endpoints': len(clean), 'endpoints': clean, 'sources': {'html': len(html_endpoints), 'javascript': len(js_endpoints), 'common_paths': len(endpoints)}}
499
+ print(json.dumps(result, indent=2, ensure_ascii=False))
500
+ "`;
501
+ return await execShell(ctx, cmd);
502
+ },
503
+ }),
504
+
505
+ // 新增: 报告生成工具
506
+ report_generate: tool({
507
+ description: "生成安全测试报告。支持 Markdown、HTML、JSON 格式,内置 OWASP API Top 10 模板",
508
+ args: {
509
+ format: tool.schema.enum(["markdown", "html", "json"]).optional(),
510
+ template: tool.schema.enum(["owasp-api", "custom"]).optional(),
511
+ target: tool.schema.string().optional(),
512
+ findings: tool.schema.string().optional(),
513
+ severity_filter: tool.schema.enum(["all", "critical", "high", "medium", "low", "info"]).optional()
514
+ },
515
+ async execute(args, ctx) {
516
+ const deps = checkDeps(ctx);
517
+ const corePath = getCorePath(ctx);
518
+ const format = (args.format as string) || "markdown";
519
+ const template = (args.template as string) || "owasp-api";
520
+ const target = (args.target as string) || "Unknown";
521
+ const findings = (args.findings as string) || "";
522
+ const severityFilter = (args.severity_filter as string) || "all";
523
+ const cmd = `${deps}python3 -c "
524
+ import sys, json
525
+ from datetime import datetime
526
+ sys.path.insert(0, '${corePath}')
527
+ format = '${format}'
528
+ target = '${target.replace(/'/g, "\\'")}'
529
+ findings = '''${findings.replace(/'/g, "\\'")}'''
530
+ severity_filter = '${severityFilter}'
531
+ parsed_findings = []
532
+ if findings.strip():
533
+ try: parsed_findings = json.loads(findings)
534
+ except:
535
+ for line in findings.strip().split('\\n'):
536
+ if line.strip():
537
+ try: parsed_findings.append(json.loads(line))
538
+ except: pass
539
+ if severity_filter != 'all' and parsed_findings:
540
+ parsed_findings = [f for f in parsed_findings if f.get('severity','').lower() == severity_filter.lower()]
541
+ sev_counts = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0, 'info': 0}
542
+ for f in parsed_findings:
543
+ s = f.get('severity','info').lower()
544
+ if s in sev_counts: sev_counts[s] += 1
545
+ total = len(parsed_findings)
546
+ risk_score = sev_counts['critical']*10 + sev_counts['high']*7 + sev_counts['medium']*4 + sev_counts['low']*1
547
+ risk_level = 'CRITICAL' if risk_score >= 50 else 'HIGH' if risk_score >= 30 else 'MEDIUM' if risk_score >= 15 else 'LOW' if risk_score > 0 else 'NONE'
548
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
549
+ if format == 'markdown':
550
+ report = f'# API 安全测试报告\\n\\n## 基本信息\\n\\n| 项目 | 值 |\\n|------|------|\\n| 目标 | {target} |\\n| 测试时间 | {now} |\\n| 风险等级 | **{risk_level}** |\\n| 风险分数 | {risk_score}/100 |\\n\\n## 漏洞统计\\n\\n| 严重程度 | 数量 |\\n|---------|------|\\n| Critical | {sev_counts[\\\"critical\\\"]} |\\n| High | {sev_counts[\\\"high\\\"]} |\\n| Medium | {sev_counts[\\\"medium\\\"]} |\\n| Low | {sev_counts[\\\"low\\\"]} |\\n| Info | {sev_counts[\\\"info\\\"]} |\\n| **总计** | **{total}** |\\n\\n## 漏洞详情\\n\\n'
551
+ if not parsed_findings: report += '未发现安全漏洞。\\n\\n'
552
+ else:
553
+ for i, f in enumerate(parsed_findings, 1):
554
+ title = f.get('title', f.get('vulnerability', f'Vulnerability #{i}'))
555
+ sev = f.get('severity', 'Unknown').upper()
556
+ ep = f.get('endpoint', f.get('url', 'N/A'))
557
+ desc = f.get('description', f.get('detail', 'No description'))
558
+ poc = f.get('poc', f.get('proof_of_concept', ''))
559
+ rec = f.get('recommendation', f.get('fix', 'Review and fix'))
560
+ report += f'### {i}. [{sev}] {title}\\n\\n| 属性 | 值 |\\n|------|------|\\n| 端点 | \\\`{ep}\\\` |\\n\\n**描述:**\\n\\n{desc}\\n\\n'
561
+ if poc: report += f'**PoC:**\\n\\n\\\`\\\`\\\`bash\\n{poc}\\n\\\`\\\`\\\`\\n\\n'
562
+ report += f'**修复建议:**\\n\\n{rec}\\n\\n---\\n\\n'
563
+ report += f'\\n## 测试覆盖范围\\n\\n| 测试类别 | 状态 |\\n|---------|------|\\n| SQL 注入 | ✅ 已测试 |\\n| XSS | ✅ 已测试 |\\n| IDOR | ✅ 已测试 |\\n| 认证绕过 | ✅ 已测试 |\\n| 敏感数据 | ✅ 已测试 |\\n| 业务逻辑 | ✅ 已测试 |\\n| 安全配置 | ✅ 已测试 |\\n| 暴力破解 | ✅ 已测试 |\\n| SSRF | ✅ 已测试 |\\n| GraphQL | ✅ 已测试 |\\n\\n---\\n\\n*报告生成时间: {now}*\\n*工具: opencode-api-security-testing v5.1.0*\\n'
564
+ elif format == 'json':
565
+ report = json.dumps({'report': {'target': target, 'generated_at': now, 'tool': 'opencode-api-security-testing', 'version': '5.1.0'}, 'summary': {'total': total, 'risk_score': risk_score, 'risk_level': risk_level, 'severity_counts': sev_counts}, 'findings': parsed_findings}, indent=2, ensure_ascii=False)
566
+ else:
567
+ report = f'<!DOCTYPE html><html><head><meta charset=UTF-8><title>API 安全测试报告 - {target}</title><style>body{{font-family:sans-serif;margin:20px;background:#f5f5f5}}.container{{max-width:1200px;margin:0 auto;background:white;border-radius:8px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.1)}}h1{{color:#333}}.stats{{display:flex;gap:15px;flex-wrap:wrap;margin:20px 0}}.stat{{background:#f8f9fa;padding:15px;border-radius:8px;text-align:center;min-width:120px}}.stat .num{{font-size:32px;font-weight:bold}}.stat.critical .num{{color:#dc3545}}.stat.high .num{{color:#fd7e14}}.stat.medium .num{{color:#ffc107}}.stat.low .num{{color:#28a745}}.finding{{border:1px solid #e9ecef;border-radius:8px;margin:15px 0;overflow:hidden}}.finding-header{{padding:15px;display:flex;align-items:center;gap:10px}}.finding-header.critical{{background:#fff5f5;border-left:4px solid #dc3545}}.finding-header.high{{background:#fff8f0;border-left:4px solid #fd7e14}}.finding-header.medium{{background:#fffdf0;border-left:4px solid #ffc107}}.finding-header.low{{background:#f0fff4;border-left:4px solid #28a745}}.badge{{padding:3px 8px;border-radius:4px;font-size:11px;font-weight:bold;color:white}}.badge.critical{{background:#dc3545}}.badge.high{{background:#fd7e14}}.badge.medium{{background:#ffc107;color:#333}}.badge.low{{background:#28a745}}.finding-body{{padding:15px}}pre{{background:#f8f9fa;padding:10px;border-radius:4px;overflow-x:auto}}</style></head><body><div class=container><h1>API 安全测试报告</h1><p>目标: {target} | 时间: {now} | 风险: <strong>{risk_level}</strong> ({risk_score}/100)</p><div class=stats><div class=stat critical><div class=num>{sev_counts[\\\"critical\\\"]}</div><div>Critical</div></div><div class=stat high><div class=num>{sev_counts[\\\"high\\\"]}</div><div>High</div></div><div class=stat medium><div class=num>{sev_counts[\\\"medium\\\"]}</div><div>Medium</div></div><div class=stat low><div class=num>{sev_counts[\\\"low\\\"]}</div><div>Low</div></div><div class=stat><div class=num>{total}</div><div>Total</div></div></div>'
568
+ if not parsed_findings: report += '<h2 style=color:#28a745>未发现安全漏洞</h2>'
569
+ else:
570
+ for i, f in enumerate(parsed_findings, 1):
571
+ title = f.get('title', f.get('vulnerability', f'#{i}'))
572
+ sev = f.get('severity','unknown').lower()
573
+ ep = f.get('endpoint', f.get('url', 'N/A'))
574
+ desc = f.get('description', f.get('detail', ''))
575
+ poc = f.get('poc', '')
576
+ rec = f.get('recommendation', f.get('fix', ''))
577
+ report += f'<div class=finding><div class=finding-header {sev}><span class=badge {sev}>{sev.upper()}</span><strong>{i}. {title}</strong></div><div class=finding-body><p><strong>端点:</strong> <code>{ep}</code></p><p>{desc}</p>'
578
+ if poc: report += f'<h4>PoC</h4><pre>{poc}</pre>'
579
+ if rec: report += f'<h4>修复建议</h4><p>{rec}</p>'
580
+ report += '</div></div>'
581
+ report += f'<p style=color:#666;font-size:12px;text-align:center;margin-top:20px>opencode-api-security-testing v5.1.0 | {now}</p></div></body></html>'
582
+ print(report)
418
583
  "`;
419
584
  return await execShell(ctx, cmd);
420
585
  },