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.
- 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,1042 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Agentic Reasoning Engine - 智能推理引擎
|
|
4
|
+
|
|
5
|
+
多层级推理流程:
|
|
6
|
+
Surface → Context → Causal → Strategic
|
|
7
|
+
|
|
8
|
+
核心功能:
|
|
9
|
+
- 从表面现象到深层因果理解
|
|
10
|
+
- 规则引擎驱动的推理机制
|
|
11
|
+
- 置信度评估
|
|
12
|
+
- 洞察生成
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import hashlib
|
|
17
|
+
import time
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from typing import Dict, List, Set, Tuple, Optional, Any, Callable, Type
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from collections import defaultdict
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UnderstandingLevel(Enum):
|
|
29
|
+
"""理解层级"""
|
|
30
|
+
SURFACE = "surface" # 表面现象
|
|
31
|
+
CONTEXT = "context" # 上下文理解
|
|
32
|
+
CAUSAL = "causal" # 因果推理
|
|
33
|
+
STRATEGIC = "strategic" # 战略调整
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InsightType(Enum):
|
|
37
|
+
"""洞察类型"""
|
|
38
|
+
OBSERVATION = "observation" # 观察到的事实
|
|
39
|
+
PATTERN = "pattern" # 发现的模式
|
|
40
|
+
INFERENCE = "inference" # 推断
|
|
41
|
+
BLOCKER = "blocker" # 阻碍因素
|
|
42
|
+
OPPORTUNITY = "opportunity" # 机会
|
|
43
|
+
STRATEGY_CHANGE = "strategy" # 策略调整
|
|
44
|
+
WARNING = "warning" # 警告
|
|
45
|
+
VALIDATION = "validation" # 验证结果
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Finding:
|
|
50
|
+
"""推理发现"""
|
|
51
|
+
what: str # 观察到什么
|
|
52
|
+
so_what: str # 这意味着什么(核心)
|
|
53
|
+
why: str # 为什么(原因分析)
|
|
54
|
+
implication: str # 对测试的影响
|
|
55
|
+
strategy: str # 调整后的策略
|
|
56
|
+
confidence: float # 置信度 0-1
|
|
57
|
+
level: UnderstandingLevel
|
|
58
|
+
evidence: List[str] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Insight:
|
|
63
|
+
"""洞察"""
|
|
64
|
+
id: str
|
|
65
|
+
type: InsightType
|
|
66
|
+
content: str
|
|
67
|
+
|
|
68
|
+
findings: List[Finding] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
source: str = "" # 哪个模块生成
|
|
71
|
+
confidence: float = 1.0
|
|
72
|
+
|
|
73
|
+
action_required: Optional[str] = None
|
|
74
|
+
affected_strategies: List[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
generated_at: datetime = field(default_factory=datetime.now)
|
|
77
|
+
valid_until: Optional[datetime] = None
|
|
78
|
+
is_active: bool = True
|
|
79
|
+
|
|
80
|
+
observations: List[str] = field(default_factory=list) # 基于哪些观察
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> Dict:
|
|
83
|
+
return {
|
|
84
|
+
'id': self.id,
|
|
85
|
+
'type': self.type.value,
|
|
86
|
+
'content': self.content,
|
|
87
|
+
'confidence': self.confidence,
|
|
88
|
+
'findings': [{
|
|
89
|
+
'what': f.what,
|
|
90
|
+
'so_what': f.so_what,
|
|
91
|
+
'why': f.why,
|
|
92
|
+
'implication': f.implication,
|
|
93
|
+
'strategy': f.strategy,
|
|
94
|
+
'confidence': f.confidence,
|
|
95
|
+
'level': f.level.value
|
|
96
|
+
} for f in self.findings],
|
|
97
|
+
'source': self.source,
|
|
98
|
+
'action_required': self.action_required,
|
|
99
|
+
'generated_at': self.generated_at.isoformat(),
|
|
100
|
+
'is_active': self.is_active
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class Observation:
|
|
106
|
+
"""观察"""
|
|
107
|
+
id: str
|
|
108
|
+
timestamp: datetime
|
|
109
|
+
url: str
|
|
110
|
+
method: str
|
|
111
|
+
|
|
112
|
+
status_code: int = 0
|
|
113
|
+
content_type: str = ""
|
|
114
|
+
content_length: int = 0
|
|
115
|
+
content_hash: str = ""
|
|
116
|
+
|
|
117
|
+
is_html: bool = False
|
|
118
|
+
is_json: bool = False
|
|
119
|
+
is_xml: bool = False
|
|
120
|
+
is_plain_text: bool = False
|
|
121
|
+
|
|
122
|
+
spa_indicators: List[str] = field(default_factory=list)
|
|
123
|
+
api_indicators: List[str] = field(default_factory=list)
|
|
124
|
+
security_indicators: List[str] = field(default_factory=list)
|
|
125
|
+
tech_fingerprints: Dict[str, Set[str]] = field(default_factory=dict)
|
|
126
|
+
|
|
127
|
+
source: str = "" # 'js', 'html', 'api', 'browser', 'fuzz'
|
|
128
|
+
parent_url: Optional[str] = None
|
|
129
|
+
parameters: Dict[str, str] = field(default_factory=dict)
|
|
130
|
+
|
|
131
|
+
response_time: float = 0.0
|
|
132
|
+
is_first_request: bool = False
|
|
133
|
+
consecutive_failures: int = 0
|
|
134
|
+
|
|
135
|
+
raw_content: str = "" # 原始内容片段用于特征提取
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> Dict:
|
|
138
|
+
return {
|
|
139
|
+
'id': self.id,
|
|
140
|
+
'url': self.url,
|
|
141
|
+
'method': self.method,
|
|
142
|
+
'status_code': self.status_code,
|
|
143
|
+
'content_type': self.content_type,
|
|
144
|
+
'content_length': self.content_length,
|
|
145
|
+
'is_html': self.is_html,
|
|
146
|
+
'is_json': self.is_json,
|
|
147
|
+
'spa_indicators': self.spa_indicators,
|
|
148
|
+
'api_indicators': self.api_indicators,
|
|
149
|
+
'tech_fingerprints': {k: list(v) for k, v in self.tech_fingerprints.items()}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class ReasoningRule:
|
|
155
|
+
"""推理规则"""
|
|
156
|
+
name: str
|
|
157
|
+
description: str
|
|
158
|
+
|
|
159
|
+
level: UnderstandingLevel
|
|
160
|
+
|
|
161
|
+
condition: Callable[['Observation', List['Observation']], bool]
|
|
162
|
+
|
|
163
|
+
findings_builder: Callable[['Observation', List['Observation']], Finding]
|
|
164
|
+
|
|
165
|
+
priority: int = 0
|
|
166
|
+
|
|
167
|
+
enabled: bool = True
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class Result:
|
|
171
|
+
rule_name: str
|
|
172
|
+
triggered: bool
|
|
173
|
+
finding: Optional[Finding] = None
|
|
174
|
+
confidence: float = 0.0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class InsightStore:
|
|
178
|
+
"""洞察存储"""
|
|
179
|
+
|
|
180
|
+
def __init__(self):
|
|
181
|
+
self.insights: List[Insight] = []
|
|
182
|
+
self.insights_by_type: Dict[InsightType, List[Insight]] = defaultdict(list)
|
|
183
|
+
self.insights_by_source: Dict[str, List[Insight]] = defaultdict(list)
|
|
184
|
+
|
|
185
|
+
self.learning_history: List[Dict] = []
|
|
186
|
+
|
|
187
|
+
def add(self, insight: Insight):
|
|
188
|
+
"""添加洞察"""
|
|
189
|
+
self.insights.append(insight)
|
|
190
|
+
self.insights_by_type[insight.type].append(insight)
|
|
191
|
+
self.insights_by_source[insight.source].append(insight)
|
|
192
|
+
|
|
193
|
+
def get_active(self) -> List[Insight]:
|
|
194
|
+
"""获取活跃洞察"""
|
|
195
|
+
now = datetime.now()
|
|
196
|
+
return [i for i in self.insights if i.is_active and
|
|
197
|
+
(i.valid_until is None or i.valid_until > now)]
|
|
198
|
+
|
|
199
|
+
def get_by_type(self, insight_type: InsightType) -> List[Insight]:
|
|
200
|
+
"""按类型获取洞察"""
|
|
201
|
+
return self.insights_by_type.get(insight_type, [])
|
|
202
|
+
|
|
203
|
+
def get_by_source(self, source: str) -> List[Insight]:
|
|
204
|
+
"""按来源获取洞察"""
|
|
205
|
+
return self.insights_by_source.get(source, [])
|
|
206
|
+
|
|
207
|
+
def deactivate(self, insight_id: str):
|
|
208
|
+
"""停用洞察"""
|
|
209
|
+
for insight in self.insights:
|
|
210
|
+
if insight.id == insight_id:
|
|
211
|
+
insight.is_active = False
|
|
212
|
+
|
|
213
|
+
def record_learning(self, pattern: str, outcome: str, effectiveness: float):
|
|
214
|
+
"""记录学习结果"""
|
|
215
|
+
self.learning_history.append({
|
|
216
|
+
'pattern': pattern,
|
|
217
|
+
'outcome': outcome,
|
|
218
|
+
'effectiveness': effectiveness,
|
|
219
|
+
'timestamp': datetime.now()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
def get_summary(self) -> Dict:
|
|
223
|
+
"""获取洞察摘要"""
|
|
224
|
+
active = self.get_active()
|
|
225
|
+
return {
|
|
226
|
+
'total_insights': len(self.insights),
|
|
227
|
+
'active_insights': len(active),
|
|
228
|
+
'by_type': {t.value: len(self.insights_by_type[t]) for t in InsightType},
|
|
229
|
+
'learning_history_size': len(self.learning_history)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Reasoner:
|
|
234
|
+
"""
|
|
235
|
+
推理引擎
|
|
236
|
+
|
|
237
|
+
执行多层级推理:
|
|
238
|
+
1. Surface Level: 识别响应类型和基本特征
|
|
239
|
+
2. Context Level: 理解上下文关联
|
|
240
|
+
3. Causal Level: 推断因果关系
|
|
241
|
+
4. Strategic Level: 制定调整策略
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def __init__(self):
|
|
245
|
+
self.rules: List[ReasoningRule] = []
|
|
246
|
+
self.observation_history: List[Observation] = []
|
|
247
|
+
self.insight_store = InsightStore()
|
|
248
|
+
|
|
249
|
+
self._register_default_rules()
|
|
250
|
+
|
|
251
|
+
def _register_default_rules(self):
|
|
252
|
+
"""注册默认推理规则"""
|
|
253
|
+
self.register_rule(self._create_spa_fallback_rule())
|
|
254
|
+
self.register_rule(self._create_json_request_html_response_rule())
|
|
255
|
+
self.register_rule(self._create_internal_ip_rule())
|
|
256
|
+
self.register_rule(self._create_waf_detection_rule())
|
|
257
|
+
self.register_rule(self._create_tech_fingerprint_rule())
|
|
258
|
+
self.register_rule(self._create_error_leak_rule())
|
|
259
|
+
self.register_rule(self._create_auth_detection_rule())
|
|
260
|
+
self.register_rule(self._create_swagger_discovery_rule())
|
|
261
|
+
|
|
262
|
+
def register_rule(self, rule: ReasoningRule):
|
|
263
|
+
"""注册推理规则"""
|
|
264
|
+
self.rules.append(rule)
|
|
265
|
+
self.rules.sort(key=lambda r: r.priority, reverse=True)
|
|
266
|
+
logger.debug(f"Registered reasoning rule: {rule.name}")
|
|
267
|
+
|
|
268
|
+
def observe_and_reason(self, response_data: Dict, session: Any = None) -> List[Insight]:
|
|
269
|
+
"""
|
|
270
|
+
观察并推理
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
response_data: 响应数据字典
|
|
274
|
+
session: 可选的 session 用于关联分析
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
生成的洞察列表
|
|
278
|
+
"""
|
|
279
|
+
observation = self._create_observation(response_data)
|
|
280
|
+
self.observation_history.append(observation)
|
|
281
|
+
|
|
282
|
+
return self.reason(observation)
|
|
283
|
+
|
|
284
|
+
def reason(self, observation: Observation) -> List[Insight]:
|
|
285
|
+
"""
|
|
286
|
+
执行多层级推理
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
observation: 当前观察
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
生成的洞察列表
|
|
293
|
+
"""
|
|
294
|
+
insights = []
|
|
295
|
+
|
|
296
|
+
recent_obs = self.observation_history[-20:]
|
|
297
|
+
|
|
298
|
+
for rule in self.rules:
|
|
299
|
+
if not rule.enabled:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
triggered = rule.condition(observation, recent_obs)
|
|
304
|
+
|
|
305
|
+
if triggered:
|
|
306
|
+
finding = rule.findings_builder(observation, recent_obs)
|
|
307
|
+
|
|
308
|
+
if finding:
|
|
309
|
+
insight = Insight(
|
|
310
|
+
id=self._generate_insight_id(),
|
|
311
|
+
type=self._rule_to_insight_type(rule),
|
|
312
|
+
content=finding.so_what,
|
|
313
|
+
findings=[finding],
|
|
314
|
+
source=f"reasoner:{rule.name}",
|
|
315
|
+
confidence=finding.confidence,
|
|
316
|
+
action_required=finding.strategy if finding.confidence < 0.9 else None,
|
|
317
|
+
observations=[observation.id]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
insights.append(insight)
|
|
321
|
+
self.insight_store.add(insight)
|
|
322
|
+
|
|
323
|
+
logger.info(f"Rule triggered: {rule.name} → {insight.content}")
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.warning(f"Rule evaluation error ({rule.name}): {e}")
|
|
327
|
+
|
|
328
|
+
if len(recent_obs) >= 3:
|
|
329
|
+
pattern_insights = self._detect_patterns(recent_obs)
|
|
330
|
+
insights.extend(pattern_insights)
|
|
331
|
+
|
|
332
|
+
return insights
|
|
333
|
+
|
|
334
|
+
def _detect_patterns(self, observations: List[Observation]) -> List[Insight]:
|
|
335
|
+
"""检测观察模式"""
|
|
336
|
+
insights = []
|
|
337
|
+
|
|
338
|
+
html_obs = [o for o in observations if o.is_html]
|
|
339
|
+
if len(html_obs) >= 3:
|
|
340
|
+
lengths = [o.content_length for o in html_obs]
|
|
341
|
+
if len(set(lengths)) == 1:
|
|
342
|
+
length = lengths[0]
|
|
343
|
+
|
|
344
|
+
finding = Finding(
|
|
345
|
+
what=f"所有 {len(html_obs)} 个不同路径返回完全相同大小的 HTML ({length} 字节)",
|
|
346
|
+
so_what="这是典型的 SPA (Vue.js/React) fallback 行为",
|
|
347
|
+
why="前端服务器配置了 catch-all 路由,将所有请求都路由到 index.html",
|
|
348
|
+
implication="后端 API 不在当前服务器,可能在内网或使用不同的地址",
|
|
349
|
+
strategy="1. 从 JS 中提取后端 API 地址 2. 尝试不同端口/路径探测 3. 如内网地址需要代理访问",
|
|
350
|
+
confidence=0.95,
|
|
351
|
+
level=UnderstandingLevel.CAUSAL,
|
|
352
|
+
evidence=[f"{o.url} ({o.content_length} bytes)" for o in html_obs[:5]]
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
insight = Insight(
|
|
356
|
+
id=self._generate_insight_id(),
|
|
357
|
+
type=InsightType.PATTERN,
|
|
358
|
+
content=finding.so_what,
|
|
359
|
+
findings=[finding],
|
|
360
|
+
source="reasoner:spa_fallback_pattern",
|
|
361
|
+
confidence=0.95,
|
|
362
|
+
action_required=finding.strategy
|
|
363
|
+
)
|
|
364
|
+
insights.append(insight)
|
|
365
|
+
self.insight_store.add(insight)
|
|
366
|
+
|
|
367
|
+
return insights
|
|
368
|
+
|
|
369
|
+
def reason_from_pattern(self, observations: List[Observation]) -> Optional[Insight]:
|
|
370
|
+
"""从观察模式中推理"""
|
|
371
|
+
if len(observations) < 2:
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
first = observations[0]
|
|
375
|
+
last = observations[-1]
|
|
376
|
+
|
|
377
|
+
if first.content_hash == last.content_hash and first.url != last.url:
|
|
378
|
+
return Insight(
|
|
379
|
+
id=self._generate_insight_id(),
|
|
380
|
+
type=InsightType.PATTERN,
|
|
381
|
+
content="不同 URL 返回完全相同内容",
|
|
382
|
+
source="reasoner:content_similarity",
|
|
383
|
+
confidence=0.7
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def estimate_confidence(self, evidence: List[Any], historical_accuracy: float = 0.8) -> float:
|
|
389
|
+
"""
|
|
390
|
+
评估置信度
|
|
391
|
+
|
|
392
|
+
基于:
|
|
393
|
+
- 证据数量
|
|
394
|
+
- 一致性
|
|
395
|
+
- 历史准确率
|
|
396
|
+
"""
|
|
397
|
+
if not evidence:
|
|
398
|
+
return 0.3
|
|
399
|
+
|
|
400
|
+
evidence_factor = min(len(evidence) / 5.0, 1.0) * 0.3
|
|
401
|
+
|
|
402
|
+
consistency_factor = 0.4
|
|
403
|
+
|
|
404
|
+
historical_factor = historical_accuracy * 0.3
|
|
405
|
+
|
|
406
|
+
confidence = evidence_factor + consistency_factor + historical_factor
|
|
407
|
+
|
|
408
|
+
return min(max(confidence, 0.0), 1.0)
|
|
409
|
+
|
|
410
|
+
def _create_observation(self, response_data: Dict) -> Observation:
|
|
411
|
+
"""从响应数据创建观察"""
|
|
412
|
+
content = response_data.get('content', '')
|
|
413
|
+
content_hash = hashlib.md5(content[:1000].encode()).hexdigest() if content else ""
|
|
414
|
+
|
|
415
|
+
obs = Observation(
|
|
416
|
+
id=self._generate_observation_id(),
|
|
417
|
+
timestamp=datetime.now(),
|
|
418
|
+
url=response_data.get('url', ''),
|
|
419
|
+
method=response_data.get('method', 'GET'),
|
|
420
|
+
status_code=response_data.get('status_code', 0),
|
|
421
|
+
content_type=response_data.get('content_type', ''),
|
|
422
|
+
content_length=len(content),
|
|
423
|
+
content_hash=content_hash,
|
|
424
|
+
is_html=self._check_html(content),
|
|
425
|
+
is_json=self._check_json(content),
|
|
426
|
+
is_xml=self._check_xml(content),
|
|
427
|
+
source=response_data.get('source', 'unknown'),
|
|
428
|
+
response_time=response_data.get('response_time', 0.0),
|
|
429
|
+
raw_content=content[:500] if content else ""
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
obs.spa_indicators = self._detect_spa_indicators(content)
|
|
433
|
+
obs.api_indicators = self._detect_api_indicators(content)
|
|
434
|
+
obs.security_indicators = self._detect_security_indicators(content)
|
|
435
|
+
obs.tech_fingerprints = self._detect_tech_fingerprints(response_data, content)
|
|
436
|
+
|
|
437
|
+
return obs
|
|
438
|
+
|
|
439
|
+
def _check_html(self, content: str) -> bool:
|
|
440
|
+
if not content:
|
|
441
|
+
return False
|
|
442
|
+
content_lower = content.lower()
|
|
443
|
+
return '<!doctype' in content_lower or '<html' in content_lower or '<!doctype html>' in content_lower
|
|
444
|
+
|
|
445
|
+
def _check_json(self, content: str) -> bool:
|
|
446
|
+
if not content:
|
|
447
|
+
return False
|
|
448
|
+
try:
|
|
449
|
+
import json
|
|
450
|
+
json.loads(content)
|
|
451
|
+
return True
|
|
452
|
+
except:
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
def _check_xml(self, content: str) -> bool:
|
|
456
|
+
if not content:
|
|
457
|
+
return False
|
|
458
|
+
return '<?xml' in content or '<root' in content
|
|
459
|
+
|
|
460
|
+
def _detect_spa_indicators(self, content: str) -> List[str]:
|
|
461
|
+
indicators = []
|
|
462
|
+
if not content:
|
|
463
|
+
return indicators
|
|
464
|
+
content_lower = content.lower()
|
|
465
|
+
|
|
466
|
+
spa_patterns = {
|
|
467
|
+
'webpack_chunk_vendors': r'chunk-vendors',
|
|
468
|
+
'div_id_app': r'<div[^>]+id=["\']app["\']',
|
|
469
|
+
'div_id_root': r'<div[^>]+id=["\']root["\']',
|
|
470
|
+
'vue_keyword': r'vue',
|
|
471
|
+
'react_keyword': r'react',
|
|
472
|
+
'angular_keyword': r'angular',
|
|
473
|
+
'ng_app': r'ng-app',
|
|
474
|
+
'next_js': r'__NEXT_DATA__',
|
|
475
|
+
'nuxt_js': r'__nuxt',
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for name, pattern in spa_patterns.items():
|
|
479
|
+
if re.search(pattern, content_lower):
|
|
480
|
+
indicators.append(name)
|
|
481
|
+
|
|
482
|
+
return indicators
|
|
483
|
+
|
|
484
|
+
def _detect_api_indicators(self, content: str) -> List[str]:
|
|
485
|
+
indicators = []
|
|
486
|
+
if not content:
|
|
487
|
+
return indicators
|
|
488
|
+
content_lower = content.lower()
|
|
489
|
+
|
|
490
|
+
api_patterns = {
|
|
491
|
+
'swagger': r'swagger',
|
|
492
|
+
'openapi': r'openapi',
|
|
493
|
+
'api_paths': r'/api/',
|
|
494
|
+
'graphql': r'__schema',
|
|
495
|
+
'rest_paths': r'/v\d+/',
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for name, pattern in api_patterns.items():
|
|
499
|
+
if re.search(pattern, content_lower):
|
|
500
|
+
indicators.append(name)
|
|
501
|
+
|
|
502
|
+
return indicators
|
|
503
|
+
|
|
504
|
+
def _detect_security_indicators(self, content: str) -> List[str]:
|
|
505
|
+
indicators = []
|
|
506
|
+
if not content:
|
|
507
|
+
return indicators
|
|
508
|
+
content_lower = content.lower()
|
|
509
|
+
|
|
510
|
+
security_patterns = {
|
|
511
|
+
'sql_error': r'sql.*(error|syntax|warning)',
|
|
512
|
+
'mysql_error': r'mysql',
|
|
513
|
+
'postgresql_error': r'postgresql|psql',
|
|
514
|
+
'oracle_error': r'oracle',
|
|
515
|
+
'xss_reflect': r'<script|javascript:',
|
|
516
|
+
'auth_required': r'(unauthorized|401|login|auth)',
|
|
517
|
+
'forbidden': r'(forbidden|403|access denied)',
|
|
518
|
+
'error_disclosure': r'(exception|stack trace|at .+\.java)',
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
for name, pattern in security_patterns.items():
|
|
522
|
+
if re.search(pattern, content_lower):
|
|
523
|
+
indicators.append(name)
|
|
524
|
+
|
|
525
|
+
return indicators
|
|
526
|
+
|
|
527
|
+
def _detect_tech_fingerprints(self, response_data: Dict, content: str) -> Dict[str, Set[str]]:
|
|
528
|
+
fingerprints: Dict[str, Set[str]] = defaultdict(set)
|
|
529
|
+
|
|
530
|
+
headers = response_data.get('headers', {})
|
|
531
|
+
|
|
532
|
+
header_fingerprints = {
|
|
533
|
+
'server': {
|
|
534
|
+
'nginx': r'nginx',
|
|
535
|
+
'apache': r'apache',
|
|
536
|
+
'iis': r'microsoft-iis',
|
|
537
|
+
'express': r'express',
|
|
538
|
+
'kestrel': r'kestrel',
|
|
539
|
+
},
|
|
540
|
+
'x_powered_by': {
|
|
541
|
+
'php': r'php',
|
|
542
|
+
'asp.net': r'asp\.net',
|
|
543
|
+
'express': r'express',
|
|
544
|
+
'spring': r'spring',
|
|
545
|
+
'django': r'django',
|
|
546
|
+
'flask': r'flask',
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for header, patterns in header_fingerprints.items():
|
|
551
|
+
header_value = headers.get(header, '')
|
|
552
|
+
if header_value:
|
|
553
|
+
for tech, pattern in patterns.items():
|
|
554
|
+
if re.search(pattern, header_value, re.IGNORECASE):
|
|
555
|
+
fingerprints[header].add(tech)
|
|
556
|
+
|
|
557
|
+
content_lower = content.lower() if content else ''
|
|
558
|
+
|
|
559
|
+
content_fingerprints = {
|
|
560
|
+
'frontend': {
|
|
561
|
+
'vue': r'vue(\.runtime)?\.js',
|
|
562
|
+
'react': r'react\.js|react-dom',
|
|
563
|
+
'angular': r'@angular|angular\.js',
|
|
564
|
+
'jquery': r'jquery',
|
|
565
|
+
'bootstrap': r'bootstrap(\.min)?\.js',
|
|
566
|
+
'tailwind': r'tailwindcss',
|
|
567
|
+
},
|
|
568
|
+
'backend': {
|
|
569
|
+
'spring': r'spring|org\.springframework',
|
|
570
|
+
'django': r'django',
|
|
571
|
+
'flask': r'flask',
|
|
572
|
+
'express': r'express',
|
|
573
|
+
'laravel': r'laravel',
|
|
574
|
+
'rails': r'ruby-on-rails',
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for category, patterns in content_fingerprints.items():
|
|
579
|
+
for tech, pattern in patterns.items():
|
|
580
|
+
if re.search(pattern, content_lower):
|
|
581
|
+
fingerprints[category].add(tech)
|
|
582
|
+
|
|
583
|
+
return fingerprints
|
|
584
|
+
|
|
585
|
+
def _create_spa_fallback_rule(self) -> ReasoningRule:
|
|
586
|
+
"""SPA Fallback 检测规则"""
|
|
587
|
+
|
|
588
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
589
|
+
if not obs.is_html:
|
|
590
|
+
return False
|
|
591
|
+
|
|
592
|
+
html_obs = [o for o in history if o.is_html]
|
|
593
|
+
if len(html_obs) < 3:
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
lengths = [o.content_length for o in html_obs]
|
|
597
|
+
return len(set(lengths)) == 1
|
|
598
|
+
|
|
599
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
600
|
+
length = obs.content_length
|
|
601
|
+
count = len([o for o in history if o.is_html])
|
|
602
|
+
|
|
603
|
+
return Finding(
|
|
604
|
+
what=f"所有 {count} 个不同路径返回完全相同大小的 HTML ({length} 字节)",
|
|
605
|
+
so_what="这是典型的 SPA (Vue.js/React) fallback 行为",
|
|
606
|
+
why="前端服务器(Nginx)配置了 catch-all 路由,将所有请求都路由到 index.html",
|
|
607
|
+
implication="后端 API 不在当前服务器,可能在内网或使用不同的地址",
|
|
608
|
+
strategy="1. 从 JS 中提取后端 API 地址 2. 尝试不同端口/路径探测 3. 如内网地址需要代理访问",
|
|
609
|
+
confidence=0.95,
|
|
610
|
+
level=UnderstandingLevel.CAUSAL,
|
|
611
|
+
evidence=[f"{o.url}" for o in history[-5:] if o.is_html]
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
return ReasoningRule(
|
|
615
|
+
name="spa_fallback_detection",
|
|
616
|
+
description="检测 SPA fallback 行为",
|
|
617
|
+
level=UnderstandingLevel.CAUSAL,
|
|
618
|
+
condition=condition,
|
|
619
|
+
findings_builder=findings_builder,
|
|
620
|
+
priority=100
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
def _create_json_request_html_response_rule(self) -> ReasoningRule:
|
|
624
|
+
"""JSON 请求返回 HTML 的矛盾检测"""
|
|
625
|
+
|
|
626
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
627
|
+
url_lower = obs.url.lower()
|
|
628
|
+
is_json_request = any(ext in url_lower for ext in ['.json', 'swagger', 'api-docs', 'openapi'])
|
|
629
|
+
|
|
630
|
+
return is_json_request and obs.is_html
|
|
631
|
+
|
|
632
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
633
|
+
return Finding(
|
|
634
|
+
what=f"请求 JSON 相关路径 ({obs.url}) 但返回 HTML",
|
|
635
|
+
so_what="该路径在服务端不存在,是前端在模拟",
|
|
636
|
+
why="后端 API 服务器与前端分离,SPA fallback 导致请求被发到前端",
|
|
637
|
+
implication="无法通过前端服务器访问真正的 API 文档",
|
|
638
|
+
strategy="1. 从 JS 或网络请求中识别后端真实地址 2. 直接测试后端地址",
|
|
639
|
+
confidence=0.9,
|
|
640
|
+
level=UnderstandingLevel.CAUSAL,
|
|
641
|
+
evidence=[obs.url]
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return ReasoningRule(
|
|
645
|
+
name="json_request_html_response",
|
|
646
|
+
description="检测 JSON 请求返回 HTML 的矛盾",
|
|
647
|
+
level=UnderstandingLevel.CAUSAL,
|
|
648
|
+
condition=condition,
|
|
649
|
+
findings_builder=findings_builder,
|
|
650
|
+
priority=90
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
def _create_internal_ip_rule(self) -> ReasoningRule:
|
|
654
|
+
"""内网地址发现规则"""
|
|
655
|
+
|
|
656
|
+
INTERNAL_IP_PATTERNS = [
|
|
657
|
+
r'10\.\d+\.\d+\.\d+',
|
|
658
|
+
r'172\.(1[6-9]|2\d|3[01])\.\d+\.\d+',
|
|
659
|
+
r'192\.168\.\d+\.\d+',
|
|
660
|
+
r'127\.\d+\.\d+\.\d+',
|
|
661
|
+
r'localhost',
|
|
662
|
+
]
|
|
663
|
+
|
|
664
|
+
INTERNAL_HOST_PATTERNS = [
|
|
665
|
+
r'\.internal\.',
|
|
666
|
+
r'\.local$',
|
|
667
|
+
r'\.corp\.',
|
|
668
|
+
r'\.internal\.',
|
|
669
|
+
]
|
|
670
|
+
|
|
671
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
672
|
+
content = obs.raw_content
|
|
673
|
+
if not content:
|
|
674
|
+
return False
|
|
675
|
+
|
|
676
|
+
for pattern in INTERNAL_IP_PATTERNS + INTERNAL_HOST_PATTERNS:
|
|
677
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
678
|
+
return True
|
|
679
|
+
|
|
680
|
+
return False
|
|
681
|
+
|
|
682
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
683
|
+
ips = []
|
|
684
|
+
hosts = []
|
|
685
|
+
content = obs.raw_content
|
|
686
|
+
|
|
687
|
+
for pattern in INTERNAL_IP_PATTERNS:
|
|
688
|
+
ips.extend(re.findall(pattern, content))
|
|
689
|
+
|
|
690
|
+
for pattern in INTERNAL_HOST_PATTERNS:
|
|
691
|
+
hosts.extend(re.findall(pattern, content, re.IGNORECASE))
|
|
692
|
+
|
|
693
|
+
all_addresses = ips + hosts
|
|
694
|
+
|
|
695
|
+
return Finding(
|
|
696
|
+
what=f"从响应中发现内网地址: {all_addresses[:3]}",
|
|
697
|
+
so_what="后端 API 在内网环境,前端无法直接访问",
|
|
698
|
+
why="系统采用前后端分离架构,后端部署在内网",
|
|
699
|
+
implication="无法从外部直接测试后端 API",
|
|
700
|
+
strategy="1. 标记内网地址 2. 建议通过代理工具访问 3. 寻找外网暴露的测试环境",
|
|
701
|
+
confidence=0.95,
|
|
702
|
+
level=UnderstandingLevel.STRATEGIC,
|
|
703
|
+
evidence=all_addresses[:5]
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
return ReasoningRule(
|
|
707
|
+
name="internal_ip_discovery",
|
|
708
|
+
description="发现内网地址",
|
|
709
|
+
level=UnderstandingLevel.STRATEGIC,
|
|
710
|
+
condition=condition,
|
|
711
|
+
findings_builder=findings_builder,
|
|
712
|
+
priority=110
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
def _create_waf_detection_rule(self) -> ReasoningRule:
|
|
716
|
+
"""WAF 检测规则"""
|
|
717
|
+
|
|
718
|
+
WAF_SIGNATURES = {
|
|
719
|
+
'360': [r'360waf', r'360safe'],
|
|
720
|
+
'aliyun': [r'aliyuncs\.com', r' Alibaba Cloud'],
|
|
721
|
+
'tencent': [r'tencent-cloud\.net', r'WAF', r'Tencent Cloud'],
|
|
722
|
+
'aws': [r'aws-waf', r'AWSWAF'],
|
|
723
|
+
'cloudflare': [r'cloudflare', r'__cfduid'],
|
|
724
|
+
'imperva': [r'imperva', r'incapsula'],
|
|
725
|
+
'fortinet': [r'fortigate', r'fortiweb'],
|
|
726
|
+
'akamai': [r'akamai', r'akamaigas'],
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
730
|
+
content = obs.raw_content.lower()
|
|
731
|
+
headers_str = str(obs.status_code)
|
|
732
|
+
|
|
733
|
+
for waf, sigs in WAF_SIGNATURES.items():
|
|
734
|
+
for sig in sigs:
|
|
735
|
+
if re.search(sig, content, re.IGNORECASE) or re.search(sig, headers_str, re.IGNORECASE):
|
|
736
|
+
return True
|
|
737
|
+
|
|
738
|
+
if obs.status_code == 403:
|
|
739
|
+
return True
|
|
740
|
+
|
|
741
|
+
return False
|
|
742
|
+
|
|
743
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
744
|
+
detected_waf = "未知 WAF"
|
|
745
|
+
|
|
746
|
+
content = obs.raw_content.lower()
|
|
747
|
+
|
|
748
|
+
for waf, sigs in WAF_SIGNATURES.items():
|
|
749
|
+
for sig in sigs:
|
|
750
|
+
if re.search(sig, content, re.IGNORECASE):
|
|
751
|
+
detected_waf = waf
|
|
752
|
+
break
|
|
753
|
+
|
|
754
|
+
return Finding(
|
|
755
|
+
what=f"检测到 WAF 特征: {detected_waf}",
|
|
756
|
+
so_what="目标受 WAF 保护,需要使用绕过技术",
|
|
757
|
+
why="WAF 会拦截明显的攻击尝试",
|
|
758
|
+
implication="标准 payload 可能被拦截",
|
|
759
|
+
strategy="1. 激活 WAF 绕过策略 2. 使用编码和混淆 3. 尝试绕过 WAF 的已知弱点",
|
|
760
|
+
confidence=0.85,
|
|
761
|
+
level=UnderstandingLevel.STRATEGIC,
|
|
762
|
+
evidence=[f"Status: {obs.status_code}"]
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
return ReasoningRule(
|
|
766
|
+
name="waf_detection",
|
|
767
|
+
description="检测 WAF 防护",
|
|
768
|
+
level=UnderstandingLevel.STRATEGIC,
|
|
769
|
+
condition=condition,
|
|
770
|
+
findings_builder=findings_builder,
|
|
771
|
+
priority=105
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
def _create_tech_fingerprint_rule(self) -> ReasoningRule:
|
|
775
|
+
"""技术栈指纹识别规则"""
|
|
776
|
+
|
|
777
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
778
|
+
return bool(obs.tech_fingerprints)
|
|
779
|
+
|
|
780
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
781
|
+
techs = []
|
|
782
|
+
|
|
783
|
+
for category, fingerprints in obs.tech_fingerprints.items():
|
|
784
|
+
for fp in fingerprints:
|
|
785
|
+
techs.append(f"{category}:{fp}")
|
|
786
|
+
|
|
787
|
+
return Finding(
|
|
788
|
+
what=f"识别到技术栈: {', '.join(techs[:5])}",
|
|
789
|
+
so_what="目标使用特定技术栈,可以针对性地测试",
|
|
790
|
+
why="通过响应头和内容特征识别",
|
|
791
|
+
implication="可以使用针对该技术栈的专项测试",
|
|
792
|
+
strategy=f"1. 启用 {techs[0] if techs else '通用'} 专项测试 2. 调整 payload 适配技术栈",
|
|
793
|
+
confidence=0.8,
|
|
794
|
+
level=UnderstandingLevel.CONTEXT,
|
|
795
|
+
evidence=techs
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
return ReasoningRule(
|
|
799
|
+
name="tech_fingerprint",
|
|
800
|
+
description="识别技术栈指纹",
|
|
801
|
+
level=UnderstandingLevel.CONTEXT,
|
|
802
|
+
condition=condition,
|
|
803
|
+
findings_builder=findings_builder,
|
|
804
|
+
priority=50
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
def _create_error_leak_rule(self) -> ReasoningRule:
|
|
808
|
+
"""错误信息泄露检测"""
|
|
809
|
+
|
|
810
|
+
ERROR_PATTERNS = [
|
|
811
|
+
(r'SQL syntax|MySQL', 'MySQL SQL 错误'),
|
|
812
|
+
(r'PostgreSQL.*ERROR|PSQL', 'PostgreSQL 错误'),
|
|
813
|
+
(r'Oracle.*Exception|java\.sql', 'Java SQL 错误'),
|
|
814
|
+
(r'Syntax error.*Python|Traceback.*Python', 'Python 错误'),
|
|
815
|
+
(r'Syntax error.*PHP|php error', 'PHP 错误'),
|
|
816
|
+
(r'Stack trace|at\s+\w+\.\w+\(', '堆栈跟踪泄露'),
|
|
817
|
+
(r'file_get_contents|fopen.*failed', '文件操作错误'),
|
|
818
|
+
(r'Connection.*refused|Connection.*timeout', '连接错误'),
|
|
819
|
+
]
|
|
820
|
+
|
|
821
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
822
|
+
content = obs.raw_content.lower()
|
|
823
|
+
|
|
824
|
+
for pattern, _ in ERROR_PATTERNS:
|
|
825
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
826
|
+
return True
|
|
827
|
+
|
|
828
|
+
return False
|
|
829
|
+
|
|
830
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
831
|
+
detected_errors = []
|
|
832
|
+
|
|
833
|
+
content = obs.raw_content
|
|
834
|
+
|
|
835
|
+
for pattern, name in ERROR_PATTERNS:
|
|
836
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
837
|
+
detected_errors.append(name)
|
|
838
|
+
|
|
839
|
+
return Finding(
|
|
840
|
+
what=f"发现错误信息泄露: {', '.join(detected_errors[:3])}",
|
|
841
|
+
so_what="应用泄露了技术栈和错误细节",
|
|
842
|
+
why="错误处理不当导致敏感信息输出",
|
|
843
|
+
implication="可用于指纹识别和针对性攻击",
|
|
844
|
+
strategy="1. 收集所有错误信息 2. 用于指纹识别 3. 利用错误信息辅助 SQL 注入",
|
|
845
|
+
confidence=0.85,
|
|
846
|
+
level=UnderstandingLevel.CONTEXT,
|
|
847
|
+
evidence=detected_errors
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
return ReasoningRule(
|
|
851
|
+
name="error_leak_detection",
|
|
852
|
+
description="检测错误信息泄露",
|
|
853
|
+
level=UnderstandingLevel.CONTEXT,
|
|
854
|
+
condition=condition,
|
|
855
|
+
findings_builder=findings_builder,
|
|
856
|
+
priority=70
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
def _create_auth_detection_rule(self) -> ReasoningRule:
|
|
860
|
+
"""认证机制检测"""
|
|
861
|
+
|
|
862
|
+
AUTH_PATTERNS = {
|
|
863
|
+
'jwt': [r'eyJ[a-zA-Z0-9_-]*\.eyJ', r'jwt-token', r'Bearer\s+\w+'],
|
|
864
|
+
'session': [r'sessionid', r'session_id', r'SESSIONID', r'PHPSESSID', r'JSESSIONID'],
|
|
865
|
+
'basic': [r'Authorization:\s*Basic', r'auth_basic'],
|
|
866
|
+
'oauth': [r'oauth', r'OAuth', r'access_token', r'refresh_token'],
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
870
|
+
url_lower = obs.url.lower()
|
|
871
|
+
content_lower = obs.raw_content.lower()
|
|
872
|
+
headers = str(obs.status_code)
|
|
873
|
+
|
|
874
|
+
auth_keywords = ['login', 'signin', 'auth', 'token', 'password', 'credential']
|
|
875
|
+
|
|
876
|
+
for keyword in auth_keywords:
|
|
877
|
+
if keyword in url_lower:
|
|
878
|
+
return True
|
|
879
|
+
|
|
880
|
+
for auth_type, patterns in AUTH_PATTERNS.items():
|
|
881
|
+
for pattern in patterns:
|
|
882
|
+
if re.search(pattern, content_lower, re.IGNORECASE):
|
|
883
|
+
return True
|
|
884
|
+
|
|
885
|
+
if obs.status_code in [401, 403]:
|
|
886
|
+
return True
|
|
887
|
+
|
|
888
|
+
return False
|
|
889
|
+
|
|
890
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
891
|
+
auth_types = []
|
|
892
|
+
content_lower = obs.raw_content
|
|
893
|
+
|
|
894
|
+
for auth_type, patterns in AUTH_PATTERNS.items():
|
|
895
|
+
for pattern in patterns:
|
|
896
|
+
if re.search(pattern, content_lower, re.IGNORECASE):
|
|
897
|
+
auth_types.append(auth_type)
|
|
898
|
+
break
|
|
899
|
+
|
|
900
|
+
return Finding(
|
|
901
|
+
what=f"检测到认证机制: {', '.join(auth_types) if auth_types else '需要认证'}",
|
|
902
|
+
so_what="接口需要认证或使用了特定认证方式",
|
|
903
|
+
why="响应头或内容中包含认证相关信息",
|
|
904
|
+
implication="测试时需要考虑认证绕过",
|
|
905
|
+
strategy="1. 收集认证相关信息 2. 测试 JWT/Token 安全 3. 检查认证绕过漏洞",
|
|
906
|
+
confidence=0.75,
|
|
907
|
+
level=UnderstandingLevel.CONTEXT,
|
|
908
|
+
evidence=auth_types
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
return ReasoningRule(
|
|
912
|
+
name="auth_detection",
|
|
913
|
+
description="检测认证机制",
|
|
914
|
+
level=UnderstandingLevel.CONTEXT,
|
|
915
|
+
condition=condition,
|
|
916
|
+
findings_builder=findings_builder,
|
|
917
|
+
priority=60
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def _create_swagger_discovery_rule(self) -> ReasoningRule:
|
|
921
|
+
"""Swagger/API 文档发现"""
|
|
922
|
+
|
|
923
|
+
SWAGGER_PATTERNS = [
|
|
924
|
+
r'swagger-ui', r'swagger-ui\.html',
|
|
925
|
+
r'api-docs', r'/swagger/',
|
|
926
|
+
r'openapi', r'/v\d+/api-docs',
|
|
927
|
+
]
|
|
928
|
+
|
|
929
|
+
def condition(obs: Observation, history: List[Observation]) -> bool:
|
|
930
|
+
url_lower = obs.url.lower()
|
|
931
|
+
|
|
932
|
+
for pattern in SWAGGER_PATTERNS:
|
|
933
|
+
if re.search(pattern, url_lower):
|
|
934
|
+
return True
|
|
935
|
+
|
|
936
|
+
content_lower = obs.raw_content.lower()
|
|
937
|
+
if 'swagger' in content_lower or 'openapi' in content_lower:
|
|
938
|
+
return True
|
|
939
|
+
|
|
940
|
+
return False
|
|
941
|
+
|
|
942
|
+
def findings_builder(obs: Observation, history: List[Observation]) -> Finding:
|
|
943
|
+
return Finding(
|
|
944
|
+
what=f"发现 API 文档路径: {obs.url}",
|
|
945
|
+
so_what="存在可访问的 API 文档,可能泄露敏感信息",
|
|
946
|
+
why="服务器配置允许访问 API 文档",
|
|
947
|
+
implication="可以直接从文档获取 API 结构",
|
|
948
|
+
strategy="1. 访问 API 文档获取完整端点列表 2. 分析文档中的敏感接口 3. 尝试未文档化的端点",
|
|
949
|
+
confidence=0.9,
|
|
950
|
+
level=UnderstandingLevel.STRATEGIC,
|
|
951
|
+
evidence=[obs.url]
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
return ReasoningRule(
|
|
955
|
+
name="swagger_discovery",
|
|
956
|
+
description="发现 Swagger/API 文档",
|
|
957
|
+
level=UnderstandingLevel.STRATEGIC,
|
|
958
|
+
condition=condition,
|
|
959
|
+
findings_builder=findings_builder,
|
|
960
|
+
priority=80
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
def _rule_to_insight_type(self, rule: ReasoningRule) -> InsightType:
|
|
964
|
+
"""根据规则层级映射到洞察类型"""
|
|
965
|
+
mapping = {
|
|
966
|
+
UnderstandingLevel.SURFACE: InsightType.OBSERVATION,
|
|
967
|
+
UnderstandingLevel.CONTEXT: InsightType.INFERENCE,
|
|
968
|
+
UnderstandingLevel.CAUSAL: InsightType.PATTERN,
|
|
969
|
+
UnderstandingLevel.STRATEGIC: InsightType.STRATEGY_CHANGE,
|
|
970
|
+
}
|
|
971
|
+
return mapping.get(rule.level, InsightType.INFERENCE)
|
|
972
|
+
|
|
973
|
+
def _generate_observation_id(self) -> str:
|
|
974
|
+
return f"obs_{int(time.time() * 1000)}"
|
|
975
|
+
|
|
976
|
+
def _generate_insight_id(self) -> str:
|
|
977
|
+
return f"ins_{int(time.time() * 1000)}"
|
|
978
|
+
|
|
979
|
+
def get_insight_store(self) -> InsightStore:
|
|
980
|
+
"""获取洞察存储"""
|
|
981
|
+
return self.insight_store
|
|
982
|
+
|
|
983
|
+
def get_observations(self) -> List[Observation]:
|
|
984
|
+
"""获取观察历史"""
|
|
985
|
+
return self.observation_history
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def create_reasoner() -> Reasoner:
|
|
989
|
+
"""创建推理引擎的工厂函数"""
|
|
990
|
+
return Reasoner()
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
if __name__ == "__main__":
|
|
994
|
+
import json
|
|
995
|
+
|
|
996
|
+
reasoner = create_reasoner()
|
|
997
|
+
|
|
998
|
+
test_responses = [
|
|
999
|
+
{
|
|
1000
|
+
'url': 'http://example.com/login',
|
|
1001
|
+
'method': 'GET',
|
|
1002
|
+
'status_code': 200,
|
|
1003
|
+
'content_type': 'text/html',
|
|
1004
|
+
'content': '<!DOCTYPE html><html><div id="app"></div><script src="/static/js/chunk-vendors.js"></script></html>' * 10,
|
|
1005
|
+
'source': 'html',
|
|
1006
|
+
'response_time': 0.5
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
'url': 'http://example.com/admin',
|
|
1010
|
+
'method': 'GET',
|
|
1011
|
+
'status_code': 200,
|
|
1012
|
+
'content_type': 'text/html',
|
|
1013
|
+
'content': '<!DOCTYPE html><html><div id="app"></div><script src="/static/js/chunk-vendors.js"></script></html>' * 10,
|
|
1014
|
+
'source': 'html',
|
|
1015
|
+
'response_time': 0.5
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
'url': 'http://example.com/api/swagger.json',
|
|
1019
|
+
'method': 'GET',
|
|
1020
|
+
'status_code': 200,
|
|
1021
|
+
'content_type': 'text/html',
|
|
1022
|
+
'content': '<!DOCTYPE html><html><div id="app"></div></html>' * 10,
|
|
1023
|
+
'source': 'api',
|
|
1024
|
+
'response_time': 0.3
|
|
1025
|
+
}
|
|
1026
|
+
]
|
|
1027
|
+
|
|
1028
|
+
for resp in test_responses:
|
|
1029
|
+
insights = reasoner.observe_and_reason(resp)
|
|
1030
|
+
|
|
1031
|
+
print(f"\n[*] URL: {resp['url']}")
|
|
1032
|
+
for insight in insights:
|
|
1033
|
+
print(f" Insight: {insight.content}")
|
|
1034
|
+
if insight.findings:
|
|
1035
|
+
f = insight.findings[0]
|
|
1036
|
+
print(f" → What: {f.what}")
|
|
1037
|
+
print(f" → So What: {f.so_what}")
|
|
1038
|
+
print(f" → Confidence: {f.confidence}")
|
|
1039
|
+
|
|
1040
|
+
print("\n[*] Insight Store Summary:")
|
|
1041
|
+
summary = reasoner.insight_store.get_summary()
|
|
1042
|
+
print(json.dumps(summary, indent=2))
|