gyntoolkit 2.1.1__py3-none-any.whl
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.
- gyntoolkit/__init__.py +80 -0
- gyntoolkit/__main__.py +7 -0
- gyntoolkit/brute.py +179 -0
- gyntoolkit/cli.py +451 -0
- gyntoolkit/config.py +109 -0
- gyntoolkit/core.py +86 -0
- gyntoolkit/export.py +139 -0
- gyntoolkit/i18n.py +487 -0
- gyntoolkit/recon.py +358 -0
- gyntoolkit/scan.py +150 -0
- gyntoolkit/ui.py +132 -0
- gyntoolkit/utils.py +61 -0
- gyntoolkit-2.1.1.dist-info/METADATA +386 -0
- gyntoolkit-2.1.1.dist-info/RECORD +18 -0
- gyntoolkit-2.1.1.dist-info/WHEEL +5 -0
- gyntoolkit-2.1.1.dist-info/entry_points.txt +2 -0
- gyntoolkit-2.1.1.dist-info/licenses/LICENSE +21 -0
- gyntoolkit-2.1.1.dist-info/top_level.txt +1 -0
gyntoolkit/cli.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fluxo interativo (menu) e ponto de entrada da CLI."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from colorama import Fore, Style
|
|
11
|
+
|
|
12
|
+
from . import i18n, ui
|
|
13
|
+
from .brute import (
|
|
14
|
+
cupp_generate,
|
|
15
|
+
http_bruteforce,
|
|
16
|
+
load_wordlist,
|
|
17
|
+
print_ethical_warning,
|
|
18
|
+
ssh_bruteforce,
|
|
19
|
+
)
|
|
20
|
+
from .config import CONFIG
|
|
21
|
+
from .core import is_admin_windows, sanitize_input, show_menu
|
|
22
|
+
from .export import save_report
|
|
23
|
+
from .recon import (
|
|
24
|
+
dns_lookup,
|
|
25
|
+
escolher_tipo_dns,
|
|
26
|
+
geo_ip,
|
|
27
|
+
hibp_breaches,
|
|
28
|
+
http_fingerprint,
|
|
29
|
+
internetdb_lookup,
|
|
30
|
+
mac_vendor,
|
|
31
|
+
reverse_dns,
|
|
32
|
+
ssl_inspect,
|
|
33
|
+
subdomain_enum,
|
|
34
|
+
traceroute,
|
|
35
|
+
whois_lookup,
|
|
36
|
+
)
|
|
37
|
+
from .scan import network_discovery, perform_scan
|
|
38
|
+
from .utils import b64_decode, b64_encode, hash_file, hash_text, jwt_decode
|
|
39
|
+
|
|
40
|
+
# Valores canônicos de scan_type (DATA, usados por perform_scan). Neutros de
|
|
41
|
+
# idioma; entradas localizadas (en/pt) são mapeadas por _normalize_scan_type.
|
|
42
|
+
_FAST = "fast"
|
|
43
|
+
_FULL = "full"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _cyan(key: str, **kw) -> str:
|
|
47
|
+
"""Prompt ciano traduzido para input()."""
|
|
48
|
+
return f"\n{Fore.CYAN}{i18n.t(key, **kw)}{Style.RESET_ALL}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _pause() -> None:
|
|
52
|
+
input(f"\n{Fore.YELLOW}{i18n.t('common.press_enter')}{Style.RESET_ALL}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _normalize_scan_type(raw: str, default: str) -> str:
|
|
56
|
+
raw = (raw or "").strip().lower() or default
|
|
57
|
+
# aceita canônico "fast", localizados pt ("rápido"/"rapido") e abreviações
|
|
58
|
+
return _FAST if raw in (_FAST, "rápido", "rapido", "f", "r") else _FULL
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _scan_risk_label(risco: str) -> str:
|
|
62
|
+
return i18n.t("scan.risk_high") if risco == "high" else i18n.t("scan.risk_low")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _scan_service_label(service: str) -> str:
|
|
66
|
+
return i18n.t("scan.service_unknown") if service == "unknown" else service
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _offer_export(data, basename: str, title: str) -> None:
|
|
70
|
+
"""Oferece salvar ``data`` em JSON/HTML conforme config. No-op se vazio."""
|
|
71
|
+
if not data:
|
|
72
|
+
return
|
|
73
|
+
cfg = CONFIG["export"]
|
|
74
|
+
if cfg.get("auto"):
|
|
75
|
+
fmt = cfg.get("format", "json")
|
|
76
|
+
else:
|
|
77
|
+
ans = input(
|
|
78
|
+
f"{Fore.CYAN}{i18n.t('msg.export_prompt')}{Style.RESET_ALL}"
|
|
79
|
+
).strip().lower()
|
|
80
|
+
if ans not in ("json", "html"):
|
|
81
|
+
return
|
|
82
|
+
fmt = ans
|
|
83
|
+
try:
|
|
84
|
+
path = save_report(data, basename, fmt=fmt, out_dir=cfg.get("dir", "reports"), title=title)
|
|
85
|
+
ui.success(i18n.t("msg.report_saved", path=path))
|
|
86
|
+
except OSError as e:
|
|
87
|
+
ui.error(i18n.t("msg.export_fail", err=e))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def main_flow():
|
|
91
|
+
while True:
|
|
92
|
+
choice = show_menu(i18n.t("menu.main.title"), [
|
|
93
|
+
i18n.t("menu.main.info"),
|
|
94
|
+
i18n.t("menu.main.brute"),
|
|
95
|
+
i18n.t("menu.main.scan"),
|
|
96
|
+
i18n.t("menu.main.utils"),
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
if choice == 0:
|
|
100
|
+
print(f"\n{Fore.MAGENTA}{i18n.t('common.exiting')}{Style.RESET_ALL}")
|
|
101
|
+
sys.exit()
|
|
102
|
+
|
|
103
|
+
elif choice == 1: # Obter Informações
|
|
104
|
+
sub_choice = show_menu(i18n.t("menu.info.title"), [
|
|
105
|
+
i18n.t("menu.info.whois"),
|
|
106
|
+
i18n.t("menu.info.dns"),
|
|
107
|
+
i18n.t("menu.info.geo"),
|
|
108
|
+
i18n.t("menu.info.revdns"),
|
|
109
|
+
i18n.t("menu.info.subenum"),
|
|
110
|
+
i18n.t("menu.info.ssl"),
|
|
111
|
+
i18n.t("menu.info.httpfp"),
|
|
112
|
+
i18n.t("menu.info.internetdb"),
|
|
113
|
+
i18n.t("menu.info.hibp"),
|
|
114
|
+
i18n.t("menu.info.macvendor"),
|
|
115
|
+
i18n.t("menu.info.traceroute"),
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
if sub_choice == 1:
|
|
119
|
+
domain = input(_cyan("prompt.domain"))
|
|
120
|
+
with ui.status(i18n.t("status.whois", d=domain)):
|
|
121
|
+
result = whois_lookup(domain)
|
|
122
|
+
print(f"\n{Fore.GREEN}{i18n.t('label.result')}{Style.RESET_ALL}")
|
|
123
|
+
print(result)
|
|
124
|
+
if not isinstance(result, str):
|
|
125
|
+
_offer_export({"domain": domain, "whois": str(result)},
|
|
126
|
+
f"whois-{domain}", f"WHOIS {domain}")
|
|
127
|
+
|
|
128
|
+
elif sub_choice == 2:
|
|
129
|
+
domain = sanitize_input(input(_cyan("prompt.domain")), r"[A-Za-z0-9.-]")
|
|
130
|
+
|
|
131
|
+
rtype = escolher_tipo_dns()
|
|
132
|
+
if not rtype:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
ns = input(f"{Fore.CYAN}{i18n.t('prompt.dns_server')}{Style.RESET_ALL}") or None
|
|
136
|
+
|
|
137
|
+
with ui.status(i18n.t("status.dns", t=rtype, d=domain)):
|
|
138
|
+
records = dns_lookup(domain, rtype.upper(), ns)
|
|
139
|
+
ui.print_list(i18n.t("label.dns_records", t=rtype, d=domain), records)
|
|
140
|
+
_offer_export({"domain": domain, "type": rtype, "records": records},
|
|
141
|
+
f"dns-{domain}-{rtype}", f"DNS {rtype} {domain}")
|
|
142
|
+
|
|
143
|
+
elif sub_choice == 3:
|
|
144
|
+
target = sanitize_input(input(_cyan("prompt.ip_or_domain")), r"[A-Za-z0-9.:-]")
|
|
145
|
+
with ui.status(i18n.t("status.geo", t=target)):
|
|
146
|
+
info = geo_ip(target)
|
|
147
|
+
if "erro" in info:
|
|
148
|
+
ui.error(info["erro"])
|
|
149
|
+
else:
|
|
150
|
+
label_map = {
|
|
151
|
+
"query": i18n.t("geo.ip"), "country": i18n.t("geo.country"),
|
|
152
|
+
"regionName": i18n.t("geo.region"), "city": i18n.t("geo.city"),
|
|
153
|
+
"zip": i18n.t("geo.zip"), "lat": i18n.t("geo.lat"),
|
|
154
|
+
"lon": i18n.t("geo.lon"), "timezone": i18n.t("geo.timezone"),
|
|
155
|
+
"isp": i18n.t("geo.isp"), "org": i18n.t("geo.org"),
|
|
156
|
+
"as": i18n.t("geo.as"), "reverse": i18n.t("geo.reverse"),
|
|
157
|
+
"mobile": i18n.t("geo.mobile"), "proxy": i18n.t("geo.proxy"),
|
|
158
|
+
"hosting": i18n.t("geo.hosting"),
|
|
159
|
+
}
|
|
160
|
+
shown = {label: info[key] for key, label in label_map.items() if key in info}
|
|
161
|
+
ui.print_kv(i18n.t("label.geo", t=target), shown)
|
|
162
|
+
_offer_export(info, f"geoip-{target}", f"Geo IP {target}")
|
|
163
|
+
|
|
164
|
+
elif sub_choice == 4:
|
|
165
|
+
target = sanitize_input(input(_cyan("prompt.ip_or_domain")), r"[A-Za-z0-9.:-]")
|
|
166
|
+
with ui.status(i18n.t("status.revdns", t=target)):
|
|
167
|
+
r = reverse_dns(target)
|
|
168
|
+
ui.print_kv(i18n.t("label.revdns", t=target), r)
|
|
169
|
+
_offer_export(r, f"revdns-{target}", f"Reverse DNS {target}")
|
|
170
|
+
|
|
171
|
+
elif sub_choice == 5:
|
|
172
|
+
domain = sanitize_input(input(_cyan("prompt.root_domain")), r"[A-Za-z0-9.-]")
|
|
173
|
+
with ui.status(i18n.t("status.subenum")):
|
|
174
|
+
subs = subdomain_enum(domain, timeout=CONFIG["recon"]["subdomain_timeout"])
|
|
175
|
+
if not subs:
|
|
176
|
+
ui.error(i18n.t("msg.no_subdomains"))
|
|
177
|
+
else:
|
|
178
|
+
ui.print_list(i18n.t("label.subdomains", d=domain, n=len(subs)), subs)
|
|
179
|
+
_offer_export({"domain": domain, "total": len(subs), "subdomains": subs},
|
|
180
|
+
f"subdomains-{domain}", f"Subdomínios {domain}")
|
|
181
|
+
|
|
182
|
+
elif sub_choice == 6:
|
|
183
|
+
host = sanitize_input(input(_cyan("prompt.host")), r"[A-Za-z0-9.-]")
|
|
184
|
+
port = int(input(f"{Fore.CYAN}{i18n.t('prompt.port_443')}{Style.RESET_ALL}") or 443)
|
|
185
|
+
with ui.status(i18n.t("status.ssl", h=host, p=port)):
|
|
186
|
+
info = ssl_inspect(host, port, timeout=CONFIG["recon"]["ssl_timeout"])
|
|
187
|
+
if "erro" in info:
|
|
188
|
+
ui.error(info["erro"])
|
|
189
|
+
else:
|
|
190
|
+
ui.print_kv(
|
|
191
|
+
i18n.t("label.ssl", h=host, p=port), info,
|
|
192
|
+
warn_keys={"dias_para_expirar": lambda v: isinstance(v, int) and v < 30},
|
|
193
|
+
)
|
|
194
|
+
_offer_export(info, f"ssl-{host}", f"SSL {host}:{port}")
|
|
195
|
+
|
|
196
|
+
elif sub_choice == 7:
|
|
197
|
+
url = input(f"\n{Fore.CYAN}{i18n.t('prompt.url_http')}{Style.RESET_ALL}").strip()
|
|
198
|
+
with ui.status(i18n.t("status.httpfp", u=url)):
|
|
199
|
+
info = http_fingerprint(url, timeout=CONFIG["recon"]["http_timeout"])
|
|
200
|
+
if "erro" in info:
|
|
201
|
+
ui.error(info["erro"])
|
|
202
|
+
else:
|
|
203
|
+
ui.print_kv(i18n.t("label.httpfp", u=url), info)
|
|
204
|
+
_offer_export(info, f"httpfp-{url}", f"HTTP Fingerprint {url}")
|
|
205
|
+
|
|
206
|
+
elif sub_choice == 8:
|
|
207
|
+
target = sanitize_input(input(_cyan("prompt.ip_or_domain")), r"[A-Za-z0-9.:-]")
|
|
208
|
+
with ui.status(i18n.t("status.internetdb")):
|
|
209
|
+
info = internetdb_lookup(target)
|
|
210
|
+
if "erro" in info:
|
|
211
|
+
ui.error(info["erro"])
|
|
212
|
+
elif "info" in info:
|
|
213
|
+
ui.notice(info["info"])
|
|
214
|
+
else:
|
|
215
|
+
shown = {
|
|
216
|
+
i18n.t("idb.ip"): info.get("ip"),
|
|
217
|
+
i18n.t("idb.hostnames"): info.get("hostnames", []),
|
|
218
|
+
i18n.t("idb.ports"): info.get("ports", []),
|
|
219
|
+
i18n.t("idb.tags"): info.get("tags", []),
|
|
220
|
+
i18n.t("idb.cpes"): info.get("cpes", [])[:10],
|
|
221
|
+
i18n.t("idb.cves"): info.get("vulns", [])[:15],
|
|
222
|
+
}
|
|
223
|
+
ui.print_kv(i18n.t("label.internetdb", t=target), shown)
|
|
224
|
+
_offer_export(info, f"internetdb-{target}", f"InternetDB {target}")
|
|
225
|
+
|
|
226
|
+
elif sub_choice == 9:
|
|
227
|
+
domain = sanitize_input(input(_cyan("prompt.domain_simple")), r"[A-Za-z0-9.-]")
|
|
228
|
+
with ui.status(i18n.t("status.hibp", d=domain)):
|
|
229
|
+
breaches = hibp_breaches(domain)
|
|
230
|
+
if not breaches:
|
|
231
|
+
ui.success(i18n.t("msg.no_breaches", d=domain))
|
|
232
|
+
elif breaches and "erro" in breaches[0]:
|
|
233
|
+
ui.error(breaches[0]["erro"])
|
|
234
|
+
else:
|
|
235
|
+
rows = [
|
|
236
|
+
(b.get("Name"), b.get("BreachDate"), b.get("PwnCount"),
|
|
237
|
+
", ".join(b.get("DataClasses", [])))
|
|
238
|
+
for b in breaches
|
|
239
|
+
]
|
|
240
|
+
ui.print_table(i18n.t("label.breaches", d=domain),
|
|
241
|
+
i18n.t("table.breaches").split("|"), rows)
|
|
242
|
+
_offer_export(breaches, f"hibp-{domain}", f"HIBP {domain}")
|
|
243
|
+
|
|
244
|
+
elif sub_choice == 10:
|
|
245
|
+
mac = input(f"\n{Fore.CYAN}{i18n.t('prompt.mac')}{Style.RESET_ALL}").strip()
|
|
246
|
+
with ui.status(i18n.t("status.macvendor")):
|
|
247
|
+
vendor = mac_vendor(mac)
|
|
248
|
+
ui.print_kv(i18n.t("label.macvendor"),
|
|
249
|
+
{i18n.t("kv.mac"): mac, i18n.t("kv.vendor"): vendor})
|
|
250
|
+
|
|
251
|
+
elif sub_choice == 11:
|
|
252
|
+
target = sanitize_input(input(_cyan("prompt.target")), r"[A-Za-z0-9.-]")
|
|
253
|
+
max_hops = int(input(f"{Fore.CYAN}{i18n.t('prompt.max_hops', n=CONFIG['recon']['traceroute_max_hops'])}{Style.RESET_ALL}")
|
|
254
|
+
or CONFIG["recon"]["traceroute_max_hops"])
|
|
255
|
+
dport = int(input(f"{Fore.CYAN}{i18n.t('prompt.dest_port', n=CONFIG['recon']['traceroute_dport'])}{Style.RESET_ALL}")
|
|
256
|
+
or CONFIG["recon"]["traceroute_dport"])
|
|
257
|
+
with ui.status(i18n.t("status.traceroute", t=target, p=dport)):
|
|
258
|
+
hops = traceroute(target, max_hops=max_hops, dport=dport)
|
|
259
|
+
if hops and "erro" in hops[0]:
|
|
260
|
+
ui.error(hops[0]["erro"])
|
|
261
|
+
else:
|
|
262
|
+
rows = [(h["ttl"], h["ip"], f"{h['rtt_ms']} ms" if h["rtt_ms"] is not None else "*")
|
|
263
|
+
for h in hops]
|
|
264
|
+
ui.print_table(i18n.t("label.traceroute", t=target, p=dport),
|
|
265
|
+
i18n.t("table.traceroute").split("|"), rows)
|
|
266
|
+
_offer_export({"target": target, "dport": dport, "hops": hops},
|
|
267
|
+
f"traceroute-{target}", f"Traceroute {target}")
|
|
268
|
+
|
|
269
|
+
elif choice == 2: # Brute Force
|
|
270
|
+
b = CONFIG["brute"]
|
|
271
|
+
sub_choice = show_menu(i18n.t("menu.brute.title"), [
|
|
272
|
+
i18n.t("menu.brute.cupp"),
|
|
273
|
+
i18n.t("menu.brute.ssh"),
|
|
274
|
+
i18n.t("menu.brute.http"),
|
|
275
|
+
])
|
|
276
|
+
|
|
277
|
+
if sub_choice == 1:
|
|
278
|
+
cupp_generate()
|
|
279
|
+
|
|
280
|
+
elif sub_choice == 2:
|
|
281
|
+
if not print_ethical_warning("Brute-force SSH"):
|
|
282
|
+
ui.error(i18n.t("msg.auth_not_confirmed"))
|
|
283
|
+
_pause()
|
|
284
|
+
continue
|
|
285
|
+
host = sanitize_input(input(_cyan("prompt.target_host")))
|
|
286
|
+
port = int(input(f"{Fore.CYAN}{i18n.t('prompt.port_22')}{Style.RESET_ALL}") or 22)
|
|
287
|
+
user_default = f" [{b['users_wordlist']}]" if b["users_wordlist"] else ""
|
|
288
|
+
user_input = (input(f"{Fore.CYAN}{i18n.t('prompt.user_or_users_wl', default=user_default)}{Style.RESET_ALL}").strip()
|
|
289
|
+
or b["users_wordlist"])
|
|
290
|
+
users = load_wordlist(user_input) if Path(user_input).is_file() else [user_input]
|
|
291
|
+
pass_default = f" [{b['passwords_wordlist']}]" if b["passwords_wordlist"] else ""
|
|
292
|
+
pass_path = (input(f"{Fore.CYAN}{i18n.t('prompt.pass_wl', default=pass_default)}{Style.RESET_ALL}").strip()
|
|
293
|
+
or b["passwords_wordlist"])
|
|
294
|
+
passwords = load_wordlist(pass_path)
|
|
295
|
+
if not users or not users[0] or not passwords:
|
|
296
|
+
ui.error(i18n.t("msg.empty_wordlist"))
|
|
297
|
+
_pause()
|
|
298
|
+
continue
|
|
299
|
+
workers = int(input(f"{Fore.CYAN}{i18n.t('prompt.workers', n=b['ssh_workers'])}{Style.RESET_ALL}") or b["ssh_workers"])
|
|
300
|
+
ui.notice(i18n.t("msg.ssh_start", h=host, p=port, u=len(users), pw=len(passwords), c=len(users)*len(passwords)))
|
|
301
|
+
results = await ssh_bruteforce(host, users, passwords, port=port, workers=workers,
|
|
302
|
+
delay=b["delay"], timeout=b["ssh_timeout"])
|
|
303
|
+
if results:
|
|
304
|
+
ui.print_table(i18n.t("label.creds_found"), i18n.t("table.creds").split("|"), results)
|
|
305
|
+
else:
|
|
306
|
+
ui.error(i18n.t("msg.no_valid_creds"))
|
|
307
|
+
|
|
308
|
+
elif sub_choice == 3:
|
|
309
|
+
if not print_ethical_warning("Brute-force HTTP"):
|
|
310
|
+
ui.error(i18n.t("msg.auth_not_confirmed"))
|
|
311
|
+
_pause()
|
|
312
|
+
continue
|
|
313
|
+
url = input(f"\n{Fore.CYAN}{i18n.t('prompt.url_target')}{Style.RESET_ALL}").strip()
|
|
314
|
+
mode = (input(f"{Fore.CYAN}{i18n.t('prompt.mode')}{Style.RESET_ALL}").strip().lower() or "basic")
|
|
315
|
+
user_field = pass_field = fail_sig = ""
|
|
316
|
+
if mode == "form":
|
|
317
|
+
user_field = input(f"{Fore.CYAN}{i18n.t('prompt.user_field')}{Style.RESET_ALL}").strip() or "username"
|
|
318
|
+
pass_field = input(f"{Fore.CYAN}{i18n.t('prompt.pass_field')}{Style.RESET_ALL}").strip() or "password"
|
|
319
|
+
fail_sig = input(f"{Fore.CYAN}{i18n.t('prompt.fail_sig')}{Style.RESET_ALL}").strip()
|
|
320
|
+
user_default = f" [{b['users_wordlist']}]" if b["users_wordlist"] else ""
|
|
321
|
+
user_input = (input(f"{Fore.CYAN}{i18n.t('prompt.user_or_wl', default=user_default)}{Style.RESET_ALL}").strip()
|
|
322
|
+
or b["users_wordlist"])
|
|
323
|
+
users = load_wordlist(user_input) if Path(user_input).is_file() else [user_input]
|
|
324
|
+
pass_default = f" [{b['passwords_wordlist']}]" if b["passwords_wordlist"] else ""
|
|
325
|
+
pass_path = (input(f"{Fore.CYAN}{i18n.t('prompt.pass_wl', default=pass_default)}{Style.RESET_ALL}").strip()
|
|
326
|
+
or b["passwords_wordlist"])
|
|
327
|
+
passwords = load_wordlist(pass_path)
|
|
328
|
+
if not users or not users[0] or not passwords:
|
|
329
|
+
ui.error(i18n.t("msg.empty_wordlist"))
|
|
330
|
+
_pause()
|
|
331
|
+
continue
|
|
332
|
+
workers = int(input(f"{Fore.CYAN}{i18n.t('prompt.workers', n=b['http_workers'])}{Style.RESET_ALL}") or b["http_workers"])
|
|
333
|
+
ui.notice(i18n.t("msg.http_start", u=url, c=len(users)*len(passwords)))
|
|
334
|
+
results = await http_bruteforce(
|
|
335
|
+
url, users, passwords, mode=mode,
|
|
336
|
+
user_field=user_field, pass_field=pass_field,
|
|
337
|
+
fail_signature=fail_sig, workers=workers,
|
|
338
|
+
delay=b["delay"], timeout=b["http_timeout"],
|
|
339
|
+
)
|
|
340
|
+
if results:
|
|
341
|
+
ui.print_table(i18n.t("label.creds_found"), i18n.t("table.creds").split("|"), results)
|
|
342
|
+
else:
|
|
343
|
+
ui.error(i18n.t("msg.no_valid_creds"))
|
|
344
|
+
|
|
345
|
+
elif choice == 3: # Varredura Avançada
|
|
346
|
+
default_type = CONFIG["scan"]["default_type"]
|
|
347
|
+
target = sanitize_input(input(_cyan("prompt.target_net")))
|
|
348
|
+
scan_type = _normalize_scan_type(
|
|
349
|
+
input(i18n.t("prompt.scan_type", d=default_type)), default_type
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if '/' in target:
|
|
353
|
+
with ui.status(i18n.t("status.discover")):
|
|
354
|
+
hosts = network_discovery(target)
|
|
355
|
+
if not hosts:
|
|
356
|
+
ui.error(i18n.t("msg.no_host_responded"))
|
|
357
|
+
_pause()
|
|
358
|
+
continue
|
|
359
|
+
ui.print_list(i18n.t("label.hosts_found", n=len(hosts)),
|
|
360
|
+
[f"{i}. {h}" for i, h in enumerate(hosts, 1)])
|
|
361
|
+
selection = input(i18n.t("prompt.select_host")).strip()
|
|
362
|
+
if selection:
|
|
363
|
+
try:
|
|
364
|
+
targets = [hosts[int(selection) - 1]]
|
|
365
|
+
except (ValueError, IndexError):
|
|
366
|
+
ui.error(i18n.t("msg.invalid_selection"))
|
|
367
|
+
continue
|
|
368
|
+
else:
|
|
369
|
+
targets = hosts
|
|
370
|
+
else:
|
|
371
|
+
targets = [target]
|
|
372
|
+
|
|
373
|
+
report = {}
|
|
374
|
+
for host in targets:
|
|
375
|
+
with ui.status(i18n.t("status.scanning", h=host)):
|
|
376
|
+
results = await perform_scan(host, scan_type)
|
|
377
|
+
report[host] = results
|
|
378
|
+
rows = [
|
|
379
|
+
(port, _scan_service_label(data["service"]), _scan_risk_label(data["risco"]),
|
|
380
|
+
(data["banner"] or "")[:60],
|
|
381
|
+
", ".join(data["vulnerabilidades"]) or "—")
|
|
382
|
+
for port, data in results.items()
|
|
383
|
+
]
|
|
384
|
+
row_styles = ["red" if data["risco"] == "high" else "green" for data in results.values()]
|
|
385
|
+
if rows:
|
|
386
|
+
ui.print_table(i18n.t("label.scan_results", h=host),
|
|
387
|
+
i18n.t("table.scan").split("|"),
|
|
388
|
+
rows, row_styles=row_styles)
|
|
389
|
+
else:
|
|
390
|
+
ui.notice(i18n.t("msg.no_open_ports", h=host))
|
|
391
|
+
_offer_export(report, f"scan-{targets[0]}", f"Scan {', '.join(targets)}")
|
|
392
|
+
|
|
393
|
+
elif choice == 4: # Utilitários
|
|
394
|
+
sub_choice = show_menu(i18n.t("menu.utils.title"), [
|
|
395
|
+
i18n.t("menu.utils.hashtext"),
|
|
396
|
+
i18n.t("menu.utils.hashfile"),
|
|
397
|
+
i18n.t("menu.utils.b64enc"),
|
|
398
|
+
i18n.t("menu.utils.b64dec"),
|
|
399
|
+
i18n.t("menu.utils.jwt"),
|
|
400
|
+
])
|
|
401
|
+
|
|
402
|
+
if sub_choice == 1:
|
|
403
|
+
text = input(_cyan("prompt.text"))
|
|
404
|
+
algo = (input(f"{Fore.CYAN}{i18n.t('prompt.algo')}{Style.RESET_ALL}").strip().lower() or "sha256")
|
|
405
|
+
try:
|
|
406
|
+
ui.print_kv(i18n.t("label.hash"), {algo: hash_text(text, algo)})
|
|
407
|
+
except ValueError as e:
|
|
408
|
+
ui.error(i18n.t("msg.invalid_algo", err=e))
|
|
409
|
+
|
|
410
|
+
elif sub_choice == 2:
|
|
411
|
+
path = input(f"\n{Fore.CYAN}{i18n.t('prompt.file_path')}{Style.RESET_ALL}").strip()
|
|
412
|
+
algo = (input(f"{Fore.CYAN}{i18n.t('prompt.algo')}{Style.RESET_ALL}").strip().lower() or "sha256")
|
|
413
|
+
try:
|
|
414
|
+
ui.print_kv(i18n.t("label.file_hash"), {algo: hash_file(path, algo)})
|
|
415
|
+
except ValueError as e:
|
|
416
|
+
ui.error(i18n.t("msg.invalid_algo", err=e))
|
|
417
|
+
|
|
418
|
+
elif sub_choice == 3:
|
|
419
|
+
text = input(_cyan("prompt.text"))
|
|
420
|
+
ui.print_kv(i18n.t("label.b64enc"), {"b64": b64_encode(text)})
|
|
421
|
+
|
|
422
|
+
elif sub_choice == 4:
|
|
423
|
+
text = input(f"\n{Fore.CYAN}{i18n.t('prompt.base64')}{Style.RESET_ALL}")
|
|
424
|
+
ui.print_kv(i18n.t("label.b64dec"), {"decoded": b64_decode(text)})
|
|
425
|
+
|
|
426
|
+
elif sub_choice == 5:
|
|
427
|
+
token = input(f"\n{Fore.CYAN}{i18n.t('prompt.jwt')}{Style.RESET_ALL}").strip()
|
|
428
|
+
r = jwt_decode(token)
|
|
429
|
+
if "erro" in r:
|
|
430
|
+
ui.error(r["erro"])
|
|
431
|
+
else:
|
|
432
|
+
print(f"\n {Fore.YELLOW}header:{Style.RESET_ALL} {json.dumps(r['header'], indent=2)}")
|
|
433
|
+
print(f" {Fore.YELLOW}payload:{Style.RESET_ALL} {json.dumps(r['payload'], indent=2)}")
|
|
434
|
+
print(f" {Fore.YELLOW}signature:{Style.RESET_ALL} {r['signature']}")
|
|
435
|
+
|
|
436
|
+
_pause()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def main_entry() -> None:
|
|
440
|
+
"""Entry point CLI (usado por pyproject scripts)."""
|
|
441
|
+
i18n.set_lang(i18n.resolve_lang(CONFIG.get("ui", {}).get("lang")))
|
|
442
|
+
try:
|
|
443
|
+
if os.name == 'posix' and os.geteuid() != 0:
|
|
444
|
+
print(f"\n{Fore.RED}{i18n.t('warn.need_root')}{Style.RESET_ALL}")
|
|
445
|
+
elif os.name == 'nt' and not is_admin_windows():
|
|
446
|
+
print(f"\n{Fore.RED}{i18n.t('warn.need_admin')}{Style.RESET_ALL}")
|
|
447
|
+
|
|
448
|
+
asyncio.run(main_flow())
|
|
449
|
+
except KeyboardInterrupt:
|
|
450
|
+
print(f"\n{Fore.RED}{i18n.t('common.interrupted')}{Style.RESET_ALL}")
|
|
451
|
+
sys.exit(1)
|
gyntoolkit/config.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Configuração central via ``.gyntoolkit.yaml`` com fallback nos defaults.
|
|
3
|
+
|
|
4
|
+
Ordem de resolução do arquivo:
|
|
5
|
+
1. Caminho explícito passado a :func:`load_config`.
|
|
6
|
+
2. Variável de ambiente ``GYNTOOLKIT_CONFIG``.
|
|
7
|
+
3. ``.gyntoolkit.yaml`` no diretório atual.
|
|
8
|
+
4. ``.gyntoolkit.yaml`` na raiz do projeto.
|
|
9
|
+
|
|
10
|
+
Sem arquivo, usa apenas os defaults. Valores do arquivo fazem *deep-merge*
|
|
11
|
+
por cima dos defaults, então basta declarar as chaves que quer sobrescrever.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import copy
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .core import PROJECT_ROOT, log
|
|
20
|
+
|
|
21
|
+
# Defaults espelham os valores hardcoded originais da CLI.
|
|
22
|
+
DEFAULTS: dict[str, Any] = {
|
|
23
|
+
"scan": {
|
|
24
|
+
"default_type": "fast", # "fast" ou "full"
|
|
25
|
+
},
|
|
26
|
+
"brute": {
|
|
27
|
+
"ssh_workers": 8,
|
|
28
|
+
"http_workers": 10,
|
|
29
|
+
"delay": 0.1,
|
|
30
|
+
"ssh_timeout": 5,
|
|
31
|
+
"http_timeout": 10,
|
|
32
|
+
"users_wordlist": "", # path default sugerido no prompt (opcional)
|
|
33
|
+
"passwords_wordlist": "", # idem
|
|
34
|
+
},
|
|
35
|
+
"recon": {
|
|
36
|
+
"subdomain_timeout": 30,
|
|
37
|
+
"ssl_timeout": 8,
|
|
38
|
+
"http_timeout": 10,
|
|
39
|
+
"traceroute_max_hops": 20,
|
|
40
|
+
"traceroute_dport": 80,
|
|
41
|
+
},
|
|
42
|
+
"export": {
|
|
43
|
+
"dir": "reports", # diretório de saída dos relatórios
|
|
44
|
+
"auto": False, # exportar sem perguntar
|
|
45
|
+
"format": "json", # formato default quando auto=True: json|html
|
|
46
|
+
},
|
|
47
|
+
"ui": {
|
|
48
|
+
"lang": "auto", # idioma da UI: auto | en | pt
|
|
49
|
+
},
|
|
50
|
+
"api_keys": {
|
|
51
|
+
"hibp": "", # reservado (endpoints atuais são públicos)
|
|
52
|
+
"shodan": "",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
CONFIG_FILENAME = ".gyntoolkit.yaml"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
+
"""Merge recursivo de ``override`` sobre ``base`` (não muta os originais)."""
|
|
61
|
+
result = copy.deepcopy(base)
|
|
62
|
+
for key, value in override.items():
|
|
63
|
+
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
64
|
+
result[key] = _deep_merge(result[key], value)
|
|
65
|
+
else:
|
|
66
|
+
result[key] = value
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _candidate_paths(explicit: str | None) -> list:
|
|
71
|
+
paths = []
|
|
72
|
+
if explicit:
|
|
73
|
+
paths.append(Path(explicit).expanduser())
|
|
74
|
+
env = os.environ.get("GYNTOOLKIT_CONFIG")
|
|
75
|
+
if env:
|
|
76
|
+
paths.append(Path(env).expanduser())
|
|
77
|
+
paths.append(Path.cwd() / CONFIG_FILENAME)
|
|
78
|
+
paths.append(PROJECT_ROOT / CONFIG_FILENAME)
|
|
79
|
+
return paths
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_config(path: str | None = None) -> dict[str, Any]:
|
|
83
|
+
"""Carrega config do primeiro arquivo existente, mesclado sobre os defaults."""
|
|
84
|
+
config_file = next((p for p in _candidate_paths(path) if p.is_file()), None)
|
|
85
|
+
if not config_file:
|
|
86
|
+
return copy.deepcopy(DEFAULTS)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
import yaml
|
|
90
|
+
except ImportError:
|
|
91
|
+
log.warning("pyyaml não instalado; usando defaults. Rode: pip install pyyaml")
|
|
92
|
+
return copy.deepcopy(DEFAULTS)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
with config_file.open("r", encoding="utf-8") as f:
|
|
96
|
+
data = yaml.safe_load(f) or {}
|
|
97
|
+
if not isinstance(data, dict):
|
|
98
|
+
log.warning("Config %s não é um mapeamento; usando defaults.", config_file)
|
|
99
|
+
return copy.deepcopy(DEFAULTS)
|
|
100
|
+
merged = _deep_merge(DEFAULTS, data)
|
|
101
|
+
log.info("Config carregada de %s", config_file)
|
|
102
|
+
return merged
|
|
103
|
+
except Exception as e: # OSError, yaml.YAMLError, etc.
|
|
104
|
+
log.warning("Falha ao ler config %s: %s; usando defaults.", config_file, e)
|
|
105
|
+
return copy.deepcopy(DEFAULTS)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Config resolvida uma vez na importação; a CLI lê daqui.
|
|
109
|
+
CONFIG: dict[str, Any] = load_config()
|
gyntoolkit/core.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Núcleo compartilhado: constantes, logging, helpers de terminal e menu."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from colorama import Fore, Style, init
|
|
11
|
+
|
|
12
|
+
from . import i18n
|
|
13
|
+
|
|
14
|
+
# Raiz do projeto (um nível acima do pacote) — usada p/ localizar cupp/, etc.
|
|
15
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
|
|
17
|
+
# UTF-8 stdout/stderr no Windows (Python 3.7+)
|
|
18
|
+
try:
|
|
19
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
20
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
21
|
+
except (AttributeError, OSError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
# Inicialização do Colorama
|
|
25
|
+
init(autoreset=True)
|
|
26
|
+
|
|
27
|
+
# Logging estruturado (arquivo + stderr silencioso p/ não poluir UI)
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
level=logging.WARNING,
|
|
30
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
31
|
+
handlers=[logging.FileHandler("gyntoolkit.log", encoding="utf-8")],
|
|
32
|
+
)
|
|
33
|
+
log = logging.getLogger("gyntoolkit")
|
|
34
|
+
|
|
35
|
+
# --------------------------
|
|
36
|
+
# Configurações e Constantes
|
|
37
|
+
# --------------------------
|
|
38
|
+
LOGO = f"""{Fore.GREEN}
|
|
39
|
+
██████╗██╗ ██╗███╗ ██╗ ████████╗ ██████╗ ██████╗ ██╗ ██╗ ██╗██╗████████╗
|
|
40
|
+
██╔════╝╚██╗ ██╔╝████╗ ██║ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██║ ██╔╝██║╚══██╔══╝
|
|
41
|
+
██║ ███╗╚████╔╝ ██╔██╗ ██║ ██║ ██║ ██║██║ ██║██║ █████╔╝ ██║ ██║
|
|
42
|
+
██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.0
|
|
43
|
+
╚██████╔╝ ██║ ██║ ╚████║ ██║ ╚██████╔╝╚██████╔╝███████╗██║ ██╗██║ ██║ by: PH,Fl4vs
|
|
44
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
45
|
+
{Style.RESET_ALL}"""
|
|
46
|
+
|
|
47
|
+
PROMPT = f"{Fore.RED}gyntoolkit:~# {Style.RESET_ALL}"
|
|
48
|
+
TOP_PORTS = [21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080, 8443]
|
|
49
|
+
MAX_THREADS = 100
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --------------------------
|
|
53
|
+
# Funções Utilitárias
|
|
54
|
+
# --------------------------
|
|
55
|
+
def clear_screen():
|
|
56
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
57
|
+
|
|
58
|
+
def is_admin_windows():
|
|
59
|
+
"""Verifica se é administrador no Windows"""
|
|
60
|
+
try:
|
|
61
|
+
from ctypes import windll
|
|
62
|
+
return windll.shell32.IsUserAnAdmin() != 0
|
|
63
|
+
except Exception:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def sanitize_input(input_str: str, pattern: str = r"[A-Za-z0-9./:-]") -> str:
|
|
67
|
+
"""Remove caracteres não permitidos da entrada"""
|
|
68
|
+
return ''.join(re.findall(pattern, input_str))
|
|
69
|
+
|
|
70
|
+
def show_menu(title: str, options: list) -> int:
|
|
71
|
+
"""Exibe menu interativo com tratamento de erros"""
|
|
72
|
+
while True:
|
|
73
|
+
clear_screen()
|
|
74
|
+
print(LOGO)
|
|
75
|
+
print(f"\n{Fore.CYAN}{title}{Style.RESET_ALL}")
|
|
76
|
+
for idx, opt in enumerate(options, 1):
|
|
77
|
+
print(f" {Fore.YELLOW}[{idx}]{Style.RESET_ALL} {opt}")
|
|
78
|
+
print(f" {Fore.YELLOW}[0]{Style.RESET_ALL} {i18n.t('common.back_exit')}")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
choice = int(input(f"\n{PROMPT}"))
|
|
82
|
+
if 0 <= choice <= len(options):
|
|
83
|
+
return choice
|
|
84
|
+
raise ValueError
|
|
85
|
+
except ValueError:
|
|
86
|
+
print(f"\n{Fore.RED}{i18n.t('common.invalid_option')}{Style.RESET_ALL}")
|