opencode-api-security-testing 2.1.0 → 2.1.2

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 (61) hide show
  1. package/SKILL.md +1797 -0
  2. package/core/advanced_recon.py +788 -0
  3. package/core/agentic_analyzer.py +445 -0
  4. package/core/analyzers/api_parser.py +210 -0
  5. package/core/analyzers/response_analyzer.py +212 -0
  6. package/core/analyzers/sensitive_finder.py +184 -0
  7. package/core/api_fuzzer.py +422 -0
  8. package/core/api_interceptor.py +525 -0
  9. package/core/api_parser.py +955 -0
  10. package/core/browser_tester.py +479 -0
  11. package/core/cloud_storage_tester.py +1330 -0
  12. package/core/collectors/__init__.py +23 -0
  13. package/core/collectors/api_path_finder.py +300 -0
  14. package/core/collectors/browser_collect.py +645 -0
  15. package/core/collectors/browser_collector.py +411 -0
  16. package/core/collectors/http_client.py +111 -0
  17. package/core/collectors/js_collector.py +490 -0
  18. package/core/collectors/js_parser.py +780 -0
  19. package/core/collectors/url_collector.py +319 -0
  20. package/core/context_manager.py +682 -0
  21. package/core/deep_api_tester_v35.py +844 -0
  22. package/core/deep_api_tester_v55.py +366 -0
  23. package/core/dynamic_api_analyzer.py +532 -0
  24. package/core/http_client.py +179 -0
  25. package/core/models.py +296 -0
  26. package/core/orchestrator.py +890 -0
  27. package/core/prerequisite.py +227 -0
  28. package/core/reasoning_engine.py +1042 -0
  29. package/core/response_classifier.py +606 -0
  30. package/core/runner.py +938 -0
  31. package/core/scan_engine.py +599 -0
  32. package/core/skill_executor.py +435 -0
  33. package/core/skill_executor_v2.py +670 -0
  34. package/core/skill_executor_v3.py +704 -0
  35. package/core/smart_analyzer.py +687 -0
  36. package/core/strategy_pool.py +707 -0
  37. package/core/testers/auth_tester.py +264 -0
  38. package/core/testers/idor_tester.py +200 -0
  39. package/core/testers/sqli_tester.py +211 -0
  40. package/core/testing_loop.py +655 -0
  41. package/core/utils/base_path_dict.py +255 -0
  42. package/core/utils/payload_lib.py +167 -0
  43. package/core/utils/ssrf_detector.py +220 -0
  44. package/core/verifiers/vuln_verifier.py +536 -0
  45. package/package.json +17 -13
  46. package/references/asset-discovery.md +119 -612
  47. package/references/graphql-guidance.md +65 -641
  48. package/references/intake.md +84 -0
  49. package/references/report-template.md +131 -38
  50. package/references/rest-guidance.md +55 -526
  51. package/references/severity-model.md +52 -264
  52. package/references/test-matrix.md +65 -263
  53. package/references/validation.md +53 -400
  54. package/scripts/postinstall.js +46 -0
  55. package/agents/cyber-supervisor.md +0 -55
  56. package/agents/probing-miner.md +0 -42
  57. package/agents/resource-specialist.md +0 -31
  58. package/commands/api-security-testing-scan.md +0 -59
  59. package/commands/api-security-testing-test.md +0 -49
  60. package/commands/api-security-testing.md +0 -72
  61. 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']}")