opencode-api-security-testing 5.2.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +0 -6
- package/agents/api-cyber-supervisor.md +3 -5
- package/package.json +48 -48
- package/postinstall.mjs +39 -243
- package/src/index.ts +0 -165
- package/src/tools/endpoint-discover.ts +0 -325
- package/src/tools/report-generator.ts +0 -355
- package/src/utils/env-checker.ts +0 -264
|
@@ -1,325 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,355 +0,0 @@
|
|
|
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
|
-
});
|