opencode-api-security-testing 3.0.10 → 3.0.11
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/README.md +74 -0
- package/SKILL.md +1797 -0
- package/core/advanced_recon.py +788 -0
- package/core/agentic_analyzer.py +445 -0
- package/core/analyzers/api_parser.py +210 -0
- package/core/analyzers/response_analyzer.py +212 -0
- package/core/analyzers/sensitive_finder.py +184 -0
- package/core/api_fuzzer.py +422 -0
- package/core/api_interceptor.py +525 -0
- package/core/api_parser.py +955 -0
- package/core/browser_tester.py +479 -0
- package/core/cloud_storage_tester.py +1330 -0
- package/core/collectors/__init__.py +23 -0
- package/core/collectors/api_path_finder.py +300 -0
- package/core/collectors/browser_collect.py +645 -0
- package/core/collectors/browser_collector.py +411 -0
- package/core/collectors/http_client.py +111 -0
- package/core/collectors/js_collector.py +490 -0
- package/core/collectors/js_parser.py +780 -0
- package/core/collectors/url_collector.py +319 -0
- package/core/context_manager.py +682 -0
- package/core/deep_api_tester_v35.py +844 -0
- package/core/deep_api_tester_v55.py +366 -0
- package/core/dynamic_api_analyzer.py +532 -0
- package/core/http_client.py +179 -0
- package/core/models.py +296 -0
- package/core/orchestrator.py +890 -0
- package/core/prerequisite.py +227 -0
- package/core/reasoning_engine.py +1042 -0
- package/core/response_classifier.py +606 -0
- package/core/runner.py +938 -0
- package/core/scan_engine.py +599 -0
- package/core/skill_executor.py +435 -0
- package/core/skill_executor_v2.py +670 -0
- package/core/skill_executor_v3.py +704 -0
- package/core/smart_analyzer.py +687 -0
- package/core/strategy_pool.py +707 -0
- package/core/testers/auth_tester.py +264 -0
- package/core/testers/idor_tester.py +200 -0
- package/core/testers/sqli_tester.py +211 -0
- package/core/testing_loop.py +655 -0
- package/core/utils/base_path_dict.py +255 -0
- package/core/utils/payload_lib.py +167 -0
- package/core/utils/ssrf_detector.py +220 -0
- package/core/verifiers/vuln_verifier.py +536 -0
- package/package.json +1 -1
- package/references/README.md +72 -0
- package/references/asset-discovery.md +119 -0
- package/references/fuzzing-patterns.md +129 -0
- package/references/graphql-guidance.md +108 -0
- package/references/intake.md +84 -0
- package/references/pua-agent.md +192 -0
- package/references/report-template.md +156 -0
- package/references/rest-guidance.md +76 -0
- package/references/severity-model.md +76 -0
- package/references/test-matrix.md +86 -0
- package/references/validation.md +78 -0
- package/references/vulnerabilities/01-sqli-tests.md +1128 -0
- package/references/vulnerabilities/02-user-enum-tests.md +423 -0
- package/references/vulnerabilities/03-jwt-tests.md +499 -0
- package/references/vulnerabilities/04-idor-tests.md +362 -0
- package/references/vulnerabilities/05-sensitive-data-tests.md +466 -0
- package/references/vulnerabilities/06-biz-logic-tests.md +501 -0
- package/references/vulnerabilities/07-security-config-tests.md +511 -0
- package/references/vulnerabilities/08-brute-force-tests.md +457 -0
- package/references/vulnerabilities/09-vulnerability-chains.md +465 -0
- package/references/vulnerabilities/10-auth-tests.md +537 -0
- package/references/vulnerabilities/11-graphql-tests.md +355 -0
- package/references/vulnerabilities/12-ssrf-tests.md +396 -0
- package/references/vulnerabilities/README.md +148 -0
- package/references/workflows.md +192 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Collectors Package - 信息采集模块
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .js_collector import JSCollector, JSFingerprintCache, ParsedJSResult
|
|
6
|
+
from .api_path_finder import ApiPathFinder, ApiPathCombiner, APIFindResult
|
|
7
|
+
from .url_collector import URLCollector, DomainURLCollector, URLCollectionResult
|
|
8
|
+
from .browser_collector import HeadlessBrowserCollector, BrowserCollectorFacade, BrowserCollectionResult
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'JSCollector',
|
|
12
|
+
'JSFingerprintCache',
|
|
13
|
+
'ParsedJSResult',
|
|
14
|
+
'ApiPathFinder',
|
|
15
|
+
'ApiPathCombiner',
|
|
16
|
+
'APIFindResult',
|
|
17
|
+
'URLCollector',
|
|
18
|
+
'DomainURLCollector',
|
|
19
|
+
'URLCollectionResult',
|
|
20
|
+
'HeadlessBrowserCollector',
|
|
21
|
+
'BrowserCollectorFacade',
|
|
22
|
+
'BrowserCollectionResult',
|
|
23
|
+
]
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
API Path Finder - API 路径发现器
|
|
4
|
+
使用 25+ 正则规则从 JS/HTML/API 响应中提取 API 路径
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import List, Dict, Set, Tuple
|
|
9
|
+
from urllib.parse import urljoin, urlparse
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class APIFindResult:
|
|
15
|
+
"""API 发现结果"""
|
|
16
|
+
path: str
|
|
17
|
+
method: str = "GET"
|
|
18
|
+
source_type: str = ""
|
|
19
|
+
url_type: str = ""
|
|
20
|
+
parameters: Set[str] = field(default_factory=set)
|
|
21
|
+
full_url: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApiPathFinder:
|
|
25
|
+
"""
|
|
26
|
+
API 路径发现器
|
|
27
|
+
|
|
28
|
+
功能:
|
|
29
|
+
- 25+ 正则规则发现 API 路径
|
|
30
|
+
- 智能路径组合
|
|
31
|
+
- 父路径探测
|
|
32
|
+
- 跨来源 Fuzzing
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# 常见 RESTful 后缀
|
|
36
|
+
RESTFUL_SUFFIXES = [
|
|
37
|
+
'list', 'get', 'add', 'create', 'update', 'edit', 'delete', 'remove',
|
|
38
|
+
'detail', 'info', 'view', 'show', 'query', 'search', 'fetch', 'load',
|
|
39
|
+
'save', 'submit', 'submit', 'export', 'import', 'upload', 'download',
|
|
40
|
+
'config', 'setting', 'settings', 'options', 'permissions', 'all',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# 常见参数名
|
|
44
|
+
COMMON_PARAMS = [
|
|
45
|
+
'id', 'page', 'pageNum', 'pageSize', 'page_size', 'num', 'size',
|
|
46
|
+
'limit', 'offset', 'start', 'end', 'from', 'to', 'date', 'time',
|
|
47
|
+
'type', 'category', 'status', 'state', 'flag', 'mode', 'action',
|
|
48
|
+
'name', 'title', 'desc', 'description', 'content', 'text', 'data',
|
|
49
|
+
'token', 'key', 'secret', 'email', 'phone', 'mobile', 'username',
|
|
50
|
+
'userId', 'user_id', 'userid', 'role', 'permission', 'menu',
|
|
51
|
+
'search', 'query', 'keyword', 'filter', 'sort', 'order', 'by',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# API 路径正则 (25+ 规则)
|
|
55
|
+
API_PATTERNS = [
|
|
56
|
+
# fetch
|
|
57
|
+
(r"fetch\s*\(\s*['\"]([^'\"]+)['\"]", "GET"),
|
|
58
|
+
(r"fetch\s*\(\s*[`'\"]([^`'\"]+)[`'\"]", "GET"),
|
|
59
|
+
|
|
60
|
+
# axios
|
|
61
|
+
(r"axios\.(get|post|put|delete|patch|head)\s*\(\s*['\"]([^'\"]+)['\"]", None),
|
|
62
|
+
(r"axios\.(get|post|put|delete|patch|head)\s*\(\s*[`'\"]([`'\"]+)[`'\"]", None),
|
|
63
|
+
|
|
64
|
+
# $.ajax
|
|
65
|
+
(r"\$\.ajax\s*\(\s*\{[^}]*url\s*:\s*['\"](/[^'\"]+)['\"]", "GET"),
|
|
66
|
+
(r"\$\.ajax\s*\(\s*\{[^}]*type\s*:\s*['\"]([^'\"]+)['\"]", None),
|
|
67
|
+
|
|
68
|
+
# request
|
|
69
|
+
(r"request\s*\(\s*\{[^}]*url\s*:\s*['\"](/[^'\"]+)['\"]", None),
|
|
70
|
+
(r"request\s*\(\s*['\"]([^'\"]+)['\"]", "GET"),
|
|
71
|
+
|
|
72
|
+
# 直接路径匹配
|
|
73
|
+
(r"['\"](/api/[a-zA-Z0-9_/-]+)['\"]", "GET"),
|
|
74
|
+
(r"['\"](\/v\d+/[a-zA-Z0-9_/-]+)['\"]", "GET"),
|
|
75
|
+
(r"['\"](\/[a-zA-Z]+/[a-zA-Z0-9_/-]+)['\"]", "GET"),
|
|
76
|
+
|
|
77
|
+
# RESTful 模板
|
|
78
|
+
(r"['\"](/[a-zA-Z]+/\{[a-zA-Z_][a-zA-Z0-9_]*\})['\"]", "GET"),
|
|
79
|
+
(r"['\"](/[a-zA-Z]+/[a-zA-Z]+/\{[a-zA-Z_][a-zA-Z0-9_]*\})['\"]", "GET"),
|
|
80
|
+
|
|
81
|
+
# WebSocket
|
|
82
|
+
(r"new\s+WebSocket\s*\(\s*['\"]([^'\"]+)['\"]", "WS"),
|
|
83
|
+
(r"wss?://[^\s'\"<>]+", "WS"),
|
|
84
|
+
|
|
85
|
+
# GraphQL
|
|
86
|
+
(r"graphql\s*\(\s*\{[^}]*query\s*:\s*['\"]([^'\"]+)['\"]", "POST"),
|
|
87
|
+
(r"gql\s*`[^`]+query\s+(\w+)", "POST"),
|
|
88
|
+
|
|
89
|
+
# 相对路径
|
|
90
|
+
(r"\.\s*\+\s*['\"](/[^'\"]+)['\"]", "GET"),
|
|
91
|
+
(r"baseURL\s*\+\s*['\"](/[^'\"]+)['\"]", "GET"),
|
|
92
|
+
|
|
93
|
+
# JSON 数据中的路径
|
|
94
|
+
(r"\"url\"\s*:\s*\"(/[^\"]+)\"", "GET"),
|
|
95
|
+
(r"\"path\"\s*:\s*\"(/[^\"]+)\"", "GET"),
|
|
96
|
+
(r"\"endpoint\"\s*:\s*\"(/[^\"]+)\"", "GET"),
|
|
97
|
+
(r"\"uri\"\s*:\s*\"(/[^\"]+)\"", "GET"),
|
|
98
|
+
|
|
99
|
+
# 完整 URL
|
|
100
|
+
(r"https?://[a-zA-Z0-9.-]+(:\d+)?(/[a-zA-Z0-9_/.-]*)?['\"]", "GET"),
|
|
101
|
+
|
|
102
|
+
# Vue Router
|
|
103
|
+
(r"path\s*:\s*['\"](/[^'\"]+)['\"]", "GET"),
|
|
104
|
+
(r"router\.push\s*\(\s*['\"](/[^'\"]+)['\"]", "GET"),
|
|
105
|
+
(r"router\.replace\s*\(\s*['\"](/[^'\"]+)['\"]", "GET"),
|
|
106
|
+
|
|
107
|
+
# React Router
|
|
108
|
+
(r"<Route\s+[^>]*path=['\"](/[^'\"]+)['\"]", "GET"),
|
|
109
|
+
(r"Link\s+[^>]*to=['\"](/[^'\"]+)['\"]", "GET"),
|
|
110
|
+
|
|
111
|
+
# 路径参数
|
|
112
|
+
(r":([a-zA-Z_][a-zA-Z0-9_]*)\s*[,})]", ""),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
def __init__(self):
|
|
116
|
+
self.found_paths: Set[str] = set()
|
|
117
|
+
self.found_apis: List[APIFindResult] = []
|
|
118
|
+
|
|
119
|
+
def find_api_paths_in_text(self, text: str, source: str = "text") -> List[APIFindResult]:
|
|
120
|
+
"""从文本中发现 API 路径"""
|
|
121
|
+
results = []
|
|
122
|
+
|
|
123
|
+
for pattern, default_method in self.API_PATTERNS:
|
|
124
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
125
|
+
for match in matches:
|
|
126
|
+
if isinstance(match, tuple):
|
|
127
|
+
if len(match) == 2:
|
|
128
|
+
method = match[0].upper() if match[0].lower() in ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] else default_method or "GET"
|
|
129
|
+
path = match[1]
|
|
130
|
+
else:
|
|
131
|
+
method = default_method or "GET"
|
|
132
|
+
path = match[0]
|
|
133
|
+
else:
|
|
134
|
+
method = default_method or "GET"
|
|
135
|
+
path = match
|
|
136
|
+
|
|
137
|
+
if self._is_valid_path(path):
|
|
138
|
+
full_path = self._normalize_path(path)
|
|
139
|
+
if full_path and full_path not in self.found_paths:
|
|
140
|
+
self.found_paths.add(full_path)
|
|
141
|
+
|
|
142
|
+
api_result = APIFindResult(
|
|
143
|
+
path=full_path,
|
|
144
|
+
method=method,
|
|
145
|
+
source_type=source,
|
|
146
|
+
url_type="discovered"
|
|
147
|
+
)
|
|
148
|
+
results.append(api_result)
|
|
149
|
+
self.found_apis.append(api_result)
|
|
150
|
+
|
|
151
|
+
return results
|
|
152
|
+
|
|
153
|
+
def _is_valid_path(self, path: str) -> bool:
|
|
154
|
+
"""验证路径是否有效"""
|
|
155
|
+
if not path or len(path) < 2:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
if path.startswith('//') or path.startswith('http'):
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
skip_patterns = [
|
|
162
|
+
r'\.css', r'\.jpg', r'\.png', r'\.gif', r'\.svg', r'\.ico',
|
|
163
|
+
r'\.woff', r'\.ttf', r'\.eot', r'\.map$', r'\.js$',
|
|
164
|
+
r'github\.com', r'cdn\.', r'googleapis\.com',
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
for pattern in skip_patterns:
|
|
168
|
+
if re.search(pattern, path, re.IGNORECASE):
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def _normalize_path(self, path: str) -> str:
|
|
174
|
+
"""规范化路径"""
|
|
175
|
+
path = path.strip()
|
|
176
|
+
|
|
177
|
+
if not path.startswith('/') and not path.startswith('http'):
|
|
178
|
+
path = '/' + path
|
|
179
|
+
|
|
180
|
+
path = re.sub(r'["\']\s*\+\s*["\']', '', path)
|
|
181
|
+
|
|
182
|
+
path = re.sub(r'\{[^}]+\}', '{param}', path)
|
|
183
|
+
|
|
184
|
+
return path
|
|
185
|
+
|
|
186
|
+
def get_all_paths(self) -> List[str]:
|
|
187
|
+
"""获取所有发现的路径"""
|
|
188
|
+
return list(self.found_paths)
|
|
189
|
+
|
|
190
|
+
def get_parent_paths(self) -> Set[str]:
|
|
191
|
+
"""获取所有父路径"""
|
|
192
|
+
parent_paths = set()
|
|
193
|
+
|
|
194
|
+
for path in self.found_paths:
|
|
195
|
+
parts = path.strip('/').split('/')
|
|
196
|
+
if len(parts) > 1:
|
|
197
|
+
for i in range(1, len(parts)):
|
|
198
|
+
parent = '/' + '/'.join(parts[:i])
|
|
199
|
+
parent_paths.add(parent)
|
|
200
|
+
|
|
201
|
+
return parent_paths
|
|
202
|
+
|
|
203
|
+
def get_resource_names(self) -> Set[str]:
|
|
204
|
+
"""获取所有资源名"""
|
|
205
|
+
resources = set()
|
|
206
|
+
|
|
207
|
+
for path in self.found_paths:
|
|
208
|
+
parts = path.strip('/').split('/')
|
|
209
|
+
for part in parts:
|
|
210
|
+
if part not in ['api', 'v1', 'v2', 'v3', 'rest']:
|
|
211
|
+
if not part.startswith('{') and not part.startswith(':'):
|
|
212
|
+
if len(part) > 1 and not part.isdigit():
|
|
213
|
+
resources.add(part)
|
|
214
|
+
|
|
215
|
+
return resources
|
|
216
|
+
|
|
217
|
+
def combine_paths(self, base_paths: Set[str], suffixes: List[str]) -> List[str]:
|
|
218
|
+
"""组合路径: 父路径 + 后缀"""
|
|
219
|
+
combined = []
|
|
220
|
+
|
|
221
|
+
for base in base_paths:
|
|
222
|
+
for suffix in suffixes[:20]:
|
|
223
|
+
path = f"{base}/{suffix}"
|
|
224
|
+
if path not in self.found_paths:
|
|
225
|
+
combined.append(path)
|
|
226
|
+
|
|
227
|
+
return combined
|
|
228
|
+
|
|
229
|
+
def generate_fuzz_targets(self, parent_paths: Set[str], resources: Set[str]) -> List[str]:
|
|
230
|
+
"""生成 Fuzz 目标"""
|
|
231
|
+
targets = []
|
|
232
|
+
|
|
233
|
+
for parent in parent_paths:
|
|
234
|
+
targets.append(parent)
|
|
235
|
+
|
|
236
|
+
for suffix in self.RESTFUL_SUFFIXES[:15]:
|
|
237
|
+
path = f"{parent}/{suffix}"
|
|
238
|
+
if path not in self.found_paths:
|
|
239
|
+
targets.append(path)
|
|
240
|
+
|
|
241
|
+
for resource in list(resources)[:10]:
|
|
242
|
+
path = f"{parent}/{resource}"
|
|
243
|
+
if path not in self.found_paths:
|
|
244
|
+
targets.append(path)
|
|
245
|
+
|
|
246
|
+
for suffix in self.RESTFUL_SUFFIXES[:10]:
|
|
247
|
+
path = f"{parent}/{resource}/{suffix}"
|
|
248
|
+
if path not in self.found_paths:
|
|
249
|
+
targets.append(path)
|
|
250
|
+
|
|
251
|
+
return targets[:200]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ApiPathCombiner:
|
|
255
|
+
"""API 路径组合器 - 跨来源智能路径组合"""
|
|
256
|
+
|
|
257
|
+
def __init__(self):
|
|
258
|
+
self.path_segments: Set[str] = set()
|
|
259
|
+
self.base_urls: Set[str] = set()
|
|
260
|
+
|
|
261
|
+
def add_path_segment(self, segment: str):
|
|
262
|
+
"""添加路径片段"""
|
|
263
|
+
if segment and len(segment) > 1:
|
|
264
|
+
self.path_segments.add(segment)
|
|
265
|
+
|
|
266
|
+
def add_base_url(self, url: str):
|
|
267
|
+
"""添加 Base URL"""
|
|
268
|
+
parsed = urlparse(url)
|
|
269
|
+
base = f"{parsed.scheme}://{parsed.netloc}"
|
|
270
|
+
self.base_urls.add(base)
|
|
271
|
+
|
|
272
|
+
path = parsed.path
|
|
273
|
+
if path:
|
|
274
|
+
parts = path.strip('/').split('/')
|
|
275
|
+
for part in parts:
|
|
276
|
+
if part:
|
|
277
|
+
self.path_segments.add(part)
|
|
278
|
+
|
|
279
|
+
def combine_cross_source(self, html_paths: List[str], js_paths: List[str], api_paths: List[str]) -> List[str]:
|
|
280
|
+
"""跨来源组合路径"""
|
|
281
|
+
all_segments: Set[str] = set()
|
|
282
|
+
|
|
283
|
+
for path in html_paths + js_paths + api_paths:
|
|
284
|
+
parts = path.strip('/').split('/')
|
|
285
|
+
for part in parts:
|
|
286
|
+
if part and not part.startswith('{') and not part.isdigit():
|
|
287
|
+
all_segments.add(part)
|
|
288
|
+
|
|
289
|
+
combined = []
|
|
290
|
+
|
|
291
|
+
common_prefixes = ['/api', '/v1', '/v2', '/admin', '/user', '/auth', '/service']
|
|
292
|
+
|
|
293
|
+
for prefix in common_prefixes:
|
|
294
|
+
for segment in list(all_segments)[:30]:
|
|
295
|
+
if segment not in common_prefixes:
|
|
296
|
+
path = f"{prefix}/{segment}"
|
|
297
|
+
if path not in combined:
|
|
298
|
+
combined.append(path)
|
|
299
|
+
|
|
300
|
+
return combined[:100]
|