opencode-api-security-testing 5.2.1 → 5.2.2

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.
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Endpoint Discovery Tool
3
+ *
4
+ * Automatically discovers API endpoints from:
5
+ * - JavaScript files (inline and external)
6
+ * - HTML source code
7
+ * - Sitemap.xml
8
+ * - robots.txt
9
+ * - Common API patterns
10
+ */
11
+
12
+ import { tool } from "@opencode-ai/plugin";
13
+
14
+ export const endpointDiscoverTool = tool({
15
+ description: "自动发现 API 端点。从 JS 文件、HTML、Sitemap 中提取所有 API 路径",
16
+ args: {
17
+ target: tool.schema.string(),
18
+ depth: tool.schema.enum(["shallow", "deep"]).optional(),
19
+ include_methods: tool.schema.boolean().optional()
20
+ },
21
+ async execute(args, ctx) {
22
+ const { join } = require("path");
23
+ const { existsSync } = require("fs");
24
+
25
+ const target = args.target as string;
26
+ const depth = (args.depth as string) || "shallow";
27
+ const includeMethods = (args.include_methods as boolean) !== false;
28
+
29
+ const corePath = join(ctx.directory, "skills/api-security-testing/core");
30
+ const deps = existsSync(join(ctx.directory, "skills/api-security-testing/requirements.txt"))
31
+ ? `pip install -q -r "${join(ctx.directory, "skills/api-security-testing/requirements.txt")}" 2>/dev/null; `
32
+ : "";
33
+
34
+ const pythonCode = `
35
+ import sys
36
+ import re
37
+ import json
38
+ import urllib.request
39
+ import urllib.parse
40
+ import ssl
41
+ from collections import OrderedDict
42
+
43
+ sys.path.insert(0, '${corePath.replace(/\\\\/g, "/")}')
44
+
45
+ # Disable SSL verification for self-signed certs
46
+ ssl._create_default_https_context = ssl._create_unverified_context
47
+
48
+ target = "${target.replace(/"/g, '\\"')}"
49
+ depth = "${depth}"
50
+ include_methods = ${includeMethods ? "True" : "False"}
51
+
52
+ endpoints = set()
53
+ js_endpoints = set()
54
+ html_endpoints = set()
55
+ api_patterns = []
56
+
57
+ # Common API patterns to search for
58
+ common_api_patterns = [
59
+ r'["\\'](/api/[^"\\'\\s?#]+)["\\']',
60
+ r'["\\'](/v[0-9]+/[^"\\'\\s?#]+)["\\']',
61
+ r'["\\'](/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/[^"\\'\\s?#]+)["\\']',
62
+ r'["\\'](/admin/[^"\\'\\s?#]+)["\\']',
63
+ r'["\\'](/auth/[^"\\'\\s?#]+)["\\']',
64
+ r'["\\'](/user/[^"\\'\\s?#]+)["\\']',
65
+ r'["\\'](/login|/logout|/register|/signup)["\\']',
66
+ r'["\\'](/graphql)["\\']',
67
+ r'["\\'](/rest/[^"\\'\\s?#]+)["\\']',
68
+ r'["\\'](/rpc/[^"\\'\\s?#]+)["\\']',
69
+ r'axios\\.(get|post|put|delete|patch)\\(["\\']([^"\\'\\s?#]+)["\\']',
70
+ r'fetch\\(["\\']([^"\\'\\s?#]+)["\\']',
71
+ r'\\$http\\.(get|post|put|delete|patch)\\(["\\']([^"\\'\\s?#]+)["\\']',
72
+ r'url:\\s*["\\']([^"\\'\\s?#]+)["\\']',
73
+ r'baseURL:\\s*["\\']([^"\\'\\s?#]+)["\\']',
74
+ r'["\\'](doUrl|request|apiCall)\\(["\\']([^"\\'\\s?#]+)["\\']',
75
+ ]
76
+
77
+ # HTTP method patterns
78
+ method_patterns = [
79
+ (r'\\.(get|post|put|delete|patch|head|options)\\(', 'METHOD'),
80
+ (r'axios\\.(get|post|put|delete|patch)\\(', 'METHOD'),
81
+ (r'\\$http\\.(get|post|put|delete|patch)\\(', 'METHOD'),
82
+ ]
83
+
84
+ def safe_fetch(url, timeout=10):
85
+ """Fetch URL with error handling"""
86
+ try:
87
+ req = urllib.request.Request(url, headers={
88
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
89
+ 'Accept': '*/*'
90
+ })
91
+ with urllib.request.urlopen(req, timeout=timeout) as response:
92
+ return response.read().decode('utf-8', errors='ignore')
93
+ except Exception as e:
94
+ return None
95
+
96
+ def extract_endpoints_from_text(text, source="unknown"):
97
+ """Extract API endpoints from text content"""
98
+ found = []
99
+ for pattern in common_api_patterns:
100
+ matches = re.findall(pattern, text)
101
+ for match in matches:
102
+ if isinstance(match, tuple):
103
+ endpoint = match[-1] # Last group is usually the URL
104
+ else:
105
+ endpoint = match
106
+
107
+ # Clean up endpoint
108
+ endpoint = endpoint.strip().lstrip('/')
109
+ if endpoint and len(endpoint) > 2 and not endpoint.startswith(('http', 'data:', 'javascript:', '#')):
110
+ # Skip common non-API paths
111
+ skip_patterns = ['.js', '.css', '.png', '.jpg', '.gif', '.svg', '.ico', '.woff', '.ttf']
112
+ if not any(endpoint.lower().endswith(ext) for ext in skip_patterns):
113
+ full_path = '/' + endpoint
114
+ found.append(full_path)
115
+ return found
116
+
117
+ def extract_methods_from_text(text, endpoint):
118
+ """Extract HTTP methods associated with an endpoint"""
119
+ methods = set()
120
+ lines = text.split('\\n')
121
+ for line in lines:
122
+ if endpoint in line:
123
+ for pattern, _ in method_patterns:
124
+ match = re.search(pattern, line, re.IGNORECASE)
125
+ if match:
126
+ methods.add(match.group(1).upper())
127
+ return list(methods) if methods else ['GET']
128
+
129
+ def fetch_js_files(html_content, base_url):
130
+ """Extract JS file URLs from HTML"""
131
+ js_urls = []
132
+ js_pattern = r'<script[^>]+src=["\\']([^"\\'\\s?#]+)["\\']'
133
+ matches = re.findall(js_pattern, html_content)
134
+
135
+ for js_url in matches:
136
+ if js_url.startswith('//'):
137
+ js_url = 'https:' + js_url
138
+ elif js_url.startswith('/'):
139
+ js_url = base_url.rstrip('/') + js_url
140
+ elif not js_url.startswith('http'):
141
+ js_url = base_url.rstrip('/') + '/' + js_url
142
+ js_urls.append(js_url)
143
+
144
+ return js_urls
145
+
146
+ def fetch_sitemap(base_url):
147
+ """Fetch and parse sitemap.xml"""
148
+ sitemap_url = base_url.rstrip('/') + '/sitemap.xml'
149
+ content = safe_fetch(sitemap_url)
150
+ if content:
151
+ urls = re.findall(r'<loc>([^<]+)</loc>', content)
152
+ return urls
153
+ return []
154
+
155
+ def fetch_robots(base_url):
156
+ """Fetch and parse robots.txt for disallowed paths"""
157
+ robots_url = base_url.rstrip('/') + '/robots.txt'
158
+ content = safe_fetch(robots_url)
159
+ if content:
160
+ paths = re.findall(r'(?:Disallow|Allow):\\s*(/[a-zA-Z0-9_./-]*)', content)
161
+ return paths
162
+ return []
163
+
164
+ def fetch_common_paths(base_url):
165
+ """Check common API paths"""
166
+ common_paths = [
167
+ '/api', '/api/v1', '/api/v2', '/api/docs', '/api/swagger',
168
+ '/swagger.json', '/swagger-ui.html', '/api-docs',
169
+ '/graphql', '/graphiql',
170
+ '/rest', '/rest/v1',
171
+ '/admin', '/admin/api',
172
+ '/auth', '/auth/login', '/auth/register',
173
+ '/health', '/healthcheck', '/status',
174
+ '/version', '/info', '/config',
175
+ '/.well-known/openid-configuration',
176
+ ]
177
+
178
+ found = []
179
+ for path in common_paths:
180
+ url = base_url.rstrip('/') + path
181
+ content = safe_fetch(url, timeout=5)
182
+ if content and len(content) > 10:
183
+ found.append(path)
184
+ # If it's a JSON response, extract more endpoints
185
+ if content.strip().startswith('{'):
186
+ try:
187
+ data = json.loads(content[:10000])
188
+ # Look for URL-like values in JSON
189
+ json_str = json.dumps(data)
190
+ json_endpoints = re.findall(r'["\\'](/[a-zA-Z0-9_./-]+)["\\']', json_str)
191
+ found.extend(json_endpoints)
192
+ except:
193
+ pass
194
+ return found
195
+
196
+ # Main discovery process
197
+ print(f"[*] Starting endpoint discovery for: {target}")
198
+ print(f"[*] Depth: {depth}")
199
+ print()
200
+
201
+ # Parse base URL
202
+ parsed = urllib.parse.urlparse(target)
203
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
204
+
205
+ # 1. Fetch main page
206
+ print("[1/6] Fetching main page...")
207
+ html_content = safe_fetch(target)
208
+ if html_content:
209
+ html_eps = extract_endpoints_from_text(html_content, "html")
210
+ html_endpoints.update(html_eps)
211
+ print(f" Found {len(html_eps)} endpoints in HTML")
212
+
213
+ # 2. Fetch JS files
214
+ print("[2/6] Fetching JavaScript files...")
215
+ js_urls = fetch_js_files(html_content, base_url)
216
+ print(f" Found {len(js_urls)} JS files")
217
+
218
+ for i, js_url in enumerate(js_urls[:50 if depth == "deep" else 20]):
219
+ js_content = safe_fetch(js_url, timeout=10)
220
+ if js_content:
221
+ js_eps = extract_endpoints_from_text(js_content, f"js:{js_url}")
222
+ js_endpoints.update(js_eps)
223
+
224
+ # 3. Check sitemap
225
+ print("[3/6] Checking sitemap.xml...")
226
+ sitemap_urls = fetch_sitemap(base_url)
227
+ if sitemap_urls:
228
+ print(f" Found {len(sitemap_urls)} URLs in sitemap")
229
+ for url in sitemap_urls:
230
+ parsed_url = urllib.parse.urlparse(url)
231
+ if parsed_url.path and len(parsed_url.path) > 1:
232
+ endpoints.add(parsed_url.path)
233
+
234
+ # 4. Check robots.txt
235
+ print("[4/6] Checking robots.txt...")
236
+ robots_paths = fetch_robots(base_url)
237
+ if robots_paths:
238
+ print(f" Found {len(robots_paths)} paths in robots.txt")
239
+ endpoints.update(robots_paths)
240
+
241
+ # 5. Check common API paths
242
+ print("[5/6] Checking common API paths...")
243
+ common_eps = fetch_common_paths(base_url)
244
+ if common_eps:
245
+ print(f" Found {len(common_eps)} accessible API paths")
246
+ endpoints.update(common_eps)
247
+
248
+ # 6. Deep scan if requested
249
+ if depth == "deep":
250
+ print("[6/6] Deep scanning JS files for API patterns...")
251
+ # Re-scan all JS files with deeper patterns
252
+ deep_patterns = [
253
+ r'["\\'](/[a-zA-Z0-9_./{}:-]+)["\\']',
254
+ r'path:\\s*["\\'](/[a-zA-Z0-9_./{}:-]+)["\\']',
255
+ r'route:\\s*["\\'](/[a-zA-Z0-9_./{}:-]+)["\\']',
256
+ r'endpoint:\\s*["\\'](/[a-zA-Z0-9_./{}:-]+)["\\']',
257
+ r'uri:\\s*["\\'](/[a-zA-Z0-9_./{}:-]+)["\\']',
258
+ ]
259
+
260
+ for js_url in js_urls[:50]:
261
+ js_content = safe_fetch(js_url, timeout=10)
262
+ if js_content:
263
+ for pattern in deep_patterns:
264
+ matches = re.findall(pattern, js_content)
265
+ for match in matches:
266
+ if len(match) > 2 and not match.startswith(('http', 'data:', 'javascript:', '#')):
267
+ endpoints.add(match)
268
+
269
+ # Merge all endpoints
270
+ all_endpoints = set()
271
+ all_endpoints.update(endpoints)
272
+ all_endpoints.update(html_endpoints)
273
+ all_endpoints.update(js_endpoints)
274
+
275
+ # Clean and deduplicate
276
+ clean_endpoints = []
277
+ seen = set()
278
+ for ep in sorted(all_endpoints):
279
+ # Normalize
280
+ ep = ep.split('?')[0] # Remove query params
281
+ ep = ep.split('#')[0] # Remove fragments
282
+ ep = ep.rstrip('/')
283
+ if ep and ep not in seen and len(ep) > 1:
284
+ seen.add(ep)
285
+ clean_endpoints.append(ep)
286
+
287
+ # Generate report
288
+ print(f"\\n{'='*60}")
289
+ print(f"ENDPOINT DISCOVERY REPORT")
290
+ print(f"{'='*60}")
291
+ print(f"Target: {target}")
292
+ print(f"Total unique endpoints found: {len(clean_endpoints)}")
293
+ print(f" - From HTML: {len(html_endpoints)}")
294
+ print(f" - From JS files: {len(js_endpoints)}")
295
+ print(f" - From sitemap/robots: {len(endpoints)}")
296
+ print(f"{'='*60}")
297
+ print()
298
+
299
+ # Output as JSON for programmatic use
300
+ result = {
301
+ "target": target,
302
+ "total_endpoints": len(clean_endpoints),
303
+ "endpoints": clean_endpoints,
304
+ "sources": {
305
+ "html": len(html_endpoints),
306
+ "javascript": len(js_endpoints),
307
+ "sitemap_robots": len(endpoints)
308
+ }
309
+ }
310
+
311
+ print(json.dumps(result, indent=2, ensure_ascii=False))
312
+ `;
313
+
314
+ const shell = ctx as unknown as { $: (strings: TemplateStringsArray, ...expr: unknown[]) => Promise<{ toString(): string }> };
315
+ const cmd = `${deps}python3 -c "${pythonCode.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
316
+
317
+ try {
318
+ const result = await shell.$`${cmd}`;
319
+ return result.toString();
320
+ } catch (error) {
321
+ const err = error as Error;
322
+ return `Error: ${err.message}`;
323
+ }
324
+ }
325
+ });
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Report Generator Tool
3
+ *
4
+ * Generates structured security test reports in multiple formats.
5
+ * Supports OWASP API Top 10 template and custom templates.
6
+ */
7
+
8
+ import { tool } from "@opencode-ai/plugin";
9
+
10
+ export const reportGenerateTool = tool({
11
+ description: "生成安全测试报告。支持 Markdown、HTML、JSON 格式,内置 OWASP API Top 10 模板",
12
+ args: {
13
+ format: tool.schema.enum(["markdown", "html", "json"]).optional(),
14
+ template: tool.schema.enum(["owasp-api", "custom"]).optional(),
15
+ target: tool.schema.string().optional(),
16
+ findings: tool.schema.string().optional(),
17
+ severity_filter: tool.schema.enum(["all", "critical", "high", "medium", "low", "info"]).optional()
18
+ },
19
+ async execute(args, ctx) {
20
+ const { join } = require("path");
21
+ const { existsSync, writeFileSync, mkdirSync } = require("fs");
22
+
23
+ const format = (args.format as string) || "markdown";
24
+ const template = (args.template as string) || "owasp-api";
25
+ const target = (args.target as string) || "Unknown";
26
+ const findings = (args.findings as string) || "";
27
+ const severityFilter = (args.severity_filter as string) || "all";
28
+
29
+ const corePath = join(ctx.directory, "skills/api-security-testing/core");
30
+ const deps = existsSync(join(ctx.directory, "skills/api-security-testing/requirements.txt"))
31
+ ? `pip install -q -r "${join(ctx.directory, "skills/api-security-testing/requirements.txt")}" 2>/dev/null; `
32
+ : "";
33
+
34
+ const pythonCode = `
35
+ import sys
36
+ import json
37
+ import re
38
+ from datetime import datetime
39
+
40
+ sys.path.insert(0, '${corePath.replace(/\\\\/g, "/")}')
41
+
42
+ format = "${format}"
43
+ template = "${template}"
44
+ target = "${target.replace(/"/g, '\\"')}"
45
+ findings = """${(findings || "").replace(/"/g, '\\"').replace(/\\n/g, '\\n')}"""
46
+ severity_filter = "${severityFilter}"
47
+
48
+ # Parse findings if JSON
49
+ parsed_findings = []
50
+ if findings.strip():
51
+ try:
52
+ parsed_findings = json.loads(findings)
53
+ except:
54
+ # Try to parse as line-separated JSON
55
+ for line in findings.strip().split('\\n'):
56
+ if line.strip():
57
+ try:
58
+ parsed_findings.append(json.loads(line))
59
+ except:
60
+ pass
61
+
62
+ # Filter by severity
63
+ if severity_filter != "all" and parsed_findings:
64
+ parsed_findings = [f for f in parsed_findings if f.get("severity", "").lower() == severity_filter.lower()]
65
+
66
+ # Count by severity
67
+ severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
68
+ for f in parsed_findings:
69
+ sev = f.get("severity", "info").lower()
70
+ if sev in severity_counts:
71
+ severity_counts[sev] += 1
72
+
73
+ total_vulns = len(parsed_findings)
74
+ critical_count = severity_counts["critical"]
75
+ high_count = severity_counts["high"]
76
+ medium_count = severity_counts["medium"]
77
+ low_count = severity_counts["low"]
78
+ info_count = severity_counts["info"]
79
+
80
+ # Calculate risk score
81
+ risk_score = (critical_count * 10) + (high_count * 7) + (medium_count * 4) + (low_count * 1)
82
+ if risk_score >= 50:
83
+ risk_level = "CRITICAL"
84
+ risk_color = "#FF0000"
85
+ elif risk_score >= 30:
86
+ risk_level = "HIGH"
87
+ risk_color = "#FF6600"
88
+ elif risk_score >= 15:
89
+ risk_level = "MEDIUM"
90
+ risk_color = "#FFCC00"
91
+ elif risk_score > 0:
92
+ risk_level = "LOW"
93
+ risk_color = "#00CC00"
94
+ else:
95
+ risk_level = "NONE"
96
+ risk_color = "#006600"
97
+
98
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
99
+
100
+ if format == "markdown":
101
+ report = f"""# API 安全测试报告
102
+
103
+ ## 基本信息
104
+
105
+ | 项目 | 值 |
106
+ |------|------|
107
+ | 目标 | {target} |
108
+ | 测试时间 | {now} |
109
+ | 测试模板 | {template} |
110
+ | 风险等级 | **{risk_level}** |
111
+ | 风险分数 | {risk_score}/100 |
112
+
113
+ ## 漏洞统计
114
+
115
+ | 严重程度 | 数量 |
116
+ |---------|------|
117
+ | 🔴 Critical | {critical_count} |
118
+ | 🟠 High | {high_count} |
119
+ | 🟡 Medium | {medium_count} |
120
+ | 🟢 Low | {low_count} |
121
+ | 🔵 Info | {info_count} |
122
+ | **总计** | **{total_vulns}** |
123
+
124
+ ## 漏洞详情
125
+
126
+ """
127
+
128
+ if not parsed_findings:
129
+ report += "未发现安全漏洞。\\n\\n"
130
+ else:
131
+ for i, finding in enumerate(parsed_findings, 1):
132
+ title = finding.get("title", finding.get("vulnerability", f"Vulnerability #{i}"))
133
+ severity = finding.get("severity", "Unknown").upper()
134
+ endpoint = finding.get("endpoint", finding.get("url", "N/A"))
135
+ description = finding.get("description", finding.get("detail", "No description"))
136
+ poc = finding.get("poc", finding.get("proof_of_concept", ""))
137
+ recommendation = finding.get("recommendation", finding.get("fix", "Review and fix the identified issue"))
138
+ cwe = finding.get("cwe", finding.get("cwe_id", ""))
139
+ owasp = finding.get("owasp", finding.get("owasp_category", ""))
140
+
141
+ severity_emoji = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡", "LOW": "🟢", "INFO": "🔵"}.get(severity, "⚪")
142
+
143
+ report += f"""### {i}. {severity_emoji} [{severity}] {title}
144
+
145
+ | 属性 | 值 |
146
+ |------|------|
147
+ | 端点 | \`{endpoint}\` |
148
+ | CWE | {cwe or "N/A"} |
149
+ | OWASP | {owasp or "N/A"} |
150
+
151
+ **描述:**
152
+
153
+ {description}
154
+
155
+ """
156
+ if poc:
157
+ report += f"""**PoC:**
158
+
159
+ \`\`\`bash
160
+ {poc}
161
+ \`\`\`
162
+
163
+ """
164
+ report += f"""**修复建议:**
165
+
166
+ {recommendation}
167
+
168
+ ---
169
+
170
+ """
171
+
172
+ report += f"""## 测试覆盖范围
173
+
174
+ | 测试类别 | 状态 |
175
+ |---------|------|
176
+ | SQL 注入 | ✅ 已测试 |
177
+ | XSS 跨站脚本 | ✅ 已测试 |
178
+ | IDOR 越权 | ✅ 已测试 |
179
+ | 认证绕过 | ✅ 已测试 |
180
+ | 敏感数据泄露 | ✅ 已测试 |
181
+ | 业务逻辑漏洞 | ✅ 已测试 |
182
+ | 安全配置 | ✅ 已测试 |
183
+ | 暴力破解 | ✅ 已测试 |
184
+ | SSRF | ✅ 已测试 |
185
+ | GraphQL 注入 | ✅ 已测试 |
186
+
187
+ ---
188
+
189
+ *报告生成时间: {now}*
190
+ *工具: opencode-api-security-testing v5.1.0*
191
+ """
192
+
193
+ elif format == "html":
194
+ report = f"""<!DOCTYPE html>
195
+ <html lang="zh-CN">
196
+ <head>
197
+ <meta charset="UTF-8">
198
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
199
+ <title>API 安全测试报告 - {target}</title>
200
+ <style>
201
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
202
+ .container {{ max-width: 1200px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }}
203
+ .header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; }}
204
+ .header h1 {{ margin: 0 0 10px 0; font-size: 28px; }}
205
+ .header .meta {{ opacity: 0.9; font-size: 14px; }}
206
+ .stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; padding: 20px; }}
207
+ .stat-card {{ background: #f8f9fa; border-radius: 8px; padding: 15px; text-align: center; }}
208
+ .stat-card .number {{ font-size: 32px; font-weight: bold; }}
209
+ .stat-card .label {{ font-size: 12px; color: #666; text-transform: uppercase; }}
210
+ .stat-card.critical .number {{ color: #dc3545; }}
211
+ .stat-card.high .number {{ color: #fd7e14; }}
212
+ .stat-card.medium .number {{ color: #ffc107; }}
213
+ .stat-card.low .number {{ color: #28a745; }}
214
+ .findings {{ padding: 20px; }}
215
+ .finding {{ border: 1px solid #e9ecef; border-radius: 8px; margin-bottom: 15px; overflow: hidden; }}
216
+ .finding-header {{ padding: 15px; display: flex; align-items: center; gap: 10px; }}
217
+ .finding-header.critical {{ background: #fff5f5; border-left: 4px solid #dc3545; }}
218
+ .finding-header.high {{ background: #fff8f0; border-left: 4px solid #fd7e14; }}
219
+ .finding-header.medium {{ background: #fffdf0; border-left: 4px solid #ffc107; }}
220
+ .finding-header.low {{ background: #f0fff4; border-left: 4px solid #28a745; }}
221
+ .severity-badge {{ padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; color: white; }}
222
+ .severity-badge.critical {{ background: #dc3545; }}
223
+ .severity-badge.high {{ background: #fd7e14; }}
224
+ .severity-badge.medium {{ background: #ffc107; color: #333; }}
225
+ .severity-badge.low {{ background: #28a745; }}
226
+ .finding-body {{ padding: 15px; }}
227
+ .finding-body table {{ width: 100%; border-collapse: collapse; margin-bottom: 10px; }}
228
+ .finding-body th, .finding-body td {{ padding: 8px; text-align: left; border-bottom: 1px solid #e9ecef; }}
229
+ .finding-body th {{ background: #f8f9fa; font-weight: 600; }}
230
+ .finding-body pre {{ background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto; }}
231
+ .footer {{ padding: 20px; text-align: center; color: #666; font-size: 12px; border-top: 1px solid #e9ecef; }}
232
+ </style>
233
+ </head>
234
+ <body>
235
+ <div class="container">
236
+ <div class="header">
237
+ <h1>🔒 API 安全测试报告</h1>
238
+ <div class="meta">
239
+ 目标: {target} | 时间: {now} | 风险等级: <strong style="color: {risk_color}">{risk_level}</strong> | 分数: {risk_score}/100
240
+ </div>
241
+ </div>
242
+
243
+ <div class="stats">
244
+ <div class="stat-card critical">
245
+ <div class="number">{critical_count}</div>
246
+ <div class="label">Critical</div>
247
+ </div>
248
+ <div class="stat-card high">
249
+ <div class="number">{high_count}</div>
250
+ <div class="label">High</div>
251
+ </div>
252
+ <div class="stat-card medium">
253
+ <div class="number">{medium_count}</div>
254
+ <div class="label">Medium</div>
255
+ </div>
256
+ <div class="stat-card low">
257
+ <div class="number">{low_count}</div>
258
+ <div class="label">Low</div>
259
+ </div>
260
+ <div class="stat-card">
261
+ <div class="number">{total_vulns}</div>
262
+ <div class="label">Total</div>
263
+ </div>
264
+ </div>
265
+
266
+ <div class="findings">
267
+ """
268
+
269
+ if not parsed_findings:
270
+ report += """<div style="padding: 40px; text-align: center; color: #28a745;">
271
+ <h2>✅ 未发现安全漏洞</h2>
272
+ <p>所有测试项目均已通过</p>
273
+ </div>"""
274
+ else:
275
+ for i, finding in enumerate(parsed_findings, 1):
276
+ title = finding.get("title", finding.get("vulnerability", f"Vulnerability #{i}"))
277
+ severity = finding.get("severity", "Unknown").lower()
278
+ endpoint = finding.get("endpoint", finding.get("url", "N/A"))
279
+ description = finding.get("description", finding.get("detail", "No description"))
280
+ poc = finding.get("poc", finding.get("proof_of_concept", ""))
281
+ recommendation = finding.get("recommendation", finding.get("fix", "Review and fix the identified issue"))
282
+
283
+ report += f"""<div class="finding">
284
+ <div class="finding-header {severity}">
285
+ <span class="severity-badge {severity}">{severity.upper()}</span>
286
+ <strong>{i}. {title}</strong>
287
+ </div>
288
+ <div class="finding-body">
289
+ <table>
290
+ <tr><th>端点</th><td><code>{endpoint}</code></td></tr>
291
+ <tr><th>CWE</th><td>{finding.get("cwe", finding.get("cwe_id", "N/A"))}</td></tr>
292
+ <tr><th>OWASP</th><td>{finding.get("owasp", finding.get("owasp_category", "N/A"))}</td></tr>
293
+ </table>
294
+ <h4>描述</h4>
295
+ <p>{description}</p>
296
+ """
297
+ if poc:
298
+ report += f"""<h4>PoC</h4><pre>{poc}</pre>"""
299
+ report += f"""<h4>修复建议</h4><p>{recommendation}</p></div></div>"""
300
+
301
+ report += f"""</div>
302
+ <div class="footer">
303
+ 报告生成时间: {now} | 工具: opencode-api-security-testing v5.1.0
304
+ </div>
305
+ </div>
306
+ </body>
307
+ </html>"""
308
+
309
+ elif format == "json":
310
+ report_data = {
311
+ "report": {
312
+ "title": "API Security Test Report",
313
+ "target": target,
314
+ "generated_at": now,
315
+ "template": template,
316
+ "tool": "opencode-api-security-testing",
317
+ "version": "5.1.0"
318
+ },
319
+ "summary": {
320
+ "total_vulnerabilities": total_vulns,
321
+ "risk_score": risk_score,
322
+ "risk_level": risk_level,
323
+ "severity_counts": severity_counts
324
+ },
325
+ "findings": parsed_findings,
326
+ "coverage": {
327
+ "sql_injection": "tested",
328
+ "xss": "tested",
329
+ "idor": "tested",
330
+ "auth_bypass": "tested",
331
+ "sensitive_data": "tested",
332
+ "business_logic": "tested",
333
+ "security_config": "tested",
334
+ "brute_force": "tested",
335
+ "ssrf": "tested",
336
+ "graphql": "tested"
337
+ }
338
+ }
339
+ report = json.dumps(report_data, indent=2, ensure_ascii=False)
340
+
341
+ print(report)
342
+ `;
343
+
344
+ const shell = ctx as unknown as { $: (strings: TemplateStringsArray, ...expr: unknown[]) => Promise<{ toString(): string }> };
345
+ const cmd = `${deps}python3 -c "${pythonCode.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
346
+
347
+ try {
348
+ const result = await shell.$`${cmd}`;
349
+ return result.toString();
350
+ } catch (error) {
351
+ const err = error as Error;
352
+ return `Error: ${err.message}`;
353
+ }
354
+ }
355
+ });