agent-bypass 2.16.0 → 2.18.0
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/AGENT_ADDITIVE_CHANGELOG.md +21 -1
- package/README.md +7 -2
- package/SKILL.md +43 -118
- package/bin/install.js +28 -1
- package/dsh/persona.md +2 -2
- package/package.json +1 -1
- package/references/agent-routing.md +1 -1
- package/references/bruteforce-playbook.md +87 -0
- package/references/fake-ip-tun.md +114 -0
- package/references/tool-selection.md +9 -1
- package/scripts/doh_resolve.sh +110 -5
- package/scripts/fastrecon.py +55 -6
- package/scripts/netdns.py +192 -0
- package/scripts/origin_trace.py +535 -0
- package/scripts/portscan.py +119 -13
- package/tests/test_regressions.py +144 -0
package/scripts/portscan.py
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
并发端口扫描(无需 nmap
|
|
4
|
+
并发端口扫描(无需 nmap)——iSH / TUN / fake-IP 友好版
|
|
5
5
|
|
|
6
6
|
提示: 手机环境(iSH/Termux)优先用 fastrecon.py scan —— selectors 并发实测快 5-45 倍
|
|
7
7
|
|
|
8
|
+
iSH + TUN(Clash/mihomo/sing-box)常见问题:系统 DNS 被劫持成 fake-IP(198.18.0.0/15),
|
|
9
|
+
直接扫域名会得到不可路由假地址 → 全部超时/误判。本脚本默认自动检测并用 DoH 拿真实 IP。
|
|
10
|
+
|
|
8
11
|
用法:
|
|
9
12
|
python3 portscan.py <host> [--ports 1-65535] [--conc 500] [--timeout 2]
|
|
10
13
|
python3 portscan.py 1.2.3.4 --ports top100
|
|
11
|
-
python3 portscan.py
|
|
14
|
+
python3 portscan.py example.com --resolve doh # 强制 DoH 解析
|
|
15
|
+
python3 portscan.py example.com --resolve none # 只扫 IP,不解析
|
|
16
|
+
python3 portscan.py example.com --allow-fake # 允许扫 fake-IP(一般不要)
|
|
12
17
|
|
|
13
|
-
输出: jsonl
|
|
18
|
+
输出: jsonl(每行一个开放端口,含 ip / host / port / service)
|
|
19
|
+
退出码: 0 正常 / 2 参数错误 / 3 目标解析失败或只剩 fake-IP
|
|
14
20
|
"""
|
|
15
21
|
import argsmini as argparse
|
|
16
|
-
import asyncio, json, re, sys
|
|
22
|
+
import asyncio, json, os, re, sys, time
|
|
23
|
+
|
|
24
|
+
try: # 共享 DNS 工具(同目录 scripts/netdns.py)
|
|
25
|
+
import netdns
|
|
26
|
+
except Exception: # 单文件拷贝场景:退化为基础 socket 解析
|
|
27
|
+
netdns = None
|
|
28
|
+
|
|
29
|
+
ISH = os.path.isdir('/var/minis') or bool(os.environ.get('MINIS_ROOT'))
|
|
17
30
|
|
|
18
31
|
# 100 端口 = 40 高价值服务 + 60 经典(对齐 nmap top100 覆盖面)
|
|
19
32
|
TOP100 = [21,22,23,25,53,80,110,111,135,139,143,443,445,993,995,1433,1521,1723,
|
|
@@ -40,6 +53,11 @@ COMMON = {
|
|
|
40
53
|
50000:"db2"
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
FAKE_HINT = ("[fake-IP] 目标解析到 fake-IP(TUN 劫持 DNS),该地址不可信:\n"
|
|
57
|
+
" 1) 用 DoH 真 IP:python3 portscan.py <真实IP> 或 --resolve doh\n"
|
|
58
|
+
" 2) 关掉代理的 TUN 全局/DNS 劫持,或把目标加 DIRECT/fake-ip-filter\n"
|
|
59
|
+
" 3) 溯源同理:先 DoH 拿真 IP,再扫 IP(nmap -n -Pn <ip>)")
|
|
60
|
+
|
|
43
61
|
|
|
44
62
|
def parse_ports(s):
|
|
45
63
|
s = (s or "").strip().lower()
|
|
@@ -74,6 +92,53 @@ def parse_ports(s):
|
|
|
74
92
|
return sorted(out)
|
|
75
93
|
|
|
76
94
|
|
|
95
|
+
def split_host(raw):
|
|
96
|
+
"""'https://example.com:8443/x' / 'example.com' / '1.2.3.4' → ('example.com', 8443|None)"""
|
|
97
|
+
s = (raw or "").strip()
|
|
98
|
+
s = re.sub(r'^[a-zA-Z][a-zA-Z0-9+.-]*://', '', s)
|
|
99
|
+
s = s.split('/', 1)[0]
|
|
100
|
+
m = re.match(r'^(\[[^\]]+\]|[^:\s/]+)(?::(\d{1,5}))?$', s)
|
|
101
|
+
if not m:
|
|
102
|
+
return None, None
|
|
103
|
+
host = m.group(1).strip('[]')
|
|
104
|
+
port = int(m.group(2)) if m.group(2) else None
|
|
105
|
+
if port is not None and not (1 <= port <= 65535):
|
|
106
|
+
return None, None
|
|
107
|
+
return host, port
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_ipv4_literal(s):
|
|
111
|
+
if netdns is not None:
|
|
112
|
+
return netdns.is_ipv4(s)
|
|
113
|
+
return bool(re.match(r'^\d{1,3}(?:\.\d{1,3}){3}$', str(s)))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def is_fake_ip(ip):
|
|
117
|
+
if netdns is not None:
|
|
118
|
+
return netdns.is_fake_ip(ip)
|
|
119
|
+
return str(ip).startswith(('198.18.', '198.19.', '240.', '198.51.100.', '203.0.113.'))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_target(host, mode="auto", timeout=6.0):
|
|
123
|
+
"""返回 (ip, source, fake_list)。mode: auto/system/doh/none。"""
|
|
124
|
+
if is_ipv4_literal(host):
|
|
125
|
+
return host, "ip", []
|
|
126
|
+
if mode == "none":
|
|
127
|
+
return None, "none", []
|
|
128
|
+
if netdns is not None:
|
|
129
|
+
ips, source = netdns.resolve_real(host, prefer=("system" if mode == "system" else "doh" if mode == "doh" else "auto"),
|
|
130
|
+
timeout=timeout)
|
|
131
|
+
else:
|
|
132
|
+
try:
|
|
133
|
+
import socket as _s
|
|
134
|
+
ips, source = [_s.gethostbyname(host)], "system"
|
|
135
|
+
except Exception:
|
|
136
|
+
ips, source = [], "fail"
|
|
137
|
+
fake = [ip for ip in ips if is_fake_ip(ip)]
|
|
138
|
+
real = [ip for ip in ips if not is_fake_ip(ip)]
|
|
139
|
+
return (real[0] if real else (ips[0] if ips else None)), source, fake
|
|
140
|
+
|
|
141
|
+
|
|
77
142
|
async def probe(host, port, timeout=2.0):
|
|
78
143
|
try:
|
|
79
144
|
r, w = await asyncio.wait_for(
|
|
@@ -88,31 +153,45 @@ async def probe(host, port, timeout=2.0):
|
|
|
88
153
|
return None
|
|
89
154
|
|
|
90
155
|
|
|
91
|
-
async def scan(host, ports, conc=500, timeout=2.0):
|
|
156
|
+
async def scan(host, ports, conc=500, timeout=2.0, deadline=None):
|
|
157
|
+
"""deadline(time.monotonic 秒)用于总时限:到点停发新探测,返回已扫到的部分结果。
|
|
158
|
+
返回 (open_ports, timed_out)。"""
|
|
92
159
|
sem = asyncio.Semaphore(conc)
|
|
93
160
|
open_ports = []
|
|
161
|
+
timed_out = False
|
|
94
162
|
|
|
95
163
|
async def one(p):
|
|
164
|
+
nonlocal timed_out
|
|
165
|
+
if deadline is not None and time.monotonic() > deadline:
|
|
166
|
+
timed_out = True
|
|
167
|
+
return
|
|
96
168
|
async with sem:
|
|
97
169
|
r = await probe(host, p, timeout)
|
|
98
170
|
if r:
|
|
99
171
|
open_ports.append(r)
|
|
100
172
|
|
|
101
173
|
await asyncio.gather(*[one(p) for p in ports])
|
|
102
|
-
return sorted(open_ports)
|
|
174
|
+
return sorted(open_ports), timed_out
|
|
103
175
|
|
|
104
176
|
|
|
105
177
|
def main():
|
|
106
|
-
ap = argparse.ArgumentParser(description="并发端口扫描(无需 nmap)")
|
|
178
|
+
ap = argparse.ArgumentParser(description="并发端口扫描(无需 nmap;自动绕 fake-IP)")
|
|
107
179
|
ap.add_argument("host")
|
|
108
180
|
ap.add_argument("--ports", default="top20",
|
|
109
181
|
help="1-65535 / top100 / top20 / 22,80,443")
|
|
110
182
|
ap.add_argument("--conc", type=int, default=500, help="并发数")
|
|
111
183
|
ap.add_argument("--timeout", type=float, default=2.0)
|
|
112
184
|
ap.add_argument("--json", action="store_true", help="只输出 JSONL")
|
|
185
|
+
ap.add_argument("--resolve", choices=["auto", "system", "doh", "none"], default="auto",
|
|
186
|
+
help="DNS 解析方式: auto(系统→DoH) / system / doh / none(只接受 IP)")
|
|
187
|
+
ap.add_argument("--allow-fake", action="store_true",
|
|
188
|
+
help="允许扫描 fake-IP 地址(默认拒绝,防误判)")
|
|
189
|
+
ap.add_argument("--deadline", type=float, default=0,
|
|
190
|
+
help="总时限秒(0=自动:按端口数/并发估算,最长 600s;到点返回部分结果)")
|
|
113
191
|
a = ap.parse_args()
|
|
114
192
|
|
|
115
|
-
|
|
193
|
+
name, inline_port = split_host(a.host)
|
|
194
|
+
if not name:
|
|
116
195
|
print("[错误] host 只接受域名或 IP(可带 :端口)", file=sys.stderr)
|
|
117
196
|
sys.exit(2)
|
|
118
197
|
try:
|
|
@@ -124,20 +203,47 @@ def main():
|
|
|
124
203
|
if conc != a.conc:
|
|
125
204
|
print(f"[提示] 并发钳制为 {conc}(允许 1-2000)", file=sys.stderr)
|
|
126
205
|
timeout = max(0.1, min(a.timeout, 30.0))
|
|
127
|
-
if
|
|
128
|
-
|
|
206
|
+
if ISH and conc > 200:
|
|
207
|
+
conc = 200
|
|
208
|
+
print("[提示] iSH 环境并发降至 200(CPU 模拟 + 弱网更稳)", file=sys.stderr)
|
|
209
|
+
if ISH and timeout <= 2.0:
|
|
210
|
+
timeout = 3.0
|
|
211
|
+
print("[提示] iSH 环境超时上调至 3.0s", file=sys.stderr)
|
|
212
|
+
if inline_port and inline_port not in ports:
|
|
213
|
+
ports = sorted(set(ports) | {inline_port})
|
|
214
|
+
|
|
215
|
+
ip, source, fake = resolve_target(name, mode=a.resolve, timeout=max(timeout, 6.0))
|
|
216
|
+
if not ip:
|
|
217
|
+
print(f"[错误] 无法解析 {name}(--resolve {a.resolve})", file=sys.stderr)
|
|
218
|
+
print(FAKE_HINT, file=sys.stderr)
|
|
219
|
+
sys.exit(3)
|
|
220
|
+
if fake and not any(not is_fake_ip(x) for x in [ip]):
|
|
221
|
+
if not a.allow_fake:
|
|
222
|
+
print(f"[错误] {name} 只解析到 fake-IP:{','.join(fake)}", file=sys.stderr)
|
|
223
|
+
print(FAKE_HINT, file=sys.stderr)
|
|
224
|
+
sys.exit(3)
|
|
225
|
+
print(f"[警告] 正在扫描 fake-IP(--allow-fake):{ip}(结果不可信)", file=sys.stderr)
|
|
226
|
+
elif source == "doh":
|
|
227
|
+
print(f"[提示] 系统解析为 fake/IP 不可信,已用 DoH 真 IP: {ip}", file=sys.stderr)
|
|
228
|
+
|
|
129
229
|
if not a.json:
|
|
130
|
-
print(f"# 扫描 {
|
|
230
|
+
print(f"# 扫描 {name} → {ip}({source})共 {len(ports)} 端口 (并发 {conc})",
|
|
131
231
|
file=sys.stderr)
|
|
132
232
|
|
|
133
|
-
|
|
233
|
+
overall = a.deadline if a.deadline > 0 else min(600.0, max(20.0, len(ports) * timeout / conc * 3 + 10))
|
|
234
|
+
deadline = time.monotonic() + max(5.0, min(overall, 3600.0))
|
|
235
|
+
open_ports, timed_out = asyncio.run(scan(ip, ports, conc, timeout, deadline))
|
|
236
|
+
if timed_out:
|
|
237
|
+
print(f"# 达到总时限 {overall:.0f}s:返回部分结果(可加大 --deadline 或降低端口数)", file=sys.stderr)
|
|
134
238
|
|
|
135
239
|
for p in open_ports:
|
|
136
|
-
rec = {"host":
|
|
240
|
+
rec = {"host": name, "ip": ip, "port": p, "service": COMMON.get(p, "?")}
|
|
137
241
|
print(json.dumps(rec, ensure_ascii=False))
|
|
138
242
|
|
|
139
243
|
if not a.json:
|
|
140
244
|
print(f"# 开放 {len(open_ports)} 个: {open_ports}", file=sys.stderr)
|
|
245
|
+
if ISH:
|
|
246
|
+
print("# iSH 提示:并发已限制、超时已上调;长扫描用 --deadline 控制,配合 nohup 后台跑", file=sys.stderr)
|
|
141
247
|
|
|
142
248
|
|
|
143
249
|
if __name__ == "__main__":
|
|
@@ -250,5 +250,149 @@ class DoctorTests(unittest.TestCase):
|
|
|
250
250
|
self.assertEqual(list(Path(home).rglob("*")), [], "doctor 不应写文件")
|
|
251
251
|
|
|
252
252
|
|
|
253
|
+
class NetDnsScanOriginTests(unittest.TestCase):
|
|
254
|
+
"""fake-IP/TUN:netdns 判定 + portscan 解析守卫 + origin_trace 解析(全部离线)。"""
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def setUpClass(cls):
|
|
258
|
+
sys.path.insert(0, str(SCRIPTS))
|
|
259
|
+
import netdns
|
|
260
|
+
import portscan
|
|
261
|
+
import origin_trace
|
|
262
|
+
cls.nd = netdns
|
|
263
|
+
cls.ps = portscan
|
|
264
|
+
cls.ot = origin_trace
|
|
265
|
+
|
|
266
|
+
def test_fake_ip_ranges(self):
|
|
267
|
+
for ip in ("198.18.0.5", "198.19.255.1", "240.1.2.3", "198.51.100.7", "203.0.113.9"):
|
|
268
|
+
self.assertTrue(self.nd.is_fake_ip(ip), ip)
|
|
269
|
+
for ip in ("1.1.1.1", "8.8.8.8", "192.168.1.1", "10.0.0.1"):
|
|
270
|
+
self.assertFalse(self.nd.is_fake_ip(ip), ip)
|
|
271
|
+
|
|
272
|
+
def test_parse_dns_json(self):
|
|
273
|
+
sample = ('{"Answer":[{"type":1,"data":"93.184.216.34"},'
|
|
274
|
+
'{"type":28,"data":"2606:2800::1"},{"type":1,"data":"198.18.0.1"}]}')
|
|
275
|
+
self.assertEqual(self.nd._parse_dns_json(sample, "A"), ["93.184.216.34", "198.18.0.1"])
|
|
276
|
+
self.assertEqual(self.nd._parse_dns_json(sample, "AAAA"), ["2606:2800::1"])
|
|
277
|
+
self.assertEqual(self.nd._parse_dns_json("not-json", "A"), [])
|
|
278
|
+
|
|
279
|
+
def test_portscan_split_host_fake_guard(self):
|
|
280
|
+
self.assertEqual(self.ps.split_host("https://example.com:8443/x"), ("example.com", 8443))
|
|
281
|
+
self.assertEqual(self.ps.split_host("1.2.3.4"), ("1.2.3.4", None))
|
|
282
|
+
self.assertIsNone(self.ps.split_host("bad host")[0])
|
|
283
|
+
self.assertTrue(self.ps.is_fake_ip("198.18.1.2"))
|
|
284
|
+
ip, src, fake = self.ps.resolve_target("198.18.1.2", mode="auto")
|
|
285
|
+
self.assertEqual((ip, src), ("198.18.1.2", "ip"))
|
|
286
|
+
self.assertEqual(fake, [])
|
|
287
|
+
|
|
288
|
+
def test_origin_trace_parsers(self):
|
|
289
|
+
ct = '[{"name_value":"a.example.com\\n*.example.com\\nother.test"}]'
|
|
290
|
+
self.assertEqual(self.ot.parse_crtsh(ct, "example.com"), ["a.example.com", "example.com"])
|
|
291
|
+
self.assertEqual(self.ot.extract_title("<title> Hi There </title>"), "Hi There")
|
|
292
|
+
self.assertEqual(self.ot.cdn_markers({"cf-ray": "x", "server": "nginx"}), ["cf-ray"])
|
|
293
|
+
base = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": ["cf-ray"]}
|
|
294
|
+
cand = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": [], "cert_domain_ok": True}
|
|
295
|
+
score, reasons = self.ot.score_candidate(base, cand)
|
|
296
|
+
self.assertGreaterEqual(score, 5)
|
|
297
|
+
self.assertIn("title", reasons)
|
|
298
|
+
self.assertLess(self.ot.score_candidate(base, {"cdn": ["cf-ray"]})[0], score)
|
|
299
|
+
|
|
300
|
+
def test_doh_shell_syntax(self):
|
|
301
|
+
sh = shutil.which("sh")
|
|
302
|
+
if not sh:
|
|
303
|
+
self.skipTest("no sh")
|
|
304
|
+
r = _tmp_run([sh, "-n", str(SCRIPTS / "doh_resolve.sh")])
|
|
305
|
+
self.assertEqual(r.returncode, 0, r.stderr)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class SkillCompactTests(unittest.TestCase):
|
|
309
|
+
"""SKILL.md 体积/去重回归:防止再次膨胀。"""
|
|
310
|
+
|
|
311
|
+
def test_skill_structure_and_size(self):
|
|
312
|
+
skill = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
313
|
+
self.assertEqual(skill.count("<!-- module:"), 35)
|
|
314
|
+
self.assertNotIn("## 修正对照", skill, "重复的修正对照表已移除,勿再加回")
|
|
315
|
+
self.assertLess(len(skill.encode("utf-8")), 132000, "SKILL.md 体积超限(保持精简,长内容放 references/)")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class HangProtectionTests(unittest.TestCase):
|
|
319
|
+
"""防卡死:DNS 硬超时、扫描总时限、溯源预算/同名 CDN 扣分(全部离线)。"""
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def setUpClass(cls):
|
|
323
|
+
sys.path.insert(0, str(SCRIPTS))
|
|
324
|
+
import netdns, portscan, origin_trace
|
|
325
|
+
cls.nd = netdns
|
|
326
|
+
cls.ps = portscan
|
|
327
|
+
cls.ot = origin_trace
|
|
328
|
+
|
|
329
|
+
def test_system_resolve_hard_timeout(self):
|
|
330
|
+
import time as _t
|
|
331
|
+
orig = self.nd._system_resolve_impl
|
|
332
|
+
self.nd._system_resolve_impl = lambda name: (_t.sleep(3), ["1.2.3.4"])[1]
|
|
333
|
+
t0 = _t.monotonic()
|
|
334
|
+
try:
|
|
335
|
+
ips = self.nd.system_resolve("example.com", timeout=0.2)
|
|
336
|
+
finally:
|
|
337
|
+
self.nd._system_resolve_impl = orig
|
|
338
|
+
self.assertEqual(ips, [])
|
|
339
|
+
self.assertLess(_t.monotonic() - t0, 1.5, "system_resolve 必须有硬超时,不能挂住")
|
|
340
|
+
|
|
341
|
+
def test_portscan_total_deadline(self):
|
|
342
|
+
import asyncio, time as _t
|
|
343
|
+
found, timed_out = asyncio.run(
|
|
344
|
+
self.ps.scan("127.0.0.1", [9, 10], conc=1, timeout=0.05,
|
|
345
|
+
deadline=_t.monotonic() - 1))
|
|
346
|
+
self.assertEqual(found, [])
|
|
347
|
+
self.assertTrue(timed_out)
|
|
348
|
+
|
|
349
|
+
def test_origin_same_cdn_penalty(self):
|
|
350
|
+
base = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": ["cf-ray"]}
|
|
351
|
+
cand_cdn = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": ["cf-ray"]}
|
|
352
|
+
cand_origin = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": []}
|
|
353
|
+
s_cdn, r_cdn = self.ot.score_candidate(base, cand_cdn)
|
|
354
|
+
s_org, r_org = self.ot.score_candidate(base, cand_origin)
|
|
355
|
+
self.assertIn("same-cdn", r_cdn)
|
|
356
|
+
self.assertLess(s_cdn, s_org)
|
|
357
|
+
|
|
358
|
+
def test_passive_ct_parsers(self):
|
|
359
|
+
cs = '[{"dns_names":["www.example.com","*.example.com","x.test"]}]'
|
|
360
|
+
self.assertEqual(self.ot.parse_certspotter(cs, "example.com"), ["www.example.com", "example.com"])
|
|
361
|
+
otx = ('{"passive_dns":[{"hostname":"mail.example.com","address":"1.2.3.4"},'
|
|
362
|
+
'{"hostname":"x.test","address":"9.9.9.9"},'
|
|
363
|
+
'{"hostname":"cdn.example.com","address":"198.18.0.9"}]}')
|
|
364
|
+
hosts, ips = self.ot.parse_otx(otx, "example.com")
|
|
365
|
+
self.assertIn("mail.example.com", hosts)
|
|
366
|
+
self.assertEqual(ips, ["1.2.3.4"], "被动 DNS 的 fake-IP 应被过滤")
|
|
367
|
+
hosts, ips = self.ot.parse_hackertarget("vpn.example.com,5.6.7.8\nAPI count exceeded", "example.com")
|
|
368
|
+
self.assertEqual(hosts, ["vpn.example.com"])
|
|
369
|
+
self.assertEqual(ips, ["5.6.7.8"])
|
|
370
|
+
self.assertEqual(self.ot.parse_anubis('["a.example.com","b.test"]', "example.com"), ["a.example.com"])
|
|
371
|
+
self.assertEqual(self.ot.parse_rapiddns("<td>oa.example.com</td><td>x.test</td>", "example.com"),
|
|
372
|
+
["oa.example.com"])
|
|
373
|
+
for src in ("crt.sh", "certspotter", "otx", "hackertarget", "anubis", "rapiddns"):
|
|
374
|
+
self.assertIn(src, self.ot.CT_SOURCES)
|
|
375
|
+
|
|
376
|
+
def test_ct_multi_source_fallback(self):
|
|
377
|
+
def fake(name, domain, timeout):
|
|
378
|
+
if name == "crt.sh":
|
|
379
|
+
raise RuntimeError("502 Bad Gateway")
|
|
380
|
+
return (["s1.example.com", "s2.example.com"], ["1.2.3.4"])
|
|
381
|
+
orig = self.ot._ct_fetch
|
|
382
|
+
self.ot._ct_fetch = fake
|
|
383
|
+
try:
|
|
384
|
+
hosts, ips, status = self.ot.fetch_passive_names(
|
|
385
|
+
"example.com", sources=["crt.sh", "otx"], budget=5)
|
|
386
|
+
finally:
|
|
387
|
+
self.ot._ct_fetch = orig
|
|
388
|
+
self.assertEqual(hosts, ["s1.example.com", "s2.example.com"])
|
|
389
|
+
self.assertEqual(ips, ["1.2.3.4"])
|
|
390
|
+
self.assertIn("失败", status["crt.sh"], "单源 502 应被记录并跳过")
|
|
391
|
+
|
|
392
|
+
def test_origin_bad_flag_rejected(self):
|
|
393
|
+
r = _tmp_run([sys.executable, str(SCRIPTS / "origin_trace.py"), "--bogus"])
|
|
394
|
+
self.assertEqual(r.returncode, 2)
|
|
395
|
+
|
|
396
|
+
|
|
253
397
|
if __name__ == "__main__":
|
|
254
398
|
unittest.main()
|