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
package/core/runner.py ADDED
@@ -0,0 +1,938 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ SKILL.md 的完整可执行实现
5
+
6
+ 模块联动流程:
7
+ 1. TestContext - 共享测试上下文(session、endpoints、vulnerabilities)
8
+ 2. PrerequisiteChecker - 前置检查
9
+ 3. AssetDiscovery - 端点发现(静态+动态+Hook)
10
+ 4. VulnerabilityTester - 漏洞测试
11
+ 5. APIFuzzer - 模糊测试
12
+ 6. CloudStorageTester - 云存储测试
13
+ 7. ReportGenerator - 报告生成
14
+
15
+ 使用方式:
16
+ python3 -m core.runner http://target.com
17
+ """
18
+
19
+ import sys
20
+ import re
21
+ import time
22
+ import json
23
+ import logging
24
+ from datetime import datetime
25
+ from typing import Dict, List, Optional, Any, Set
26
+ from dataclasses import dataclass, field
27
+
28
+ sys.path.insert(0, '/workspace/skill-play/API-Security-Testing-Optimized')
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass
34
+ class TestContext:
35
+ """
36
+ 测试上下文 - 各模块共享数据
37
+ """
38
+ target: str
39
+ session: Any = None
40
+
41
+ # 共享数据
42
+ endpoints: List[Dict] = field(default_factory=list)
43
+ vulnerabilities: List[Dict] = field(default_factory=list)
44
+ cloud_findings: List[Dict] = field(default_factory=list)
45
+ parent_paths: Dict = field(default_factory=dict)
46
+ tech_stack: Dict = field(default_factory=dict)
47
+
48
+ # Hook 到的 API
49
+ hooked_apis: List[Dict] = field(default_factory=list)
50
+ sensitive_apis: List[Dict] = field(default_factory=list)
51
+ test_vectors: List[Dict] = field(default_factory=list)
52
+
53
+ # 状态
54
+ playwright_available: bool = False
55
+ backend_reachable: bool = True
56
+ nginx_fallback: bool = False
57
+
58
+ def add_endpoints(self, endpoints: List[Dict]):
59
+ """添加端点(去重)"""
60
+ existing = set((e.get('method', 'GET'), e.get('path', '')) for e in self.endpoints)
61
+ for ep in endpoints:
62
+ key = (ep.get('method', 'GET'), ep.get('path', ''))
63
+ if key not in existing:
64
+ self.endpoints.append(ep)
65
+ existing.add(key)
66
+
67
+ def add_vulnerability(self, vuln: Dict):
68
+ """添加漏洞(去重)"""
69
+ key = (vuln.get('type', ''), vuln.get('endpoint', ''))
70
+ existing = set((v.get('type', ''), v.get('endpoint', '')) for v in self.vulnerabilities)
71
+ if key not in existing:
72
+ self.vulnerabilities.append(vuln)
73
+
74
+ def get_all_endpoints(self) -> List[Dict]:
75
+ """获取所有端点(包括 Hook 到的)"""
76
+ all_eps = list(self.endpoints)
77
+ for hook in self.hooked_apis:
78
+ path = hook.get('path', hook.get('url', ''))
79
+ method = hook.get('method', 'GET')
80
+ if not any(e.get('path') == path and e.get('method') == method for e in all_eps):
81
+ all_eps.append(hook)
82
+ return all_eps
83
+
84
+
85
+ class PrerequisiteChecker:
86
+ """阶段 0: 前置检查"""
87
+
88
+ @staticmethod
89
+ def check(ctx: TestContext) -> bool:
90
+ """检查所有依赖,设置上下文"""
91
+ from core.prerequisite import prerequisite_check
92
+
93
+ print("[0] 前置检查")
94
+ print("-" * 70)
95
+
96
+ # 使用新的前置检查模块 (支持平替检测和自动安装)
97
+ playwright_available, browser_type, can_proceed = prerequisite_check()
98
+
99
+ ctx.playwright_available = playwright_available
100
+
101
+ # requests 检查
102
+ try:
103
+ import requests
104
+ ctx.session = requests.Session()
105
+ ctx.session.headers.update({
106
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
107
+ })
108
+ print(" [OK] requests")
109
+ except ImportError:
110
+ print(" [FAIL] requests")
111
+ return False
112
+
113
+ if not can_proceed:
114
+ print("\n[FATAL] 前置检查失败 - 缺少无头浏览器支持")
115
+ return False
116
+
117
+ print()
118
+ return ctx.playwright_available and ctx.session is not None
119
+
120
+
121
+ class AssetDiscovery:
122
+ """阶段 1: 资产发现
123
+
124
+ 使用 TestContext 共享数据
125
+ """
126
+
127
+ def __init__(self, ctx: TestContext):
128
+ self.ctx = ctx
129
+ self.target = ctx.target
130
+ self.session = ctx.session
131
+ self.js_files = []
132
+
133
+ def run(self):
134
+ """执行资产发现(联动各模块)"""
135
+ print("[1] 资产发现")
136
+ print("-" * 70)
137
+
138
+ # 1.1 目标探测
139
+ self._probe_target()
140
+
141
+ # 1.2 静态分析 (api_parser)
142
+ self._parse_with_api_parser()
143
+
144
+ # 1.3 动态分析 (dynamic_api_analyzer)
145
+ self._analyze_dynamic()
146
+
147
+ # 1.4 API Hook (api_interceptor) - 获取真实参数
148
+ self._hook_apis()
149
+
150
+ # 1.5 父路径探测
151
+ self._probe_parent_paths()
152
+
153
+ # 更新上下文
154
+ self.ctx.backend_reachable = len([p for p in self.ctx.parent_paths.values() if p.get('is_api')]) > 0
155
+ self.ctx.nginx_fallback = not self.ctx.backend_reachable
156
+
157
+ print(f"\n 发现端点: {len(self.ctx.endpoints)}")
158
+ print(f" Hook API: {len(self.ctx.hooked_apis)}")
159
+ print(f" 敏感 API: {len(self.ctx.sensitive_apis)}")
160
+ print(f" 父路径: {len(self.ctx.parent_paths)}")
161
+ print(f" 技术栈: {self.ctx.tech_stack}")
162
+ print()
163
+
164
+ return self.ctx
165
+
166
+ def _probe_target(self):
167
+ """基础探测"""
168
+ try:
169
+ r = self.session.get(self.target, timeout=10)
170
+
171
+ server = r.headers.get('Server', 'Unknown')
172
+ print(f" Server: {server}")
173
+
174
+ html = r.text.lower()
175
+ if 'vue' in html:
176
+ self.ctx.tech_stack['frontend'] = 'Vue.js'
177
+ if 'react' in html:
178
+ self.ctx.tech_stack['frontend'] = 'React'
179
+ if 'jquery' in html:
180
+ self.ctx.tech_stack['jquery'] = True
181
+ if 'element' in html:
182
+ self.ctx.tech_stack['ui'] = 'ElementUI'
183
+ if 'angular' in html:
184
+ self.ctx.tech_stack['frontend'] = 'Angular'
185
+
186
+ cors = r.headers.get('Access-Control-Allow-Origin', '未设置')
187
+ print(f" CORS: {cors}")
188
+
189
+ except Exception as e:
190
+ print(f" [WARN] 目标探测失败: {e}")
191
+
192
+ def _infer_semantic_type(self, path: str) -> str:
193
+ """推断路径的语义类型"""
194
+ path_lower = path.lower()
195
+
196
+ mappings = {
197
+ 'auth': ['/auth', '/login', '/logout', '/token', '/signin'],
198
+ 'user': ['/user', '/profile', '/account', '/avatar'],
199
+ 'admin': ['/admin', '/manage', '/system', '/config'],
200
+ 'file': ['/file', '/upload', '/download', '/attachment', '/image'],
201
+ 'order': ['/order', '/cart', '/checkout'],
202
+ 'product': ['/product', '/goods', '/sku'],
203
+ 'data': ['/data', '/statistics', '/report', '/analytics'],
204
+ 'api': ['/api', '/v1', '/v2', '/rest'],
205
+ 'search': ['/search', '/query', '/find'],
206
+ 'list': ['/list', '/items', '/records'],
207
+ 'detail': ['/detail', '/info', '/view'],
208
+ 'create': ['/create', '/add', '/new'],
209
+ 'update': ['/update', '/edit', '/modify'],
210
+ 'delete': ['/delete', '/remove'],
211
+ }
212
+
213
+ for semantic, keywords in mappings.items():
214
+ for keyword in keywords:
215
+ if keyword in path_lower:
216
+ return semantic
217
+
218
+ return 'unknown'
219
+
220
+ def _parse_with_api_parser(self):
221
+ """静态分析 (api_parser)"""
222
+ try:
223
+ from core.api_parser import APIEndpointParser
224
+
225
+ parser = APIEndpointParser(self.target, self.session)
226
+ self.js_files = parser.discover_js_files()
227
+ print(f" 发现 JS 文件: {len(self.js_files)}")
228
+
229
+ parsed_endpoints = parser.parse_js_files(self.js_files)
230
+
231
+ for ep in parsed_endpoints:
232
+ self.ctx.add_endpoints([{
233
+ 'path': ep.path,
234
+ 'method': ep.method,
235
+ 'params': [{'name': p.name, 'type': p.param_type.value, 'required': p.required} for p in ep.params],
236
+ 'source': ep.source,
237
+ 'semantic_type': ep.semantic_type,
238
+ 'has_params': ep.has_params(),
239
+ }])
240
+
241
+ print(f" 静态解析: {len(parsed_endpoints)} 端点")
242
+
243
+ except Exception as e:
244
+ print(f" [WARN] API Parser 失败: {e}")
245
+
246
+ except Exception as e:
247
+ print(f" [WARN] API Parser 失败: {e}")
248
+
249
+ def _analyze_dynamic(self):
250
+ """动态分析 (dynamic_api_analyzer)"""
251
+ try:
252
+ from core.dynamic_api_analyzer import DynamicAPIAnalyzer
253
+
254
+ analyzer = DynamicAPIAnalyzer(self.target)
255
+ results = analyzer.analyze_full()
256
+
257
+ for ep in results.get('endpoints', []):
258
+ path = ep.get('path', '')
259
+ method = ep.get('method', 'GET')
260
+ params_data = ep.get('params', [])
261
+ if isinstance(params_data, list):
262
+ params_dict = {p: True for p in params_data}
263
+ else:
264
+ params_dict = params_data
265
+
266
+ self.ctx.add_endpoints([{
267
+ 'path': path,
268
+ 'method': method,
269
+ 'params': params_dict,
270
+ 'source': f"dynamic_{ep.get('source', 'unknown')}",
271
+ 'semantic_type': self._infer_semantic_type(path),
272
+ }])
273
+
274
+ print(f" 动态分析: {results.get('unique_endpoints', 0)} 端点")
275
+
276
+ except Exception as e:
277
+ print(f" [WARN] Dynamic API Analyzer 失败: {e}")
278
+
279
+ def _hook_apis(self):
280
+ """API Hook (api_interceptor)"""
281
+ if not self.ctx.playwright_available:
282
+ print(" [SKIP] Playwright 不可用")
283
+ return
284
+
285
+ try:
286
+ from core.api_interceptor import APIInterceptor
287
+
288
+ print(" [API Hook] 启动...")
289
+ interceptor = APIInterceptor(self.target)
290
+ hook_results = interceptor.hook_all_apis()
291
+
292
+ # 保存 Hook 结果到上下文
293
+ self.ctx.hooked_apis = hook_results.get('endpoints', [])
294
+ self.ctx.sensitive_apis = hook_results.get('sensitive', [])
295
+ self.ctx.test_vectors = hook_results.get('test_vectors', [])
296
+
297
+ # 将 Hook 到的端点添加到上下文的端点列表
298
+ for hooked_ep in hook_results.get('endpoints', []):
299
+ path = hooked_ep.get('path', hooked_ep.get('url', ''))
300
+ if path and '/' in path:
301
+ self.ctx.add_endpoints([{
302
+ 'path': path,
303
+ 'method': hooked_ep.get('method', 'GET'),
304
+ 'params': hooked_ep.get('params', {}),
305
+ 'source': f"hooked_{hooked_ep.get('source', 'unknown')}",
306
+ 'semantic_type': hooked_ep.get('semantic', 'unknown'),
307
+ }])
308
+
309
+ print(f" API Hook: {len(self.ctx.hooked_apis)} 个 API 调用")
310
+ print(f" 敏感操作: {len(self.ctx.sensitive_apis)} 个")
311
+ print(f" 测试向量: {len(self.ctx.test_vectors)} 个")
312
+
313
+ except Exception as e:
314
+ print(f" [WARN] API Hook 失败: {e}")
315
+
316
+ def _probe_parent_paths(self):
317
+ """父路径探测"""
318
+ try:
319
+ from core.api_parser import APIEndpointParser
320
+
321
+ parser = APIEndpointParser(self.target, self.session)
322
+ parser.discover_js_files()
323
+
324
+ # 获取解析后的父路径(set 格式)
325
+ parsed_endpoints = parser.parse_js_files(self.js_files)
326
+
327
+ # 转换 set 为 dict 格式
328
+ for parent in parser.parent_paths:
329
+ url = self.target.rstrip('/') + parent
330
+ try:
331
+ r = self.session.get(url, timeout=5, allow_redirects=False)
332
+ self.ctx.parent_paths[parent] = {
333
+ 'path': parent,
334
+ 'status': r.status_code,
335
+ 'is_api': 'json' in r.headers.get('Content-Type', '').lower() or '{' in r.text[:100],
336
+ }
337
+ except:
338
+ pass
339
+
340
+ print(f" 父路径: {len(self.ctx.parent_paths)} 个")
341
+
342
+ except Exception as e:
343
+ print(f" [WARN] 父路径探测失败: {e}")
344
+
345
+
346
+ class VulnerabilityTester:
347
+ """阶段 2: 多维度漏洞分析"""
348
+
349
+ def __init__(self, ctx: TestContext):
350
+ self.ctx = ctx
351
+ self.target = ctx.target
352
+ self.session = ctx.session
353
+
354
+ def run(self) -> List:
355
+ """执行漏洞测试"""
356
+ print("[2] 多维度漏洞分析")
357
+ print("-" * 70)
358
+
359
+ # 使用上下文中所有端点(包括 Hook 到的)
360
+ self.endpoints = self.ctx.get_all_endpoints()
361
+
362
+ # 2.1 SQL 注入测试
363
+ self._test_sqli()
364
+
365
+ # 2.2 XSS 测试
366
+ self._test_xss()
367
+
368
+ # 2.3 路径遍历测试
369
+ self._test_path_traversal()
370
+
371
+ # 2.4 敏感信息泄露
372
+ self._test_sensitive_exposure()
373
+
374
+ # 2.5 认证绕过测试
375
+ self._test_auth_bypass()
376
+
377
+ # 2.6 GraphQL 测试 (如果发现 GraphQL 端点)
378
+ self._test_graphql()
379
+
380
+ # 2.7 暴力破解测试 (如果发现登录端点)
381
+ self._test_brute_force()
382
+
383
+ # 2.8 IDOR 测试
384
+ self._test_idor()
385
+
386
+ print(f"\n 发现漏洞: {len(self.ctx.vulnerabilities)}")
387
+ print()
388
+
389
+ return self.ctx.vulnerabilities
390
+
391
+ def _test_sqli(self):
392
+ """SQL 注入测试"""
393
+ print(" [SQL注入] 测试...")
394
+
395
+ sqli_payloads = [
396
+ "' OR '1'='1",
397
+ "' OR 1=1--",
398
+ "' UNION SELECT NULL--",
399
+ "admin'--",
400
+ ]
401
+
402
+ for ep in self.endpoints[:20]:
403
+ if ep.get('method') != 'GET':
404
+ continue
405
+
406
+ url = ep.get('url', self.target + ep.get('path', ''))
407
+ if '?' not in url:
408
+ url = url + '?id=1'
409
+
410
+ try:
411
+ for payload in sqli_payloads[:2]:
412
+ test_url = url.replace('id=1', f'id={payload}')
413
+ r = self.session.get(test_url, timeout=5)
414
+
415
+ # 跳过 HTML 响应(通常是 nginx fallback)
416
+ content_type = r.headers.get('Content-Type', '')
417
+ if 'text/html' in content_type or r.text.strip().startswith('<!DOCTYPE'):
418
+ continue
419
+
420
+ # 检查是否是 JSON 响应
421
+ if 'application/json' not in content_type and '{' not in r.text[:100]:
422
+ continue
423
+
424
+ # SQL 注入特征检测(排除假阳性)
425
+ text_lower = r.text.lower()
426
+ # 真正的 SQL 错误特征
427
+ sql_patterns = ['sql syntax', 'sql error', 'mysql', 'oracle', 'sqlite',
428
+ 'sqlstate', 'postgresql', 'sqlserver', 'column', 'table',
429
+ 'mysqli_', 'pdo_', 'odbc_']
430
+ if any(p in text_lower for p in sql_patterns):
431
+ self.ctx.add_vulnerability({
432
+ 'type': 'SQL Injection',
433
+ 'severity': 'CRITICAL',
434
+ 'endpoint': url,
435
+ 'payload': payload,
436
+ 'evidence': 'SQL error detected'
437
+ })
438
+ print(f" [!] {ep['path']}: SQL注入")
439
+ break
440
+ except:
441
+ pass
442
+
443
+ def _test_xss(self):
444
+ """XSS 测试"""
445
+ print(" [XSS] 测试...")
446
+
447
+ xss_payloads = [
448
+ '<script>alert(1)</script>',
449
+ '<img src=x onerror=alert(1)>',
450
+ '"><script>alert(1)</script>',
451
+ ]
452
+
453
+ for ep in self.endpoints[:20]:
454
+ if ep.get('method') != 'GET':
455
+ continue
456
+
457
+ url = ep.get('url', self.target + ep.get('path', ''))
458
+
459
+ try:
460
+ for payload in xss_payloads[:1]:
461
+ if '?' in url:
462
+ test_url = url + '&q=' + payload
463
+ else:
464
+ test_url = url + '?q=' + payload
465
+
466
+ r = self.session.get(test_url, timeout=5)
467
+
468
+ if payload in r.text:
469
+ self.ctx.add_vulnerability({
470
+ 'type': 'XSS (Reflected)',
471
+ 'severity': 'HIGH',
472
+ 'endpoint': url,
473
+ 'payload': payload,
474
+ 'evidence': 'Payload reflected'
475
+ })
476
+ print(f" [!] {ep['path']}: XSS")
477
+ break
478
+ except:
479
+ pass
480
+
481
+ def _test_path_traversal(self):
482
+ """路径遍历测试"""
483
+ print(" [路径遍历] 测试...")
484
+
485
+ pt_payloads = ['../../etc/passwd', '..\\..\\windows\\win.ini', '%2e%2e%2f%2e%2e%2fetc%2fpasswd']
486
+
487
+ for ep in self.endpoints[:10]:
488
+ url = ep.get('url', self.target + ep.get('path', ''))
489
+
490
+ try:
491
+ for payload in pt_payloads[:1]:
492
+ test_url = url + '/' + payload if url.endswith('/') else url + '/' + payload
493
+ r = self.session.get(test_url, timeout=5)
494
+
495
+ if 'root:' in r.text or '[extensions]' in r.text:
496
+ self.ctx.add_vulnerability({
497
+ 'type': 'Path Traversal',
498
+ 'severity': 'HIGH',
499
+ 'endpoint': url,
500
+ 'payload': payload,
501
+ 'evidence': 'Sensitive file content exposed'
502
+ })
503
+ print(f" [!] {ep['path']}: 路径遍历")
504
+ break
505
+ except:
506
+ pass
507
+
508
+ def _test_sensitive_exposure(self):
509
+ """敏感信息泄露测试"""
510
+ print(" [敏感信息] 检测...")
511
+
512
+ sensitive_patterns = [
513
+ ('password', 'password', 'MEDIUM'),
514
+ ('secret', 'api_key', 'HIGH'),
515
+ ('token', 'token', 'MEDIUM'),
516
+ ('private_key', 'private_key', 'CRITICAL'),
517
+ ]
518
+
519
+ for ep in self.endpoints[:30]:
520
+ url = ep.get('url', self.target + ep.get('path', ''))
521
+
522
+ try:
523
+ r = self.session.get(url, timeout=5)
524
+
525
+ for pattern, name, severity in sensitive_patterns:
526
+ if pattern in r.text.lower() and 'password' not in r.text.lower()[:500]:
527
+ # 避免误报,只在实际内容中检测
528
+ content_sample = r.text[:1000].lower()
529
+ if content_sample.count(pattern) > 2:
530
+ self.ctx.add_vulnerability({
531
+ 'type': 'Sensitive Data Exposure',
532
+ 'severity': severity,
533
+ 'endpoint': url,
534
+ 'evidence': f'{name} found in response',
535
+ })
536
+ print(f" [!] {ep['path']}: 敏感信息 ({name})")
537
+ break
538
+ except:
539
+ pass
540
+
541
+ def _test_auth_bypass(self):
542
+ """认证绕过测试"""
543
+ print(" [认证绕过] 测试...")
544
+
545
+ # 测试不需要认证就能访问的敏感端点
546
+ sensitive_paths = ['/admin', '/api/admin', '/api/users', '/api/config']
547
+
548
+ for path in sensitive_paths:
549
+ url = self.target.rstrip('/') + path
550
+ try:
551
+ r = self.session.get(url, timeout=5)
552
+
553
+ if r.status_code == 200 and len(r.text) > 100:
554
+ ct = r.headers.get('Content-Type', '')
555
+ if 'json' in ct.lower() or '{' in r.text[:100]:
556
+ self.ctx.add_vulnerability({
557
+ 'type': 'Authentication Bypass',
558
+ 'severity': 'HIGH',
559
+ 'endpoint': path,
560
+ 'evidence': f'No auth required, status: {r.status_code}'
561
+ })
562
+ print(f" [!] {path}: 无需认证")
563
+ except:
564
+ pass
565
+
566
+ def _test_graphql(self):
567
+ """GraphQL 测试"""
568
+ graphql_paths = ['/graphql', '/api/graphql', '/query']
569
+
570
+ for path in graphql_paths:
571
+ url = self.target.rstrip('/') + path
572
+ try:
573
+ r = self.session.post(url, json={'query': '{__schema{types{name}}}'}, timeout=5)
574
+
575
+ if r.status_code == 200 and 'data' in r.text:
576
+ self.ctx.add_vulnerability({
577
+ 'type': 'GraphQL Introspection Enabled',
578
+ 'severity': 'MEDIUM',
579
+ 'endpoint': path,
580
+ 'evidence': 'GraphQL schema exposed'
581
+ })
582
+ print(f" [!] {path}: GraphQL 开启 introspection")
583
+
584
+ # 检查 mutation
585
+ r2 = self.session.post(url, json={'query': 'mutation{__typename}'}, timeout=5)
586
+ if r2.status_code == 200:
587
+ print(f" [!] {path}: mutation 可用")
588
+ except:
589
+ pass
590
+
591
+ def _test_brute_force(self):
592
+ """暴力破解测试"""
593
+ login_paths = ['/login', '/api/login', '/auth/login', '/signin']
594
+
595
+ for path in login_paths:
596
+ url = self.target.rstrip('/') + path
597
+ try:
598
+ # 测试多次登录
599
+ for i in range(3):
600
+ r = self.session.post(url, json={'username': f'test{i}', 'password': 'wrong'}, timeout=5)
601
+
602
+ # 检查是否有 rate limit
603
+ if r.status_code in [200, 400, 401, 403]:
604
+ # 发送大量请求测试
605
+ for i in range(10):
606
+ r = self.session.post(url, json={'username': 'admin', 'password': 'test'}, timeout=5)
607
+
608
+ # 检查响应是否变化
609
+ if r.status_code != 429: # 没有 rate limit
610
+ self.ctx.add_vulnerability({
611
+ 'type': 'Brute Force Risk',
612
+ 'severity': 'MEDIUM',
613
+ 'endpoint': path,
614
+ 'evidence': 'No rate limiting detected'
615
+ })
616
+ print(f" [!] {path}: 无暴力破解防护")
617
+ except:
618
+ pass
619
+
620
+ def _test_idor(self):
621
+ """IDOR 测试"""
622
+ idor_paths = ['/user/1', '/users/1', '/profile/1', '/api/user/1']
623
+
624
+ for path in idor_paths:
625
+ url = self.target.rstrip('/') + path
626
+ try:
627
+ r = self.session.get(url, timeout=5)
628
+
629
+ if r.status_code == 200:
630
+ ct = r.headers.get('Content-Type', '')
631
+ if 'json' in ct.lower():
632
+ self.ctx.add_vulnerability({
633
+ 'type': 'Potential IDOR',
634
+ 'severity': 'MEDIUM',
635
+ 'endpoint': path,
636
+ 'evidence': 'Direct object reference without auth check'
637
+ })
638
+ print(f" [!] {path}: 可能的 IDOR")
639
+ break
640
+ except:
641
+ pass
642
+
643
+
644
+ class CloudStorageTester:
645
+ """阶段 3: 云存储安全测试"""
646
+
647
+ def __init__(self, target: str, session):
648
+ self.target = target
649
+ self.session = session
650
+ self.findings = []
651
+
652
+ def run(self) -> List:
653
+ """执行云存储测试"""
654
+ print("[3] 云存储安全测试")
655
+ print("-" * 70)
656
+
657
+ # 检查云存储特征
658
+ cloud_patterns = [
659
+ ('oss', 'aliyun'),
660
+ ('cos', 'qcloud'),
661
+ ('s3', 'aws'),
662
+ ('minio', 'minio'),
663
+ ('obs', 'huawei'),
664
+ ]
665
+
666
+ for keyword, provider in cloud_patterns:
667
+ try:
668
+ r = self.session.get(self.target, timeout=10)
669
+
670
+ if keyword in r.text.lower():
671
+ self.findings.append({
672
+ 'type': f'{provider.upper()} Storage',
673
+ 'severity': 'INFO',
674
+ 'endpoint': self.target,
675
+ 'evidence': f'Cloud storage keyword found: {keyword}'
676
+ })
677
+ print(f" [发现] {provider} 云存储特征")
678
+
679
+ # 检查响应头
680
+ for header in r.headers:
681
+ if keyword in header.lower():
682
+ print(f" [发现] {provider} header: {header}")
683
+ except:
684
+ pass
685
+
686
+ if not self.findings:
687
+ print(" 未发现云存储特征")
688
+
689
+ print()
690
+ return self.findings
691
+
692
+
693
+ class ReportGenerator:
694
+ """阶段 4: 报告生成"""
695
+
696
+ @staticmethod
697
+ def generate(results: Dict) -> str:
698
+ """生成 Markdown 报告"""
699
+
700
+ report = []
701
+ report.append("# API 安全测试报告")
702
+ report.append("")
703
+ report.append(f"**目标**: {results.get('target', 'N/A')}")
704
+ report.append(f"**时间**: {results.get('timestamp', 'N/A')}")
705
+ report.append(f"**耗时**: {results.get('duration', 0):.1f}s")
706
+ report.append("")
707
+
708
+ # 统计
709
+ report.append("## 发现统计")
710
+ report.append("")
711
+ report.append(f"- API 端点: {len(results.get('endpoints', []))}")
712
+ report.append(f"- 漏洞: {len(results.get('vulnerabilities', []))}")
713
+ report.append(f"- 云存储: {len(results.get('cloud_findings', []))}")
714
+ report.append("")
715
+
716
+ # 技术栈
717
+ if results.get('tech_stack'):
718
+ report.append("## 技术栈")
719
+ report.append("")
720
+ for key, value in results['tech_stack'].items():
721
+ report.append(f"- {key}: {value}")
722
+ report.append("")
723
+
724
+ # 父路径探测结果
725
+ if results.get('parent_paths'):
726
+ parent_paths = results['parent_paths']
727
+ html_fallback = sum(1 for p in parent_paths.values() if not p.get('is_api'))
728
+ real_api = sum(1 for p in parent_paths.values() if p.get('is_api'))
729
+
730
+ report.append("## 父路径分析")
731
+ report.append("")
732
+ report.append(f"- 总父路径: {len(parent_paths)}")
733
+ report.append(f"- HTML fallback: {html_fallback} (nginx fallback 配置)")
734
+ report.append(f"- JSON API: {real_api}")
735
+ report.append("")
736
+
737
+ # 检测 nginx fallback 问题
738
+ if html_fallback > 0 and real_api == 0:
739
+ report.append("### 安全问题: nginx fallback 配置")
740
+ report.append("")
741
+ report.append("**问题**: 所有 API 路径都返回 HTML 而不是 JSON API")
742
+ report.append("")
743
+ report.append("**可能原因**:")
744
+ report.append("1. 后端 API 服务未运行 (端口 667 不可达)")
745
+ report.append("2. nginx 未正确配置 API 路径代理")
746
+ report.append("3. API 服务部署在不同的服务器/端口")
747
+ report.append("")
748
+ report.append("**影响**: 前端无法连接后端 API,系统功能不可用")
749
+ report.append("")
750
+ report.append("**建议**:")
751
+ report.append("1. 检查后端服务是否运行")
752
+ report.append("2. 检查 nginx proxy_pass 配置")
753
+ report.append("3. 检查防火墙/安全组规则")
754
+ report.append("")
755
+
756
+ # 添加为安全问题
757
+ results['vulnerabilities'].insert(0, {
758
+ 'type': 'Backend API Unreachable / nginx fallback',
759
+ 'severity': 'HIGH',
760
+ 'endpoint': 'Multiple paths',
761
+ 'evidence': f'{html_fallback} paths return HTML fallback instead of JSON API'
762
+ })
763
+ elif real_api > 0:
764
+ report.append("**状态**: 发现可访问的 JSON API 端点")
765
+ report.append("")
766
+
767
+ # 漏洞详情
768
+ if results.get('vulnerabilities'):
769
+ report.append("## 漏洞详情")
770
+ report.append("")
771
+
772
+ # 按严重程度分组
773
+ severity_order = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO']
774
+ vulns_by_severity = {s: [] for s in severity_order}
775
+
776
+ for v in results['vulnerabilities']:
777
+ sev = v.get('severity', 'INFO').upper()
778
+ if sev in vulns_by_severity:
779
+ vulns_by_severity[sev].append(v)
780
+ else:
781
+ vulns_by_severity['INFO'].append(v)
782
+
783
+ for severity in severity_order:
784
+ vulns = vulns_by_severity[severity]
785
+ if vulns:
786
+ report.append(f"### {severity} ({len(vulns)})")
787
+ report.append("")
788
+ for v in vulns:
789
+ report.append(f"#### {v.get('type', 'Unknown')}")
790
+ report.append(f"- **端点**: {v.get('endpoint', 'N/A')}")
791
+ report.append(f"- **证据**: {v.get('evidence', 'N/A')}")
792
+ if v.get('payload'):
793
+ report.append(f"- **Payload**: `{v.get('payload')}`")
794
+ report.append("")
795
+
796
+ # 云存储发现
797
+ if results.get('cloud_findings'):
798
+ report.append("## 云存储发现")
799
+ report.append("")
800
+ for f in results['cloud_findings']:
801
+ report.append(f"- {f.get('type')}: {f.get('evidence')}")
802
+ report.append("")
803
+
804
+ # 端点列表
805
+ if results.get('endpoints'):
806
+ report.append("## API 端点列表")
807
+ report.append("")
808
+ report.append(f"共发现 {len(results['endpoints'])} 个端点")
809
+ report.append("")
810
+
811
+ for ep in results['endpoints'][:50]:
812
+ report.append(f"- `{ep.get('method', 'GET')}` {ep.get('path', ep.get('url', ''))} ({ep.get('source', '')})")
813
+
814
+ if len(results['endpoints']) > 50:
815
+ report.append(f"- ... 还有 {len(results['endpoints']) - 50} 个端点")
816
+ report.append("")
817
+
818
+ return "\n".join(report)
819
+
820
+
821
+ def run_skill(target: str) -> Dict:
822
+ """
823
+ 执行完整的 SKILL.md 测试流程
824
+
825
+ 使用 TestContext 在模块间共享数据,实现模块联动
826
+ """
827
+ print("=" * 70)
828
+ print(" API Security Testing Skill")
829
+ print("=" * 70)
830
+ print()
831
+
832
+ # 创建测试上下文
833
+ ctx = TestContext(target=target)
834
+
835
+ # 阶段 0: 前置检查
836
+ if not PrerequisiteChecker.check(ctx):
837
+ print("[FATAL] 前置检查失败")
838
+ return {'error': '前置检查失败', 'target': target}
839
+
840
+ start_time = time.time()
841
+
842
+ # 阶段 1: 资产发现 (联动)
843
+ discovery = AssetDiscovery(ctx)
844
+ discovery.run()
845
+
846
+ # 阶段 2: 漏洞分析
847
+ print("[2] 漏洞分析")
848
+ print("-" * 70)
849
+ tester = VulnerabilityTester(ctx)
850
+ tester.run()
851
+
852
+ # 阶段 2.5: Fuzzing
853
+ print("[2.5] API Fuzzing")
854
+ print("-" * 70)
855
+ _run_fuzzing(ctx)
856
+
857
+ # 阶段 3: 云存储测试
858
+ print("[3] 云存储测试")
859
+ print("-" * 70)
860
+ cloud_tester = CloudStorageTester(ctx.target, ctx.session)
861
+ ctx.cloud_findings = cloud_tester.run()
862
+
863
+ # 更新上下文时间
864
+ ctx.duration = time.time() - start_time
865
+
866
+ # 阶段 4: 报告生成
867
+ print("[4] 生成报告")
868
+ print("-" * 70)
869
+
870
+ report = ReportGenerator.generate(ctx.__dict__)
871
+ print(report)
872
+
873
+ report_file = f"security_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
874
+ with open(report_file, 'w', encoding='utf-8') as f:
875
+ f.write(report)
876
+
877
+ print(f"\n报告已保存: {report_file}")
878
+
879
+ return vars(ctx)
880
+
881
+
882
+ def _run_fuzzing(ctx: TestContext):
883
+ """执行 Fuzzing(使用上下文)"""
884
+ try:
885
+ from core.api_parser import APIFuzzer, ParsedEndpoint, APIParam, ParamType, ParamLocation
886
+
887
+ parsed_eps = []
888
+ for ep_data in ctx.get_all_endpoints():
889
+ ep = ParsedEndpoint(
890
+ path=ep_data.get('path', ''),
891
+ method=ep_data.get('method', 'GET'),
892
+ source=ep_data.get('source', ''),
893
+ semantic_type=ep_data.get('semantic_type', ''),
894
+ )
895
+ params = ep_data.get('params', {})
896
+ if isinstance(params, dict):
897
+ for p_name, p_val in params.items():
898
+ ep.params.append(APIParam(
899
+ name=p_name,
900
+ param_type=ParamType.QUERY,
901
+ location=ParamLocation.URL,
902
+ required=False,
903
+ ))
904
+ elif isinstance(params, list):
905
+ for p in params:
906
+ if isinstance(p, dict) and 'name' in p:
907
+ ep.params.append(APIParam(
908
+ name=p['name'],
909
+ param_type=ParamType.QUERY,
910
+ location=ParamLocation.URL,
911
+ required=p.get('required', False),
912
+ ))
913
+ parsed_eps.append(ep)
914
+
915
+ fuzzer = APIFuzzer(ctx.target, ctx.session)
916
+ fuzz_results = fuzzer.fuzz_endpoints(parsed_eps, ctx.parent_paths)
917
+
918
+ for vuln in fuzz_results:
919
+ ctx.add_vulnerability(vuln)
920
+
921
+ print(f" [Fuzzing] 发现 {len(fuzz_results)} 个问题")
922
+
923
+ except Exception as e:
924
+ print(f" [WARN] Fuzzing 失败: {e}")
925
+
926
+
927
+ if __name__ == "__main__":
928
+ import argparse
929
+
930
+ parser = argparse.ArgumentParser(description='API Security Testing Skill')
931
+ parser.add_argument('target', help='目标 URL')
932
+ args = parser.parse_args()
933
+
934
+ target = args.target
935
+ if not target.startswith('http'):
936
+ target = 'http://' + target
937
+
938
+ run_skill(target)