opencode-api-security-testing 2.1.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.
- 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 +17 -13
- package/references/asset-discovery.md +119 -612
- package/references/graphql-guidance.md +65 -641
- package/references/intake.md +84 -0
- package/references/report-template.md +131 -38
- package/references/rest-guidance.md +55 -526
- package/references/severity-model.md +52 -264
- package/references/test-matrix.md +65 -263
- package/references/validation.md +53 -400
- package/scripts/postinstall.js +46 -0
- package/agents/cyber-supervisor.md +0 -55
- package/agents/probing-miner.md +0 -42
- package/agents/resource-specialist.md +0 -31
- package/commands/api-security-testing-scan.md +0 -59
- package/commands/api-security-testing-test.md +0 -49
- package/commands/api-security-testing.md +0 -72
- package/tsconfig.json +0 -17
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Context Manager - 上下文管理器
|
|
4
|
+
|
|
5
|
+
维护和管理全维度上下文:
|
|
6
|
+
- TechStackContext: 技术栈上下文
|
|
7
|
+
- NetworkContext: 网络环境上下文
|
|
8
|
+
- SecurityContext: 安全态势上下文
|
|
9
|
+
- ContentContext: 内容特征上下文
|
|
10
|
+
- GlobalContext: 全局上下文
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import pickle
|
|
15
|
+
import hashlib
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Dict, List, Set, Optional, Any, Callable
|
|
18
|
+
from dataclasses import dataclass, field, asdict
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RateLimitStatus(Enum):
|
|
27
|
+
"""速率限制状态"""
|
|
28
|
+
NORMAL = "normal"
|
|
29
|
+
WARNING = "warning"
|
|
30
|
+
RATE_LIMITED = "rate_limited"
|
|
31
|
+
BLOCKED = "blocked"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExposureLevel(Enum):
|
|
35
|
+
"""暴露等级"""
|
|
36
|
+
INTERNAL = "internal"
|
|
37
|
+
PARTNER = "partner"
|
|
38
|
+
PUBLIC = "public"
|
|
39
|
+
UNKNOWN = "unknown"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DataClassification(Enum):
|
|
43
|
+
"""数据分类"""
|
|
44
|
+
PUBLIC = "public"
|
|
45
|
+
INTERNAL = "internal"
|
|
46
|
+
CONFIDENTIAL = "confidential"
|
|
47
|
+
RESTRICTED = "restricted"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestPhase(Enum):
|
|
51
|
+
"""测试阶段"""
|
|
52
|
+
INIT = "init"
|
|
53
|
+
RECON = "recon"
|
|
54
|
+
DISCOVERY = "discovery"
|
|
55
|
+
CLASSIFICATION = "classification"
|
|
56
|
+
FUZZING = "fuzzing"
|
|
57
|
+
TESTING = "testing"
|
|
58
|
+
REPORT = "report"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ProxyConfig:
|
|
63
|
+
"""代理配置"""
|
|
64
|
+
http_proxy: Optional[str] = None
|
|
65
|
+
https_proxy: Optional[str] = None
|
|
66
|
+
no_proxy: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict:
|
|
69
|
+
return {
|
|
70
|
+
'http_proxy': self.http_proxy,
|
|
71
|
+
'https_proxy': self.https_proxy,
|
|
72
|
+
'no_proxy': self.no_proxy
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class TechStackContext:
|
|
78
|
+
"""技术栈上下文"""
|
|
79
|
+
frontend: Set[str] = field(default_factory=set)
|
|
80
|
+
backend: Set[str] = field(default_factory=set)
|
|
81
|
+
database: Set[str] = field(default_factory=set)
|
|
82
|
+
api_type: Set[str] = field(default_factory=set)
|
|
83
|
+
waf: Optional[str] = None
|
|
84
|
+
cdn: Optional[str] = None
|
|
85
|
+
|
|
86
|
+
confidence: Dict[str, float] = field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> Dict:
|
|
89
|
+
return {
|
|
90
|
+
'frontend': list(self.frontend),
|
|
91
|
+
'backend': list(self.backend),
|
|
92
|
+
'database': list(self.database),
|
|
93
|
+
'api_type': list(self.api_type),
|
|
94
|
+
'waf': self.waf,
|
|
95
|
+
'cdn': self.cdn,
|
|
96
|
+
'confidence': self.confidence
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def is_empty(self) -> bool:
|
|
100
|
+
return not (self.frontend or self.backend or self.database or self.api_type)
|
|
101
|
+
|
|
102
|
+
def get_primary_stack(self) -> Optional[str]:
|
|
103
|
+
if self.backend:
|
|
104
|
+
return list(self.backend)[0]
|
|
105
|
+
if self.frontend:
|
|
106
|
+
return list(self.frontend)[0]
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class NetworkContext:
|
|
112
|
+
"""网络环境上下文"""
|
|
113
|
+
is_reachable: bool = True
|
|
114
|
+
requires_proxy: bool = False
|
|
115
|
+
proxy_config: Optional[ProxyConfig] = None
|
|
116
|
+
rate_limit_status: RateLimitStatus = RateLimitStatus.NORMAL
|
|
117
|
+
blocked_count: int = 0
|
|
118
|
+
consecutive_failures: int = 0
|
|
119
|
+
dns_resolution: Optional[str] = None
|
|
120
|
+
|
|
121
|
+
last_request_time: Optional[datetime] = None
|
|
122
|
+
last_failure_time: Optional[datetime] = None
|
|
123
|
+
|
|
124
|
+
user_agents: List[str] = field(default_factory=list)
|
|
125
|
+
current_user_agent: str = "Mozilla/5.0 (compatible; SecurityTesting/2.0)"
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> Dict:
|
|
128
|
+
return {
|
|
129
|
+
'is_reachable': self.is_reachable,
|
|
130
|
+
'requires_proxy': self.requires_proxy,
|
|
131
|
+
'proxy_config': self.proxy_config.to_dict() if self.proxy_config else None,
|
|
132
|
+
'rate_limit_status': self.rate_limit_status.value,
|
|
133
|
+
'blocked_count': self.blocked_count,
|
|
134
|
+
'consecutive_failures': self.consecutive_failures,
|
|
135
|
+
'dns_resolution': self.dns_resolution,
|
|
136
|
+
'current_user_agent': self.current_user_agent
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class SecurityContext:
|
|
142
|
+
"""安全态势上下文"""
|
|
143
|
+
auth_required: bool = False
|
|
144
|
+
auth_type: Optional[str] = None
|
|
145
|
+
auth_endpoints: Set[str] = field(default_factory=set)
|
|
146
|
+
sensitive_endpoints: Set[str] = field(default_factory=set)
|
|
147
|
+
exposure_level: ExposureLevel = ExposureLevel.UNKNOWN
|
|
148
|
+
data_classification: DataClassification = DataClassification.INTERNAL
|
|
149
|
+
|
|
150
|
+
session_tokens: List[str] = field(default_factory=list)
|
|
151
|
+
jwt_algorithms: List[str] = field(default_factory=list)
|
|
152
|
+
api_keys: List[str] = field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
def to_dict(self) -> Dict:
|
|
155
|
+
return {
|
|
156
|
+
'auth_required': self.auth_required,
|
|
157
|
+
'auth_type': self.auth_type,
|
|
158
|
+
'auth_endpoints': list(self.auth_endpoints),
|
|
159
|
+
'sensitive_endpoints': list(self.sensitive_endpoints),
|
|
160
|
+
'exposure_level': self.exposure_level.value,
|
|
161
|
+
'data_classification': self.data_classification.value
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def is_sensitive_endpoint(self, url: str) -> bool:
|
|
165
|
+
url_lower = url.lower()
|
|
166
|
+
sensitive_patterns = ['/admin', '/login', '/password', '/pay', '/order',
|
|
167
|
+
'/checkout', '/transfer', '/delete', '/config']
|
|
168
|
+
return any(pattern in url_lower for pattern in sensitive_patterns)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class ContentContext:
|
|
173
|
+
"""内容特征上下文"""
|
|
174
|
+
is_spa: bool = False
|
|
175
|
+
has_api_docs: bool = False
|
|
176
|
+
swagger_urls: List[str] = field(default_factory=list)
|
|
177
|
+
error_leaks: List[str] = field(default_factory=list)
|
|
178
|
+
base_urls: Set[str] = field(default_factory=set)
|
|
179
|
+
internal_ips: Set[str] = field(default_factory=set)
|
|
180
|
+
|
|
181
|
+
response_pattern: str = "normal"
|
|
182
|
+
spa_fallback_size: Optional[int] = None
|
|
183
|
+
|
|
184
|
+
js_urls: List[str] = field(default_factory=list)
|
|
185
|
+
api_paths: List[str] = field(default_factory=list)
|
|
186
|
+
|
|
187
|
+
def to_dict(self) -> Dict:
|
|
188
|
+
return {
|
|
189
|
+
'is_spa': self.is_spa,
|
|
190
|
+
'has_api_docs': self.has_api_docs,
|
|
191
|
+
'swagger_urls': self.swagger_urls,
|
|
192
|
+
'error_leaks': self.error_leaks,
|
|
193
|
+
'base_urls': list(self.base_urls),
|
|
194
|
+
'internal_ips': list(self.internal_ips),
|
|
195
|
+
'response_pattern': self.response_pattern,
|
|
196
|
+
'spa_fallback_size': self.spa_fallback_size
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class Endpoint:
|
|
202
|
+
"""API 端点"""
|
|
203
|
+
path: str
|
|
204
|
+
method: str = "GET"
|
|
205
|
+
score: int = 0
|
|
206
|
+
is_high_value: bool = False
|
|
207
|
+
is_alive: bool = False
|
|
208
|
+
status_code: Optional[int] = None
|
|
209
|
+
parameters: List[str] = field(default_factory=list)
|
|
210
|
+
source: str = "unknown"
|
|
211
|
+
|
|
212
|
+
def to_dict(self) -> Dict:
|
|
213
|
+
return {
|
|
214
|
+
'path': self.path,
|
|
215
|
+
'method': self.method,
|
|
216
|
+
'score': self.score,
|
|
217
|
+
'is_high_value': self.is_high_value,
|
|
218
|
+
'is_alive': self.is_alive,
|
|
219
|
+
'status_code': self.status_code,
|
|
220
|
+
'parameters': self.parameters,
|
|
221
|
+
'source': self.source
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class TestRecord:
|
|
227
|
+
"""测试记录"""
|
|
228
|
+
timestamp: datetime
|
|
229
|
+
endpoint: str
|
|
230
|
+
action: str
|
|
231
|
+
payload: Optional[str] = None
|
|
232
|
+
result: str = ""
|
|
233
|
+
response_time: float = 0.0
|
|
234
|
+
status_code: Optional[int] = None
|
|
235
|
+
|
|
236
|
+
def to_dict(self) -> Dict:
|
|
237
|
+
return {
|
|
238
|
+
'timestamp': self.timestamp.isoformat(),
|
|
239
|
+
'endpoint': self.endpoint,
|
|
240
|
+
'action': self.action,
|
|
241
|
+
'payload': self.payload,
|
|
242
|
+
'result': self.result,
|
|
243
|
+
'response_time': self.response_time,
|
|
244
|
+
'status_code': self.status_code
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass
|
|
249
|
+
class GlobalContext:
|
|
250
|
+
"""全局上下文"""
|
|
251
|
+
target_url: str
|
|
252
|
+
start_time: datetime
|
|
253
|
+
current_phase: TestPhase = TestPhase.INIT
|
|
254
|
+
|
|
255
|
+
tech_stack: TechStackContext = field(default_factory=TechStackContext)
|
|
256
|
+
network: NetworkContext = field(default_factory=NetworkContext)
|
|
257
|
+
security: SecurityContext = field(default_factory=SecurityContext)
|
|
258
|
+
content: ContentContext = field(default_factory=ContentContext)
|
|
259
|
+
|
|
260
|
+
discovered_endpoints: List[Endpoint] = field(default_factory=list)
|
|
261
|
+
test_history: List[TestRecord] = field(default_factory=list)
|
|
262
|
+
|
|
263
|
+
user_preferences: Dict[str, Any] = field(default_factory=dict)
|
|
264
|
+
|
|
265
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
266
|
+
|
|
267
|
+
def to_dict(self) -> Dict:
|
|
268
|
+
return {
|
|
269
|
+
'target_url': self.target_url,
|
|
270
|
+
'start_time': self.start_time.isoformat(),
|
|
271
|
+
'current_phase': self.current_phase.value,
|
|
272
|
+
'tech_stack': self.tech_stack.to_dict(),
|
|
273
|
+
'network': self.network.to_dict(),
|
|
274
|
+
'security': self.security.to_dict(),
|
|
275
|
+
'content': self.content.to_dict(),
|
|
276
|
+
'discovered_endpoints': [e.to_dict() for e in self.discovered_endpoints],
|
|
277
|
+
'test_history': [t.to_dict() for t in self.test_history],
|
|
278
|
+
'user_preferences': self.user_preferences,
|
|
279
|
+
'metadata': self.metadata
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ContextManager:
|
|
284
|
+
"""
|
|
285
|
+
上下文管理器
|
|
286
|
+
|
|
287
|
+
职责:
|
|
288
|
+
- 维护和管理全局上下文
|
|
289
|
+
- 提供上下文更新接口
|
|
290
|
+
- 支持上下文持久化和恢复
|
|
291
|
+
- 触发上下文更新事件
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init__(self, target_url: str):
|
|
295
|
+
self.context = GlobalContext(
|
|
296
|
+
target_url=target_url,
|
|
297
|
+
start_time=datetime.now()
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
self.update_handlers: Dict[str, List[Callable]] = defaultdict(list)
|
|
301
|
+
|
|
302
|
+
self._history: List[Dict] = []
|
|
303
|
+
|
|
304
|
+
def on_update(self, key: str, handler: Callable):
|
|
305
|
+
"""注册上下文更新处理器"""
|
|
306
|
+
self.update_handlers[key].append(handler)
|
|
307
|
+
|
|
308
|
+
def _emit_update(self, key: str, old_value: Any, new_value: Any):
|
|
309
|
+
"""触发更新事件"""
|
|
310
|
+
for handler in self.update_handlers.get(key, []):
|
|
311
|
+
try:
|
|
312
|
+
handler(old_value, new_value)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.warning(f"Update handler error ({key}): {e}")
|
|
315
|
+
|
|
316
|
+
def update_tech_stack(self, fingerprints: Dict[str, Set[str]]):
|
|
317
|
+
"""更新技术栈上下文"""
|
|
318
|
+
old_stack = asdict(self.context.tech_stack)
|
|
319
|
+
|
|
320
|
+
for category, techs in fingerprints.items():
|
|
321
|
+
if category == 'frontend':
|
|
322
|
+
self.context.tech_stack.frontend.update(techs)
|
|
323
|
+
elif category == 'backend':
|
|
324
|
+
self.context.tech_stack.backend.update(techs)
|
|
325
|
+
elif category == 'database':
|
|
326
|
+
self.context.tech_stack.database.update(techs)
|
|
327
|
+
elif category == 'api_type':
|
|
328
|
+
self.context.tech_stack.api_type.update(techs)
|
|
329
|
+
|
|
330
|
+
for tech in fingerprints.get('frontend', set()) | fingerprints.get('backend', set()):
|
|
331
|
+
if tech not in self.context.tech_stack.confidence:
|
|
332
|
+
self.context.tech_stack.confidence[tech] = 0.8
|
|
333
|
+
|
|
334
|
+
self._emit_update('tech_stack', old_stack, asdict(self.context.tech_stack))
|
|
335
|
+
self._record_change('tech_stack_update', fingerprints)
|
|
336
|
+
|
|
337
|
+
def set_waf(self, waf_name: str, confidence: float = 0.8):
|
|
338
|
+
"""设置 WAF"""
|
|
339
|
+
old_waf = self.context.tech_stack.waf
|
|
340
|
+
self.context.tech_stack.waf = waf_name
|
|
341
|
+
self.context.tech_stack.confidence['waf'] = confidence
|
|
342
|
+
|
|
343
|
+
self._emit_update('waf', old_waf, waf_name)
|
|
344
|
+
|
|
345
|
+
def set_cdn(self, cdn_name: str):
|
|
346
|
+
"""设置 CDN"""
|
|
347
|
+
self.context.tech_stack.cdn = cdn_name
|
|
348
|
+
|
|
349
|
+
def update_network_status(self, reachable: bool, reason: Optional[str] = None):
|
|
350
|
+
"""更新网络状态"""
|
|
351
|
+
old_reachable = self.context.network.is_reachable
|
|
352
|
+
self.context.network.is_reachable = reachable
|
|
353
|
+
|
|
354
|
+
if not reachable:
|
|
355
|
+
self.context.network.consecutive_failures += 1
|
|
356
|
+
|
|
357
|
+
if self.context.network.consecutive_failures >= 3:
|
|
358
|
+
self.context.network.rate_limit_status = RateLimitStatus.RATE_LIMITED
|
|
359
|
+
if self.context.network.consecutive_failures >= 5:
|
|
360
|
+
self.context.network.rate_limit_status = RateLimitStatus.BLOCKED
|
|
361
|
+
self.context.network.blocked_count += 1
|
|
362
|
+
else:
|
|
363
|
+
self.context.network.consecutive_failures = 0
|
|
364
|
+
self.context.network.rate_limit_status = RateLimitStatus.NORMAL
|
|
365
|
+
|
|
366
|
+
self.context.network.last_request_time = datetime.now()
|
|
367
|
+
|
|
368
|
+
self._emit_update('network_status', old_reachable, reachable)
|
|
369
|
+
self._record_change('network_status', {'reachable': reachable, 'reason': reason})
|
|
370
|
+
|
|
371
|
+
def set_proxy(self, proxy_config: ProxyConfig):
|
|
372
|
+
"""设置代理"""
|
|
373
|
+
old_proxy = self.context.network.requires_proxy
|
|
374
|
+
self.context.network.proxy_config = proxy_config
|
|
375
|
+
self.context.network.requires_proxy = True
|
|
376
|
+
|
|
377
|
+
self._emit_update('proxy', old_proxy, True)
|
|
378
|
+
|
|
379
|
+
def mark_internal_address(self, address: str, source: str = "response"):
|
|
380
|
+
"""标记内网地址"""
|
|
381
|
+
if address not in self.context.content.internal_ips:
|
|
382
|
+
self.context.content.internal_ips.add(address)
|
|
383
|
+
self._emit_update('internal_address', None, address)
|
|
384
|
+
self._record_change('internal_address_found', {'address': address, 'source': source})
|
|
385
|
+
|
|
386
|
+
def update_rate_limit(self, blocked: bool, increment: int = 1):
|
|
387
|
+
"""更新速率限制状态"""
|
|
388
|
+
if blocked:
|
|
389
|
+
self.context.network.blocked_count += increment
|
|
390
|
+
self.context.network.rate_limit_status = RateLimitStatus.RATE_LIMITED
|
|
391
|
+
else:
|
|
392
|
+
self.context.network.rate_limit_status = RateLimitStatus.NORMAL
|
|
393
|
+
|
|
394
|
+
def set_user_agent(self, user_agent: str):
|
|
395
|
+
"""设置 User-Agent"""
|
|
396
|
+
old_ua = self.context.network.current_user_agent
|
|
397
|
+
self.context.network.current_user_agent = user_agent
|
|
398
|
+
|
|
399
|
+
if user_agent not in self.context.network.user_agents:
|
|
400
|
+
self.context.network.user_agents.append(user_agent)
|
|
401
|
+
|
|
402
|
+
self._emit_update('user_agent', old_ua, user_agent)
|
|
403
|
+
|
|
404
|
+
def rotate_user_agent(self) -> str:
|
|
405
|
+
"""轮换 User-Agent"""
|
|
406
|
+
user_agents = [
|
|
407
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
408
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
409
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
410
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X)",
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
current = self.context.network.current_user_agent
|
|
414
|
+
for ua in user_agents:
|
|
415
|
+
if ua != current:
|
|
416
|
+
self.set_user_agent(ua)
|
|
417
|
+
return ua
|
|
418
|
+
|
|
419
|
+
return current
|
|
420
|
+
|
|
421
|
+
def set_auth_required(self, required: bool, auth_type: Optional[str] = None):
|
|
422
|
+
"""设置认证要求"""
|
|
423
|
+
self.context.security.auth_required = required
|
|
424
|
+
if auth_type:
|
|
425
|
+
self.context.security.auth_type = auth_type
|
|
426
|
+
|
|
427
|
+
def add_auth_endpoint(self, endpoint: str):
|
|
428
|
+
"""添加认证端点"""
|
|
429
|
+
self.context.security.auth_endpoints.add(endpoint)
|
|
430
|
+
|
|
431
|
+
def add_sensitive_endpoint(self, endpoint: str):
|
|
432
|
+
"""添加敏感端点"""
|
|
433
|
+
self.context.security.sensitive_endpoints.add(endpoint)
|
|
434
|
+
|
|
435
|
+
def set_exposure_level(self, level: ExposureLevel):
|
|
436
|
+
"""设置暴露等级"""
|
|
437
|
+
self.context.security.exposure_level = level
|
|
438
|
+
|
|
439
|
+
def set_data_classification(self, classification: DataClassification):
|
|
440
|
+
"""设置数据分类"""
|
|
441
|
+
self.context.security.data_classification = classification
|
|
442
|
+
|
|
443
|
+
def set_spa_mode(self, is_spa: bool, fallback_size: Optional[int] = None):
|
|
444
|
+
"""设置 SPA 模式"""
|
|
445
|
+
self.context.content.is_spa = is_spa
|
|
446
|
+
if fallback_size:
|
|
447
|
+
self.context.content.spa_fallback_size = fallback_size
|
|
448
|
+
|
|
449
|
+
def add_swagger_url(self, url: str):
|
|
450
|
+
"""添加 Swagger URL"""
|
|
451
|
+
if url not in self.context.content.swagger_urls:
|
|
452
|
+
self.context.content.swagger_urls.append(url)
|
|
453
|
+
self.context.content.has_api_docs = True
|
|
454
|
+
|
|
455
|
+
def add_error_leak(self, error: str):
|
|
456
|
+
"""添加错误泄露"""
|
|
457
|
+
if error not in self.context.content.error_leaks:
|
|
458
|
+
self.context.content.error_leaks.append(error)
|
|
459
|
+
|
|
460
|
+
def add_base_url(self, url: str):
|
|
461
|
+
"""添加 Base URL"""
|
|
462
|
+
self.context.content.base_urls.add(url)
|
|
463
|
+
|
|
464
|
+
def add_js_url(self, url: str):
|
|
465
|
+
"""添加 JS URL"""
|
|
466
|
+
if url not in self.context.content.js_urls:
|
|
467
|
+
self.context.content.js_urls.append(url)
|
|
468
|
+
|
|
469
|
+
def add_api_path(self, path: str):
|
|
470
|
+
"""添加 API 路径"""
|
|
471
|
+
if path not in self.context.content.api_paths:
|
|
472
|
+
self.context.content.api_paths.append(path)
|
|
473
|
+
|
|
474
|
+
def add_discovered_endpoint(self, endpoint: Endpoint):
|
|
475
|
+
"""添加发现的端点"""
|
|
476
|
+
for existing in self.context.discovered_endpoints:
|
|
477
|
+
if existing.path == endpoint.path and existing.method == endpoint.method:
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
self.context.discovered_endpoints.append(endpoint)
|
|
481
|
+
self._emit_update('endpoint_discovered', None, endpoint.to_dict())
|
|
482
|
+
|
|
483
|
+
def update_endpoint_status(self, path: str, method: str, is_alive: bool, status_code: Optional[int] = None):
|
|
484
|
+
"""更新端点状态"""
|
|
485
|
+
for endpoint in self.context.discovered_endpoints:
|
|
486
|
+
if endpoint.path == path and endpoint.method == method:
|
|
487
|
+
endpoint.is_alive = is_alive
|
|
488
|
+
endpoint.status_code = status_code
|
|
489
|
+
break
|
|
490
|
+
|
|
491
|
+
def add_test_record(self, record: TestRecord):
|
|
492
|
+
"""添加测试记录"""
|
|
493
|
+
self.context.test_history.append(record)
|
|
494
|
+
|
|
495
|
+
if len(self.context.test_history) > 1000:
|
|
496
|
+
self.context.test_history = self.context.test_history[-1000:]
|
|
497
|
+
|
|
498
|
+
def set_phase(self, phase: TestPhase):
|
|
499
|
+
"""设置测试阶段"""
|
|
500
|
+
old_phase = self.context.current_phase
|
|
501
|
+
self.context.current_phase = phase
|
|
502
|
+
|
|
503
|
+
self._emit_update('phase', old_phase, phase)
|
|
504
|
+
self._record_change('phase_change', {'from': old_phase.value, 'to': phase.value})
|
|
505
|
+
|
|
506
|
+
def set_user_preference(self, key: str, value: Any):
|
|
507
|
+
"""设置用户偏好"""
|
|
508
|
+
self.context.user_preferences[key] = value
|
|
509
|
+
|
|
510
|
+
def get_user_preference(self, key: str, default: Any = None) -> Any:
|
|
511
|
+
"""获取用户偏好"""
|
|
512
|
+
return self.context.user_preferences.get(key, default)
|
|
513
|
+
|
|
514
|
+
def get_relevant_context(self, for_phase: Optional[TestPhase] = None) -> Dict:
|
|
515
|
+
"""获取相关上下文"""
|
|
516
|
+
if for_phase is None:
|
|
517
|
+
for_phase = self.context.current_phase
|
|
518
|
+
|
|
519
|
+
context_subset = {
|
|
520
|
+
'target_url': self.context.target_url,
|
|
521
|
+
'current_phase': for_phase.value,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if for_phase in [TestPhase.RECON, TestPhase.DISCOVERY]:
|
|
525
|
+
context_subset['tech_stack'] = self.context.tech_stack.to_dict()
|
|
526
|
+
context_subset['content'] = self.context.content.to_dict()
|
|
527
|
+
|
|
528
|
+
elif for_phase in [TestPhase.TESTING, TestPhase.FUZZING]:
|
|
529
|
+
context_subset['network'] = self.context.network.to_dict()
|
|
530
|
+
context_subset['security'] = self.context.security.to_dict()
|
|
531
|
+
context_subset['discovered_endpoints'] = [e.to_dict() for e in self.context.discovered_endpoints]
|
|
532
|
+
|
|
533
|
+
elif for_phase == TestPhase.REPORT:
|
|
534
|
+
context_subset['full_context'] = self.context.to_dict()
|
|
535
|
+
|
|
536
|
+
return context_subset
|
|
537
|
+
|
|
538
|
+
def get_high_value_endpoints(self) -> List[Endpoint]:
|
|
539
|
+
"""获取高价值端点"""
|
|
540
|
+
return [e for e in self.context.discovered_endpoints if e.is_high_value]
|
|
541
|
+
|
|
542
|
+
def get_alive_endpoints(self) -> List[Endpoint]:
|
|
543
|
+
"""获取存活端点"""
|
|
544
|
+
return [e for e in self.context.discovered_endpoints if e.is_alive]
|
|
545
|
+
|
|
546
|
+
def get_internal_addresses(self) -> Set[str]:
|
|
547
|
+
"""获取内网地址"""
|
|
548
|
+
return self.context.content.internal_ips.copy()
|
|
549
|
+
|
|
550
|
+
def needs_proxy(self) -> bool:
|
|
551
|
+
"""是否需要代理"""
|
|
552
|
+
return bool(self.context.content.internal_ips) or self.context.network.requires_proxy
|
|
553
|
+
|
|
554
|
+
def is_rate_limited(self) -> bool:
|
|
555
|
+
"""是否被限速"""
|
|
556
|
+
return self.context.network.rate_limit_status in [
|
|
557
|
+
RateLimitStatus.RATE_LIMITED,
|
|
558
|
+
RateLimitStatus.BLOCKED
|
|
559
|
+
]
|
|
560
|
+
|
|
561
|
+
def get_current_rate_limit(self) -> int:
|
|
562
|
+
"""获取当前速率限制"""
|
|
563
|
+
status = self.context.network.rate_limit_status
|
|
564
|
+
|
|
565
|
+
if status == RateLimitStatus.BLOCKED:
|
|
566
|
+
return 0
|
|
567
|
+
elif status == RateLimitStatus.RATE_LIMITED:
|
|
568
|
+
return 1
|
|
569
|
+
elif status == RateLimitStatus.WARNING:
|
|
570
|
+
return 5
|
|
571
|
+
else:
|
|
572
|
+
return 10
|
|
573
|
+
|
|
574
|
+
def export_context(self) -> Dict:
|
|
575
|
+
"""导出完整上下文"""
|
|
576
|
+
return self.context.to_dict()
|
|
577
|
+
|
|
578
|
+
def export_json(self) -> str:
|
|
579
|
+
"""导出 JSON 格式"""
|
|
580
|
+
return json.dumps(self.export_context(), indent=2, default=str)
|
|
581
|
+
|
|
582
|
+
def save_to_file(self, filepath: str):
|
|
583
|
+
"""保存上下文到文件"""
|
|
584
|
+
with open(filepath, 'w') as f:
|
|
585
|
+
json.dump(self.export_context(), f, indent=2, default=str)
|
|
586
|
+
|
|
587
|
+
@classmethod
|
|
588
|
+
def load_from_dict(cls, data: Dict) -> 'ContextManager':
|
|
589
|
+
"""从字典加载"""
|
|
590
|
+
manager = cls(data['target_url'])
|
|
591
|
+
|
|
592
|
+
if 'tech_stack' in data:
|
|
593
|
+
ts = data['tech_stack']
|
|
594
|
+
manager.context.tech_stack = TechStackContext(
|
|
595
|
+
frontend=set(ts.get('frontend', [])),
|
|
596
|
+
backend=set(ts.get('backend', [])),
|
|
597
|
+
database=set(ts.get('database', [])),
|
|
598
|
+
api_type=set(ts.get('api_type', [])),
|
|
599
|
+
waf=ts.get('waf'),
|
|
600
|
+
cdn=ts.get('cdn')
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
if 'network' in data:
|
|
604
|
+
nw = data['network']
|
|
605
|
+
manager.context.network = NetworkContext(
|
|
606
|
+
is_reachable=nw.get('is_reachable', True),
|
|
607
|
+
requires_proxy=nw.get('requires_proxy', False)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return manager
|
|
611
|
+
|
|
612
|
+
@classmethod
|
|
613
|
+
def load_from_file(cls, filepath: str) -> 'ContextManager':
|
|
614
|
+
"""从文件加载"""
|
|
615
|
+
with open(filepath, 'r') as f:
|
|
616
|
+
data = json.load(f)
|
|
617
|
+
return cls.load_from_dict(data)
|
|
618
|
+
|
|
619
|
+
def _record_change(self, change_type: str, data: Any):
|
|
620
|
+
"""记录变更"""
|
|
621
|
+
self._history.append({
|
|
622
|
+
'timestamp': datetime.now().isoformat(),
|
|
623
|
+
'type': change_type,
|
|
624
|
+
'data': data
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
def get_history(self) -> List[Dict]:
|
|
628
|
+
"""获取变更历史"""
|
|
629
|
+
return self._history.copy()
|
|
630
|
+
|
|
631
|
+
def get_summary(self) -> Dict:
|
|
632
|
+
"""获取上下文摘要"""
|
|
633
|
+
return {
|
|
634
|
+
'target_url': self.context.target_url,
|
|
635
|
+
'phase': self.context.current_phase.value,
|
|
636
|
+
'tech_stack': {
|
|
637
|
+
'frontend': list(self.context.tech_stack.frontend),
|
|
638
|
+
'backend': list(self.context.tech_stack.backend),
|
|
639
|
+
'waf': self.context.tech_stack.waf
|
|
640
|
+
},
|
|
641
|
+
'network': {
|
|
642
|
+
'reachable': self.context.network.is_reachable,
|
|
643
|
+
'rate_limit_status': self.context.network.rate_limit_status.value
|
|
644
|
+
},
|
|
645
|
+
'endpoints': {
|
|
646
|
+
'total': len(self.context.discovered_endpoints),
|
|
647
|
+
'alive': len(self.get_alive_endpoints()),
|
|
648
|
+
'high_value': len(self.get_high_value_endpoints())
|
|
649
|
+
},
|
|
650
|
+
'content': {
|
|
651
|
+
'is_spa': self.context.content.is_spa,
|
|
652
|
+
'has_api_docs': self.context.content.has_api_docs,
|
|
653
|
+
'internal_ips': list(self.context.content.internal_ips)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def create_context_manager(target_url: str) -> ContextManager:
|
|
659
|
+
"""创建上下文管理器工厂函数"""
|
|
660
|
+
return ContextManager(target_url)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
if __name__ == "__main__":
|
|
664
|
+
cm = create_context_manager("http://example.com")
|
|
665
|
+
|
|
666
|
+
cm.update_tech_stack({'frontend': {'vue', 'webpack'}, 'backend': {'spring'}})
|
|
667
|
+
cm.set_waf("aliyun")
|
|
668
|
+
cm.set_spa_mode(True, fallback_size=678)
|
|
669
|
+
cm.add_swagger_url("http://example.com/api-docs")
|
|
670
|
+
cm.mark_internal_address("10.0.0.1")
|
|
671
|
+
cm.add_discovered_endpoint(Endpoint(path="/api/users", method="GET", score=8, is_high_value=True))
|
|
672
|
+
cm.set_phase(TestPhase.DISCOVERY)
|
|
673
|
+
|
|
674
|
+
print("Context Summary:")
|
|
675
|
+
print(json.dumps(cm.get_summary(), indent=2))
|
|
676
|
+
|
|
677
|
+
print("\nFull Context (JSON):")
|
|
678
|
+
print(cm.export_json())
|
|
679
|
+
|
|
680
|
+
print("\nHistory:")
|
|
681
|
+
for h in cm.get_history():
|
|
682
|
+
print(f" {h['type']}: {h['data']}")
|