opencode-api-security-testing 2.0.0 → 2.1.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.
Files changed (63) hide show
  1. package/README.md +30 -24
  2. package/SKILL.md +1797 -0
  3. package/core/advanced_recon.py +788 -0
  4. package/core/agentic_analyzer.py +445 -0
  5. package/core/analyzers/api_parser.py +210 -0
  6. package/core/analyzers/response_analyzer.py +212 -0
  7. package/core/analyzers/sensitive_finder.py +184 -0
  8. package/core/api_fuzzer.py +422 -0
  9. package/core/api_interceptor.py +525 -0
  10. package/core/api_parser.py +955 -0
  11. package/core/browser_tester.py +479 -0
  12. package/core/cloud_storage_tester.py +1330 -0
  13. package/core/collectors/__init__.py +23 -0
  14. package/core/collectors/api_path_finder.py +300 -0
  15. package/core/collectors/browser_collect.py +645 -0
  16. package/core/collectors/browser_collector.py +411 -0
  17. package/core/collectors/http_client.py +111 -0
  18. package/core/collectors/js_collector.py +490 -0
  19. package/core/collectors/js_parser.py +780 -0
  20. package/core/collectors/url_collector.py +319 -0
  21. package/core/context_manager.py +682 -0
  22. package/core/deep_api_tester_v35.py +844 -0
  23. package/core/deep_api_tester_v55.py +366 -0
  24. package/core/dynamic_api_analyzer.py +532 -0
  25. package/core/http_client.py +179 -0
  26. package/core/models.py +296 -0
  27. package/core/orchestrator.py +890 -0
  28. package/core/prerequisite.py +227 -0
  29. package/core/reasoning_engine.py +1042 -0
  30. package/core/response_classifier.py +606 -0
  31. package/core/runner.py +938 -0
  32. package/core/scan_engine.py +599 -0
  33. package/core/skill_executor.py +435 -0
  34. package/core/skill_executor_v2.py +670 -0
  35. package/core/skill_executor_v3.py +704 -0
  36. package/core/smart_analyzer.py +687 -0
  37. package/core/strategy_pool.py +707 -0
  38. package/core/testers/auth_tester.py +264 -0
  39. package/core/testers/idor_tester.py +200 -0
  40. package/core/testers/sqli_tester.py +211 -0
  41. package/core/testing_loop.py +655 -0
  42. package/core/utils/base_path_dict.py +255 -0
  43. package/core/utils/payload_lib.py +167 -0
  44. package/core/utils/ssrf_detector.py +220 -0
  45. package/core/verifiers/vuln_verifier.py +536 -0
  46. package/package.json +17 -13
  47. package/references/asset-discovery.md +119 -612
  48. package/references/graphql-guidance.md +65 -641
  49. package/references/intake.md +84 -0
  50. package/references/report-template.md +131 -38
  51. package/references/rest-guidance.md +55 -526
  52. package/references/severity-model.md +52 -264
  53. package/references/test-matrix.md +65 -263
  54. package/references/validation.md +53 -400
  55. package/scripts/postinstall.js +46 -0
  56. package/src/index.ts +259 -275
  57. package/agents/cyber-supervisor.md +0 -55
  58. package/agents/probing-miner.md +0 -42
  59. package/agents/resource-specialist.md +0 -31
  60. package/commands/api-security-testing-scan.md +0 -59
  61. package/commands/api-security-testing-test.md +0 -49
  62. package/commands/api-security-testing.md +0 -72
  63. package/tsconfig.json +0 -17
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ API Fuzzer - API 路径模糊测试器
4
+ 基于发现的 API 路径,生成变体探测隐藏端点
5
+ """
6
+
7
+ import re
8
+ import time
9
+ from typing import List, Set, Dict, Tuple, Optional
10
+ from urllib.parse import urljoin, urlparse
11
+ from dataclasses import dataclass, field
12
+ import requests
13
+
14
+
15
+ @dataclass
16
+ class FuzzResult:
17
+ """Fuzzing 结果"""
18
+ path: str
19
+ method: str = "GET"
20
+ status_code: int = 0
21
+ content_length: int = 0
22
+ is_alive: bool = False
23
+ is_new: bool = False
24
+ response_time: float = 0.0
25
+
26
+
27
+ class APIfuzzer:
28
+ """
29
+ API 路径模糊测试器
30
+
31
+ 功能:
32
+ - 父路径探测 (parent_path + suffix)
33
+ - RESTful 路径生成
34
+ - 路径参数化测试
35
+ - 跨来源路径组合
36
+ """
37
+
38
+ # RESTful 常见后缀
39
+ RESTFUL_SUFFIXES = [
40
+ 'list', 'get', 'add', 'create', 'update', 'edit', 'delete', 'remove',
41
+ 'detail', 'info', 'view', 'show', 'query', 'search', 'fetch', 'load',
42
+ 'save', 'submit', 'export', 'import', 'upload', 'download',
43
+ 'config', 'setting', 'settings', 'options', 'permissions', 'all',
44
+ ]
45
+
46
+ # 常见资源名
47
+ COMMON_RESOURCES = [
48
+ 'user', 'users', 'product', 'products', 'order', 'orders',
49
+ 'admin', 'auth', 'login', 'logout', 'register', 'profile',
50
+ 'config', 'setting', 'settings', 'menu', 'role', 'permission',
51
+ 'department', 'organ', 'organization', 'company', 'employee',
52
+ ]
53
+
54
+ # 常见路径前缀
55
+ COMMON_PREFIXES = [
56
+ '/api', '/v1', '/v2', '/v3', '/rest', '/restful',
57
+ '/admin', '/user', '/auth', '/service', '/web', '/mobile',
58
+ ]
59
+
60
+ # 危险路径关键字 (跳过测试)
61
+ DANGEROUS_KEYWORDS = [
62
+ 'delete', 'remove', 'drop', 'truncate', 'shutdown', 'kill',
63
+ 'exec', 'eval', 'shell', 'cmd', 'backup', 'restore',
64
+ ]
65
+
66
+ def __init__(self, session: requests.Session = None):
67
+ self.session = session or requests.Session()
68
+ self.session.headers.update({
69
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
70
+ 'Accept': 'application/json, text/html, */*',
71
+ 'Accept-Encoding': 'gzip, deflate',
72
+ })
73
+
74
+ self.found_endpoints: List[FuzzResult] = []
75
+ self.tested_paths: Set[str] = set()
76
+
77
+ def generate_parent_fuzz_targets(self, api_paths: List[str], max_per_parent: int = 20) -> List[str]:
78
+ """
79
+ 基于父路径生成 Fuzz 目标
80
+
81
+ Args:
82
+ api_paths: 已发现的 API 路径列表
83
+ max_per_parent: 每个父路径最多生成的目标数
84
+
85
+ Returns:
86
+ Fuzz 目标路径列表
87
+ """
88
+ targets = []
89
+ parent_map = {}
90
+
91
+ for path in api_paths:
92
+ if not path or len(path) < 2:
93
+ continue
94
+
95
+ path = path.strip()
96
+ parts = path.strip('/').split('/')
97
+
98
+ for i in range(1, len(parts)):
99
+ parent = '/' + '/'.join(parts[:i])
100
+ if parent not in parent_map:
101
+ parent_map[parent] = []
102
+ if i < len(parts):
103
+ child = parts[i]
104
+ if child not in parent_map[parent]:
105
+ parent_map[parent].append(child)
106
+
107
+ for parent, children in parent_map.items():
108
+ targets.append(parent)
109
+
110
+ for suffix in self.RESTFUL_SUFFIXES[:10]:
111
+ if len(targets) >= max_per_parent * len(parent_map):
112
+ break
113
+ targets.append(f"{parent}/{suffix}")
114
+
115
+ for child in children[:5]:
116
+ if child in self.RESTFUL_SUFFIXES:
117
+ continue
118
+ targets.append(f"{parent}/{child}")
119
+ for suffix in self.RESTFUL_SUFFIXES[:5]:
120
+ targets.append(f"{parent}/{child}/{suffix}")
121
+
122
+ return list(set(targets))[:500]
123
+
124
+ def generate_cross_source_targets(self, js_paths: List[str], html_paths: List[str], api_paths: List[str]) -> List[str]:
125
+ """
126
+ 跨来源组合路径
127
+
128
+ 将不同来源的路径片段智能组合探测隐藏 API
129
+
130
+ Args:
131
+ js_paths: JS 中发现的路径
132
+ html_paths: HTML 中发现的路径
133
+ api_paths: API 中发现的路径
134
+
135
+ Returns:
136
+ 组合后的测试目标
137
+ """
138
+ all_segments: Set[str] = set()
139
+
140
+ for path_list in [js_paths, html_paths, api_paths]:
141
+ for path in path_list:
142
+ parts = path.strip('/').split('/')
143
+ for part in parts:
144
+ if part and not part.startswith('{') and not part.isdigit():
145
+ if len(part) > 1:
146
+ all_segments.add(part)
147
+
148
+ targets = []
149
+
150
+ for prefix in self.COMMON_PREFIXES[:5]:
151
+ for segment in list(all_segments)[:30]:
152
+ if segment.lower() not in [p.lower() for p in self.COMMON_PREFIXES]:
153
+ targets.append(f"{prefix}/{segment}")
154
+ for suffix in self.RESTFUL_SUFFIXES[:5]:
155
+ targets.append(f"{prefix}/{segment}/{suffix}")
156
+
157
+ return list(set(targets))[:200]
158
+
159
+ def generate_parameter_fuzz_targets(self, endpoints: List[Dict], params: List[str]) -> List[Tuple[str, Dict]]:
160
+ """
161
+ 生成参数 Fuzz 目标
162
+
163
+ Args:
164
+ endpoints: 端点列表
165
+ params: 参数名列表
166
+
167
+ Returns:
168
+ (url, params) 元组列表
169
+ """
170
+ targets = []
171
+
172
+ common_values = {
173
+ 'id': ['1', '0', '999999', '-1', "1' OR '1'='1"],
174
+ 'page': ['1', '0', '999'],
175
+ 'pageSize': ['10', '50', '100', '9999'],
176
+ 'userId': ['1', '0', 'admin', "admin'--"],
177
+ 'type': ['1', '0', 'admin', 'test'],
178
+ 'search': ["' OR '1'='1", '<script>alert(1)</script>', '${jndi}'],
179
+ 'q': ["' OR '1'='1", '<script>alert(1)</script>'],
180
+ }
181
+
182
+ for endpoint in endpoints[:50]:
183
+ path = endpoint.get('path', endpoint.get('url', ''))
184
+ method = endpoint.get('method', 'GET')
185
+
186
+ if not path:
187
+ continue
188
+
189
+ for param in params[:10]:
190
+ value = common_values.get(param, ['1', 'test', 'admin'])
191
+ for v in value[:3]:
192
+ targets.append((path, {param: v}))
193
+
194
+ return targets[:500]
195
+
196
+ def fuzz_paths(self, base_url: str, paths: List[str],
197
+ methods: List[str] = None,
198
+ timeout: float = 5.0,
199
+ skip_dangerous: bool = True) -> List[FuzzResult]:
200
+ """
201
+ 执行路径 Fuzzing
202
+
203
+ Args:
204
+ base_url: 基础 URL
205
+ paths: 路径列表
206
+ methods: HTTP 方法列表
207
+ timeout: 超时时间
208
+ skip_dangerous: 跳过危险路径
209
+
210
+ Returns:
211
+ Fuzz 结果列表
212
+ """
213
+ methods = methods or ['GET', 'POST', 'PUT', 'DELETE', 'HEAD']
214
+ results = []
215
+
216
+ for path in paths:
217
+ if path in self.tested_paths:
218
+ continue
219
+
220
+ if skip_dangerous and any(k in path.lower() for k in self.DANGEROUS_KEYWORDS):
221
+ continue
222
+
223
+ self.tested_paths.add(path)
224
+ full_url = urljoin(base_url, path)
225
+
226
+ for method in methods:
227
+ try:
228
+ start_time = time.time()
229
+ resp = self.session.request(
230
+ method,
231
+ full_url,
232
+ timeout=timeout,
233
+ allow_redirects=False
234
+ )
235
+ response_time = time.time() - start_time
236
+
237
+ result = FuzzResult(
238
+ path=path,
239
+ method=method,
240
+ status_code=resp.status_code,
241
+ content_length=len(resp.content),
242
+ is_alive=resp.status_code < 500,
243
+ is_new=resp.status_code not in [301, 302, 404],
244
+ response_time=response_time
245
+ )
246
+ results.append(result)
247
+ self.found_endpoints.append(result)
248
+
249
+ except requests.exceptions.Timeout:
250
+ results.append(FuzzResult(
251
+ path=path, method=method, status_code=0,
252
+ is_alive=False, response_time=timeout
253
+ ))
254
+ except Exception:
255
+ pass
256
+
257
+ return results
258
+
259
+ def fuzz_with_params(self, base_url: str, targets: List[Tuple[str, Dict]],
260
+ timeout: float = 5.0) -> List[FuzzResult]:
261
+ """
262
+ 执行带参数的 Fuzzing
263
+
264
+ Args:
265
+ base_url: 基础 URL
266
+ targets: (path, params) 元组列表
267
+ timeout: 超时时间
268
+
269
+ Returns:
270
+ Fuzz 结果列表
271
+ """
272
+ results = []
273
+
274
+ for path, params in targets:
275
+ full_url = urljoin(base_url, path)
276
+
277
+ try:
278
+ start_time = time.time()
279
+ resp = self.session.post(
280
+ full_url,
281
+ json=params,
282
+ timeout=timeout,
283
+ allow_redirects=False
284
+ )
285
+ response_time = time.time() - start_time
286
+
287
+ result = FuzzResult(
288
+ path=f"{path} (POST JSON {params})",
289
+ method='POST',
290
+ status_code=resp.status_code,
291
+ content_length=len(resp.content),
292
+ is_alive=resp.status_code < 500,
293
+ is_new=resp.status_code not in [301, 302, 404],
294
+ response_time=response_time
295
+ )
296
+ results.append(result)
297
+ self.found_endpoints.append(result)
298
+
299
+ except Exception:
300
+ pass
301
+
302
+ return results
303
+
304
+ def get_alive_endpoints(self) -> List[FuzzResult]:
305
+ """获取存活的端点"""
306
+ return [r for r in self.found_endpoints if r.is_alive and r.status_code not in [301, 302]]
307
+
308
+ def get_high_value_endpoints(self) -> List[FuzzResult]:
309
+ """获取高价值端点 (非标准状态码)"""
310
+ return [r for r in self.found_endpoints if r.is_new]
311
+
312
+ def get_summary(self) -> Dict:
313
+ """获取 Fuzzing 结果摘要"""
314
+ alive = self.get_alive_endpoints()
315
+ high_value = self.get_high_value_endpoints()
316
+
317
+ status_counts = {}
318
+ for r in self.found_endpoints:
319
+ status_counts[r.status_code] = status_counts.get(r.status_code, 0) + 1
320
+
321
+ return {
322
+ 'total_tested': len(self.tested_paths),
323
+ 'total_results': len(self.found_endpoints),
324
+ 'alive_endpoints': len(alive),
325
+ 'high_value_endpoints': len(high_value),
326
+ 'status_distribution': status_counts,
327
+ 'avg_response_time': sum(r.response_time for r in self.found_endpoints) / max(len(self.found_endpoints), 1)
328
+ }
329
+
330
+
331
+ def auto_fuzz(target_url: str, api_paths: List[str] = None,
332
+ js_content: str = None, html_content: str = None,
333
+ session: requests.Session = None) -> Dict:
334
+ """
335
+ 自动 Fuzzing 流程
336
+
337
+ Args:
338
+ target_url: 目标 URL
339
+ api_paths: 已发现的 API 路径
340
+ js_content: JS 文件内容
341
+ html_content: HTML 内容
342
+
343
+ Returns:
344
+ Fuzzing 结果
345
+ """
346
+ api_paths = api_paths or []
347
+ session = session or requests.Session()
348
+
349
+ fuzzer = APIfuzzer(session=session)
350
+
351
+ all_paths = set(api_paths)
352
+
353
+ if js_content:
354
+ js_api_patterns = [
355
+ r"['\"](/api/[^'\"\\\s]+)['\"]",
356
+ r"['\"](/v\d+/[^'\"\\\s]+)['\"]",
357
+ r"baseURL\s*[:=]\s*['\"]([^'\"]+)['\"]",
358
+ ]
359
+ for pattern in js_api_patterns:
360
+ matches = re.findall(pattern, js_content)
361
+ all_paths.update(matches)
362
+
363
+ if html_content:
364
+ html_patterns = [
365
+ r"href=['\"](/[^'\"]+)['\"]",
366
+ r"src=['\"](/[^'\"]+\.js)['\"]",
367
+ ]
368
+ for pattern in html_patterns:
369
+ matches = re.findall(pattern, html_content)
370
+ all_paths.update(matches)
371
+
372
+ parent_targets = fuzzer.generate_parent_fuzz_targets(list(all_paths))
373
+
374
+ cross_targets = []
375
+ if js_content and html_content:
376
+ cross_targets = fuzzer.generate_cross_source_targets(
377
+ js_paths=api_paths,
378
+ html_paths=[],
379
+ api_paths=api_paths
380
+ )
381
+
382
+ all_targets = list(set(parent_targets + cross_targets))
383
+
384
+ print(f"[*] Generated {len(all_targets)} fuzz targets")
385
+
386
+ results = fuzzer.fuzz_paths(target_url, all_targets[:200], timeout=3.0)
387
+
388
+ summary = fuzzer.get_summary()
389
+
390
+ return {
391
+ 'targets_generated': len(all_targets),
392
+ 'endpoints_tested': summary['total_tested'],
393
+ 'alive_endpoints': summary['alive_endpoints'],
394
+ 'high_value_endpoints': summary['high_value_endpoints'],
395
+ 'results': results,
396
+ 'summary': summary
397
+ }
398
+
399
+
400
+ if __name__ == "__main__":
401
+ import argparse
402
+
403
+ parser = argparse.ArgumentParser(description="API Fuzzer")
404
+ parser.add_argument("--target", required=True, help="Target URL")
405
+ parser.add_argument("--paths", help="File with API paths")
406
+ parser.add_argument("--output", help="Output file")
407
+
408
+ args = parser.parse_args()
409
+
410
+ session = requests.Session()
411
+ result = auto_fuzz(args.target, session=session)
412
+
413
+ print(f"\n[*] Fuzzing Summary:")
414
+ print(f" Targets: {result['targets_generated']}")
415
+ print(f" Tested: {result['endpoints_tested']}")
416
+ print(f" Alive: {result['alive_endpoints']}")
417
+ print(f" High Value: {result['high_value_endpoints']}")
418
+
419
+ if args.output:
420
+ import json
421
+ with open(args.output, 'w') as f:
422
+ json.dump(result, f, indent=2)