agent-bypass 2.11.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.
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ JS 静态文件提取器(§01 Step 8 侧枝折射)
5
+
6
+ 从 JS bundle 里提取:
7
+ · API 接口路径
8
+ · 硬编码密钥/token
9
+ · sourcemap 地址
10
+ · 内网地址 / 埋点上报地址
11
+ · 注释 / TODO / 调试信息
12
+
13
+ 用法:
14
+ python3 jsrip.py https://target.com/static/app.js
15
+ python3 jsrip.py https://target.com --scan # 自动找页面引用的 JS
16
+ python3 jsrip.py https://target.com/app.js --json
17
+ """
18
+ import argparse, json, re, ssl, sys, urllib.parse
19
+ import urllib.request, urllib.error
20
+
21
+ CTX = ssl.create_default_context()
22
+ CTX.check_hostname = False
23
+ CTX.verify_mode = ssl.CERT_NONE
24
+ UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36"
25
+
26
+ # 提取规则
27
+ RULES = {
28
+ "api_path": re.compile(r'["\'`](/[a-zA-Z0-9_\-/]{2,60}(?:/[a-zA-Z0-9_\-{}:.]{1,40})*)["\'`]'),
29
+ "full_url": re.compile(r'https?://[a-zA-Z0-9._\-]+(?::\d+)?[a-zA-Z0-9._\-/?#=&%]*'),
30
+ "secret": re.compile(r'(?i)(?:api[_-]?key|apikey|secret|token|passwd|password|auth)["\']?\s*[:=]\s*["\']([^"\']{8,80})["\']'),
31
+ "aws_key": re.compile(r'AKIA[0-9A-Z]{16}'),
32
+ "jwt": re.compile(r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'),
33
+ "sourcemap": re.compile(r'//[#@]\s*sourceMappingURL=([^\s]+)'),
34
+ "internal_ip": re.compile(r'\b(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b'),
35
+ "comment": re.compile(r'/\*\s*(?:TODO|FIXME|HACK|XXX|NOTE)[^*]{0,200}\*/', re.I),
36
+ "graphql": re.compile(r'(?:query|mutation)\s+\w+\s*[({]'),
37
+ "websocket": re.compile(r'wss?://[a-zA-Z0-9._\-:]+[a-zA-Z0-9._\-/?#=&%]*'),
38
+ }
39
+
40
+
41
+ def get(url, timeout=20):
42
+ h = {"User-Agent": UA, "Accept": "*/*"}
43
+ try:
44
+ r = urllib.request.Request(url, headers=h)
45
+ with urllib.request.urlopen(r, timeout=timeout, context=CTX) as x:
46
+ return x.read(3000000).decode("utf-8", "ignore")
47
+ except Exception as e:
48
+ print(f" [错误] {url}: {str(e)[:60]}", file=sys.stderr)
49
+ return ""
50
+
51
+
52
+ def find_js_urls(base):
53
+ """从 HTML 里找 JS 引用"""
54
+ html = get(base)
55
+ if not html:
56
+ return []
57
+ urls = set()
58
+ for m in re.finditer(r'<script[^>]+src=["\']([^"\']+)["\']', html, re.I):
59
+ urls.add(m.group(1))
60
+ for m in re.finditer(r'<link[^>]+href=["\']([^"\']+\.js)["\']', html, re.I):
61
+ urls.add(m.group(1))
62
+ out = []
63
+ for u in urls:
64
+ out.append(urllib.parse.urljoin(base, u))
65
+ return out
66
+
67
+
68
+ def analyze(url, content):
69
+ hits = {}
70
+ for name, rx in RULES.items():
71
+ found = rx.findall(content)
72
+ if not found:
73
+ continue
74
+ # 去重 + 截断
75
+ uniq = []
76
+ seen = set()
77
+ for f in found:
78
+ v = f if isinstance(f, str) else f[0]
79
+ v = v.strip()
80
+ if len(v) < 3 or v in seen:
81
+ continue
82
+ seen.add(v)
83
+ uniq.append(v[:150])
84
+ if uniq:
85
+ hits[name] = uniq[:40]
86
+ return hits
87
+
88
+
89
+ def main():
90
+ ap = argparse.ArgumentParser(description="JS 静态文件提取器")
91
+ ap.add_argument("target", help="JS 文件 URL 或站点 URL(配合 --scan)")
92
+ ap.add_argument("--scan", action="store_true", help="从 HTML 自动找 JS")
93
+ ap.add_argument("--json", action="store_true")
94
+ ap.add_argument("--depth", type=int, default=5, help="最多分析几个 JS")
95
+ a = ap.parse_args()
96
+
97
+ targets = []
98
+ if a.scan:
99
+ print(f"[*] 扫描 {a.target} 引用的 JS...", file=sys.stderr)
100
+ targets = find_js_urls(a.target)[:a.depth]
101
+ if not targets:
102
+ print(" (未找到 JS 引用)", file=sys.stderr)
103
+ else:
104
+ targets = [a.target]
105
+
106
+ allhits = {}
107
+ for u in targets:
108
+ print(f"[*] 分析 {u}", file=sys.stderr)
109
+ c = get(u)
110
+ if not c:
111
+ continue
112
+ hits = analyze(u, c)
113
+ if hits:
114
+ allhits[u] = hits
115
+
116
+ if a.json:
117
+ print(json.dumps(allhits, ensure_ascii=False, indent=1))
118
+ return
119
+
120
+ # 人类可读输出
121
+ LABEL = {
122
+ "api_path": "接口路径", "full_url": "完整 URL", "secret": "★ 疑似密钥",
123
+ "aws_key": "★★ AWS Key", "jwt": "★★ JWT", "sourcemap": "★ sourcemap",
124
+ "internal_ip": "★ 内网 IP", "comment": "注释/TODO",
125
+ "graphql": "GraphQL", "websocket": "WebSocket",
126
+ }
127
+ for u, hits in allhits.items():
128
+ print(f"\n{'='*66}\n{u}\n{'='*66}")
129
+ for name in ("aws_key", "jwt", "secret", "sourcemap", "internal_ip",
130
+ "websocket", "graphql", "api_path", "full_url", "comment"):
131
+ if name not in hits:
132
+ continue
133
+ print(f"\n[{LABEL.get(name, name)}] {len(hits[name])} 条")
134
+ for v in hits[name][:25]:
135
+ print(f" {v}")
136
+ if len(hits[name]) > 25:
137
+ print(f" ... 还有 {len(hits[name])-25} 条")
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 自适应变异调度器(§01 Step 6a)
5
+
6
+ 对同一个请求,按 L1→L5 层级自动生成变异,逐个发送,
7
+ 对比响应差异,找出能绕过的那一个。
8
+
9
+ 用法:
10
+ # 基础:对 URL 做路径变异
11
+ python3 mutate.py https://target.com/admin
12
+
13
+ # 带阻断基线:先记 403 的响应特征,再找能绕过的
14
+ python3 mutate.py https://target.com/admin --baseline-status 403
15
+
16
+ # 参数变异
17
+ python3 mutate.py "https://target.com/api?id=1" --mode param
18
+
19
+ # 自定义 header 变异
20
+ python3 mutate.py https://target.com/admin --mode header
21
+
22
+ # 输出全部(含失败)
23
+ python3 mutate.py https://target.com/admin --all
24
+ """
25
+ import argparse, ssl, sys, urllib.parse
26
+ import urllib.request, urllib.error
27
+
28
+ CTX = ssl.create_default_context()
29
+ CTX.check_hostname = False
30
+ CTX.verify_mode = ssl.CERT_NONE
31
+ UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36"
32
+
33
+
34
+ def req(url, method="GET", headers=None, data=None, timeout=12):
35
+ h = {"User-Agent": UA, "Accept": "*/*"}
36
+ if headers:
37
+ h.update(headers)
38
+ body = data.encode() if isinstance(data, str) else data
39
+ try:
40
+ r = urllib.request.Request(url, data=body, headers=h, method=method)
41
+ with urllib.request.urlopen(r, timeout=timeout, context=CTX) as x:
42
+ return x.status, len(x.read(300000)), dict(x.headers)
43
+ except urllib.error.HTTPError as e:
44
+ return e.code, 0, dict(e.headers)
45
+ except Exception as e:
46
+ return None, 0, {"_err": str(e)[:50]}
47
+
48
+
49
+ # ── L3 路径变异 ──
50
+ def path_variants(path):
51
+ p = path.lstrip("/")
52
+ out = [
53
+ ("原值", f"/{p}"),
54
+ ("URL编码", "/" + p.replace("/", "%2f")),
55
+ ("双重编码", "/" + p.replace("/", "%252f")),
56
+ ("全编码", "/" + "".join("%" + hex(ord(c))[2:] for c in p if c != "/")),
57
+ ("双斜杠", f"//{p}"),
58
+ ("点斜杠", f"/./{p}"),
59
+ ("尾部斜杠", f"/{p}/"),
60
+ ("尾部分号", f"/{p};.css"),
61
+ ("尾部点", f"/{p}."),
62
+ ("尾部%20", f"/{p}%20"),
63
+ ("尾部%00", f"/{p}%00.html"),
64
+ ("大写", f"/{p.upper()}"),
65
+ ("上级穿越", f"/./{p}/../{p}"),
66
+ ("问号截断", f"/{p}?x=.css"),
67
+ ("井号截断", f"/{p}#.css"),
68
+ ("分号参数", f"/{p};x=1"),
69
+ ("反斜杠", f"/{p.replace('/', chr(92))}"),
70
+ ("过编UTF8", "/" + p.replace("/", "%c0%af")),
71
+ ("全角点", "/" + p.replace("/", "%2f")),
72
+ ]
73
+ return out
74
+
75
+
76
+ # ── L5 参数变异 ──
77
+ def param_variants(query):
78
+ if not query:
79
+ return [("原值", "")]
80
+ parts = dict(urllib.parse.parse_qsl(query, keep_blank_values=True))
81
+ out = [("原值", query)]
82
+ for k, v in parts.items():
83
+ out += [
84
+ (f"{k} HPP", f"{query}&{k}={v}2"),
85
+ (f"{k} 数组", f"{k}[]={v}"),
86
+ (f"{k} 空字节", f"{k}={v}%00"),
87
+ (f"{k} 类型混淆", f"{k}={v}.0"),
88
+ (f"{k} 科学计数", f"{k}={v}e1"),
89
+ (f"{k} 负数", f"{k}=-{v}"),
90
+ (f"{k} 超长", f"{k}={'A'*500}"),
91
+ (f"{k} 换行", f"{k}={v}%0a"),
92
+ ]
93
+ return out[:40]
94
+
95
+
96
+ # ── L4 header 变异 ──
97
+ HEADER_VARIANTS = [
98
+ ("原值", {}),
99
+ ("XFF 本地", {"X-Forwarded-For": "127.0.0.1"}),
100
+ ("X-Real-IP", {"X-Real-IP": "127.0.0.1"}),
101
+ ("X-Originating-IP", {"X-Originating-IP": "127.0.0.1"}),
102
+ ("X-Forwarded-Host", {"X-Forwarded-Host": "127.0.0.1"}),
103
+ ("X-Original-URL", {"X-Original-URL": "/"}),
104
+ ("X-Rewrite-URL", {"X-Rewrite-URL": "/"}),
105
+ ("X-Forwarded-Prefix", {"X-Forwarded-Prefix": "/"}),
106
+ ("Referer 内页", {"Referer": "http://127.0.0.1/"}),
107
+ ("表单 CT", {"Content-Type": "application/x-www-form-urlencoded"}),
108
+ ("JSON CT", {"Content-Type": "application/json"}),
109
+ ("X-Custom-IP", {"X-Custom-IP-Authorization": "127.0.0.1"}),
110
+ ("Client-IP", {"Client-IP": "127.0.0.1"}),
111
+ ("X-Client-IP", {"X-Client-IP": "127.0.0.1"}),
112
+ ("Forwarded", {"Forwarded": "for=127.0.0.1;host=127.0.0.1"}),
113
+ ]
114
+
115
+ METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE"]
116
+
117
+
118
+ def main():
119
+ ap = argparse.ArgumentParser(description="自适应变异调度器")
120
+ ap.add_argument("url")
121
+ ap.add_argument("--mode", choices=["path", "param", "header", "method", "all"],
122
+ default="all")
123
+ ap.add_argument("--baseline-status", type=int, help="阻断基线状态码(如 403)")
124
+ ap.add_argument("--all", action="store_true", help="输出全部,不只差异")
125
+ a = ap.parse_args()
126
+
127
+ u = urllib.parse.urlsplit(a.url)
128
+ base = f"{u.scheme}://{u.netloc}"
129
+ path = u.path or "/"
130
+
131
+ # 基线
132
+ st0, ln0, _ = req(a.url)
133
+ print(f"[基线] {a.url} → {st0}", file=sys.stderr)
134
+ base_status = a.baseline_status or st0
135
+
136
+ results = []
137
+ tested = 0
138
+
139
+ def try_one(label, url, method="GET", headers=None):
140
+ nonlocal tested
141
+ tested += 1
142
+ st, ln, hd = req(url, method, headers)
143
+ # 判定:状态码变了 或 长度差异明显
144
+ interesting = False
145
+ if st is None:
146
+ interesting = False
147
+ elif st != base_status:
148
+ interesting = True
149
+ elif ln0 and abs(ln - ln0) > max(200, ln0 * 0.15):
150
+ interesting = True
151
+ results.append({"label": label, "url": url, "method": method,
152
+ "status": st, "len": ln, "interesting": interesting,
153
+ "loc": hd.get("Location", "")})
154
+ if interesting or a.all:
155
+ mark = "★" if interesting else " "
156
+ print(f" {mark} [{st}] len={ln:<7} {label:<20} {method} {url[:90]}")
157
+
158
+ # 按模式执行
159
+ if a.mode in ("path", "all"):
160
+ print("\n=== L3 路径变异 ===", file=sys.stderr)
161
+ for label, vp in path_variants(path):
162
+ try_one(f"路径·{label}", base + vp)
163
+
164
+ if a.mode in ("param", "all") and u.query:
165
+ print("\n=== L5 参数变异 ===", file=sys.stderr)
166
+ for label, q in param_variants(u.query):
167
+ try_one(f"参数·{label}", f"{base}{path}?{q}")
168
+
169
+ if a.mode in ("header", "all"):
170
+ print("\n=== L4 Header 变异 ===", file=sys.stderr)
171
+ for label, hd in HEADER_VARIANTS:
172
+ try_one(f"头·{label}", a.url, headers=hd)
173
+
174
+ if a.mode in ("method", "all"):
175
+ print("\n=== L4 动词变异 ===", file=sys.stderr)
176
+ for m in METHODS:
177
+ try_one(f"动词·{m}", a.url, method=m)
178
+
179
+ # 汇总
180
+ hits = [r for r in results if r["interesting"]]
181
+ print(f"\n{'='*60}", file=sys.stderr)
182
+ print(f"[结果] 共试 {tested} 种变异,发现 {len(hits)} 个差异响应", file=sys.stderr)
183
+ for h in hits:
184
+ print(f" ★ [{h['status']}] {h['label']} → {h['url'][:100]}", file=sys.stderr)
185
+
186
+ if hits:
187
+ print("\n[下一步] 差异响应写入 learned.md,继续深挖成功的那条", file=sys.stderr)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 并发端口扫描(无需 nmap)
5
+
6
+ 用法:
7
+ python3 portscan.py <host> [--ports 1-65535] [--conc 500] [--timeout 2]
8
+ python3 portscan.py 1.2.3.4 --ports top100
9
+ python3 portscan.py 1.2.3.4 --ports 22,80,443,3306,6379
10
+
11
+ 输出: jsonl(每行一个开放端口)
12
+ """
13
+ import asyncio, json, sys, argparse
14
+
15
+ TOP100 = [21,22,23,25,53,80,110,111,135,139,143,443,445,993,995,1433,1521,1723,
16
+ 2049,3306,3389,5432,5900,5984,6379,7001,8000,8008,8080,8081,8443,8888,
17
+ 9000,9090,9200,9300,10000,11211,27017,50000]
18
+ TOP20 = [21,22,23,25,53,80,443,445,3306,3389,5432,5900,6379,8000,8080,8443,8888,9000,9200,27017]
19
+
20
+ COMMON = {
21
+ 21:"ftp",22:"ssh",23:"telnet",25:"smtp",53:"dns",80:"http",110:"pop3",
22
+ 111:"rpcbind",135:"msrpc",139:"netbios",143:"imap",443:"https",445:"smb",
23
+ 993:"imaps",995:"pop3s",1433:"mssql",1521:"oracle",1723:"pptp",
24
+ 2049:"nfs",3306:"mysql",3389:"rdp",5432:"postgres",5900:"vnc",
25
+ 5984:"couchdb",6379:"redis",7001:"weblogic",8000:"http-alt",
26
+ 8008:"http-alt",8080:"http-proxy",8081:"http-alt",8443:"https-alt",
27
+ 8888:"http-alt",9000:"php-fpm",9090:"http-alt",9200:"elasticsearch",
28
+ 9300:"es-transport",10000:"webmin",11211:"memcached",27017:"mongodb",
29
+ 50000:"db2"
30
+ }
31
+
32
+
33
+ def parse_ports(s):
34
+ s = s.strip().lower()
35
+ if s == "top100":
36
+ return TOP100
37
+ if s == "top20":
38
+ return TOP20
39
+ out = []
40
+ for part in s.split(","):
41
+ part = part.strip()
42
+ if "-" in part:
43
+ a, b = part.split("-")
44
+ out.extend(range(int(a), int(b)+1))
45
+ elif part:
46
+ out.append(int(part))
47
+ return sorted(set(out))
48
+
49
+
50
+ async def probe(host, port, timeout=2.0):
51
+ try:
52
+ r, w = await asyncio.wait_for(
53
+ asyncio.open_connection(host, port), timeout=timeout)
54
+ w.close()
55
+ try:
56
+ await w.wait_closed()
57
+ except Exception:
58
+ pass
59
+ return port
60
+ except Exception:
61
+ return None
62
+
63
+
64
+ async def scan(host, ports, conc=500, timeout=2.0):
65
+ sem = asyncio.Semaphore(conc)
66
+ open_ports = []
67
+
68
+ async def one(p):
69
+ async with sem:
70
+ r = await probe(host, p, timeout)
71
+ if r:
72
+ open_ports.append(r)
73
+
74
+ await asyncio.gather(*[one(p) for p in ports])
75
+ return sorted(open_ports)
76
+
77
+
78
+ def main():
79
+ ap = argparse.ArgumentParser(description="并发端口扫描(无需 nmap)")
80
+ ap.add_argument("host")
81
+ ap.add_argument("--ports", default="top20",
82
+ help="1-65535 / top100 / top20 / 22,80,443")
83
+ ap.add_argument("--conc", type=int, default=500, help="并发数")
84
+ ap.add_argument("--timeout", type=float, default=2.0)
85
+ ap.add_argument("--json", action="store_true", help="只输出 JSONL")
86
+ a = ap.parse_args()
87
+
88
+ ports = parse_ports(a.ports)
89
+ if not a.json:
90
+ print(f"# 扫描 {a.host} 共 {len(ports)} 端口 (并发 {a.conc})",
91
+ file=sys.stderr)
92
+
93
+ open_ports = asyncio.run(scan(a.host, ports, a.conc, a.timeout))
94
+
95
+ for p in open_ports:
96
+ rec = {"host": a.host, "port": p, "service": COMMON.get(p, "?")}
97
+ print(json.dumps(rec, ensure_ascii=False))
98
+
99
+ if not a.json:
100
+ print(f"# 开放 {len(open_ports)} 个: {open_ports}", file=sys.stderr)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Web 批量指纹 + 路径探测器
5
+ 用法: python3 webprobe.py <domain> [<domain> ...] # 或完整 URL
6
+ 输出: jsonl(指纹 + 命中路径)
7
+ """
8
+ import sys, json, ssl, re, urllib.request, urllib.error, concurrent.futures
9
+
10
+ UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36"
11
+ CTX = ssl.create_default_context()
12
+ CTX.check_hostname = False
13
+ CTX.verify_mode = ssl.CERT_NONE
14
+
15
+ PROBE_PATHS = [
16
+ "/", "/robots.txt", "/.env", "/.git/HEAD", "/config.json", "/config.js",
17
+ "/admin", "/admin/", "/manage/login", "/api/admin", "/api/v1/admin",
18
+ "/actuator", "/actuator/health", "/actuator/env", "/actuator/beans",
19
+ "/swagger-ui.html", "/v3/api-docs", "/openapi.json", "/api-docs",
20
+ "/api/site", "/api/config", "/api/health", "/health", "/version",
21
+ "/api/v1/guest", "/api/v1/announcement", "/api/query/keys/batch",
22
+ "/api/v1/recharge/verify-cdk", "/api/v1/lookup/tasks", "/api/verify/cdk",
23
+ "/api/auth/login", "/api/auth/verify", "/login", "/dashboard",
24
+ "/backup.zip", "/db.sql", "/phpinfo.php", "/info.php", "/server-status",
25
+ ]
26
+
27
+ def req(url, method="GET", data=None, timeout=12, headers=None):
28
+ h = {"User-Agent": UA, "Accept": "*/*"}
29
+ if headers: h.update(headers)
30
+ body = None
31
+ if data is not None:
32
+ body = json.dumps(data).encode()
33
+ h["Content-Type"] = "application/json"
34
+ r = urllib.request.Request(url, data=body, headers=h, method=method)
35
+ try:
36
+ with urllib.request.urlopen(r, timeout=timeout, context=CTX) as resp:
37
+ raw = resp.read(4000)
38
+ return resp.status, dict(resp.headers), raw
39
+ except urllib.error.HTTPError as e:
40
+ return e.code, dict(e.headers or {}), (e.read(4000) if e.fp else b"")
41
+ except Exception as e:
42
+ return 0, {}, str(e).encode()
43
+
44
+ def scan(domain):
45
+ out = {"domain": domain, "hits": [], "errors": []}
46
+ # 指纹
47
+ st, hd, bd = req(f"https://{domain}/")
48
+ txt = bd.decode("utf-8", "replace")
49
+ out["root_status"] = st
50
+ out["server"] = hd.get("Server") or hd.get("server")
51
+ m = re.search(r"<title[^>]*>([^<]*)", txt, re.I)
52
+ out["title"] = m.group(1).strip() if m else ""
53
+ out["frameworks"] = sorted(set(re.findall(
54
+ r"(dujiaoka|独角数卡|ruoyi|若依|spring|actuator|vue|react|nuxt|next|vite|"
55
+ r"laravel|thinkphp|django|flask|express|gin|subvault|new-api|one-api)",
56
+ txt + str(hd), re.I)))
57
+ # 路径探测
58
+ for p in PROBE_PATHS:
59
+ s, h, b = req(f"https://{domain}{p}")
60
+ if s in (200, 201, 301, 302, 401, 403, 405, 500):
61
+ bt = b[:300].decode("utf-8", "replace")
62
+ # 过滤 SPA 兜底(同 root 长度 + text/html)
63
+ if s == 200 and bt[:60] == txt[:60] and len(b) == len(bd):
64
+ continue
65
+ out["hits"].append({"path": p, "status": s,
66
+ "ct": h.get("Content-Type", "")[:40],
67
+ "len": len(b), "body": bt[:220]})
68
+ return out
69
+
70
+ def main():
71
+ domains = sys.argv[1:]
72
+ if not domains:
73
+ print(__doc__.strip())
74
+ sys.exit(0)
75
+ with concurrent.futures.ThreadPoolExecutor(max_workers=6) as ex:
76
+ for r in ex.map(scan, domains):
77
+ print(json.dumps(r, ensure_ascii=False))
78
+
79
+ if __name__ == "__main__":
80
+ main()