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/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}")