gyntoolkit 2.2.0__tar.gz → 2.3.0__tar.gz

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.
Files changed (41) hide show
  1. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/PKG-INFO +22 -3
  2. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/README.md +21 -2
  3. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/__init__.py +7 -1
  4. gyntoolkit-2.3.0/gyntoolkit/audit.py +162 -0
  5. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/cli.py +63 -3
  6. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/commands.py +48 -2
  7. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/core.py +1 -1
  8. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/i18n.py +26 -2
  9. gyntoolkit-2.3.0/gyntoolkit/mailsec.py +159 -0
  10. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/scan.py +192 -17
  11. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/PKG-INFO +22 -3
  12. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/SOURCES.txt +5 -0
  13. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/pyproject.toml +1 -1
  14. gyntoolkit-2.3.0/tests/test_audit.py +94 -0
  15. gyntoolkit-2.3.0/tests/test_cve_enrich.py +116 -0
  16. gyntoolkit-2.3.0/tests/test_mailsec.py +109 -0
  17. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/LICENSE +0 -0
  18. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/__main__.py +0 -0
  19. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/brute.py +0 -0
  20. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/config.py +0 -0
  21. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/export.py +0 -0
  22. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/recon.py +0 -0
  23. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/ui.py +0 -0
  24. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/utils.py +0 -0
  25. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/web.py +0 -0
  26. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/wordlist.py +0 -0
  27. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/dependency_links.txt +0 -0
  28. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/entry_points.txt +0 -0
  29. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/requires.txt +0 -0
  30. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/top_level.txt +0 -0
  31. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/setup.cfg +0 -0
  32. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_brute.py +0 -0
  33. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_commands.py +0 -0
  34. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_config.py +0 -0
  35. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_export.py +0 -0
  36. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_i18n.py +0 -0
  37. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_scan_parse.py +0 -0
  38. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_shodan.py +0 -0
  39. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_utils.py +0 -0
  40. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_web.py +0 -0
  41. {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_wordlist.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gyntoolkit
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Canivete suíço de pentest — recon, port scan, CVE lookup e brute force.
5
5
  Author: PH, Fl4vs
6
6
  License-Expression: MIT
@@ -49,7 +49,7 @@ Dynamic: license-file
49
49
  ██████╗██╗ ██╗███╗ ██╗ ████████╗ ██████╗ ██████╗ ██╗ ██╗ ██╗██╗████████╗
50
50
  ██╔════╝╚██╗ ██╔╝████╗ ██║ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██║ ██╔╝██║╚══██╔══╝
51
51
  ██║ ███╗╚████╔╝ ██╔██╗ ██║ ██║ ██║ ██║██║ ██║██║ █████╔╝ ██║ ██║
52
- ██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.2
52
+ ██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.3
53
53
  ╚██████╔╝ ██║ ██║ ╚████║ ██║ ╚██████╔╝╚██████╔╝███████╗██║ ██╗██║ ██║ by: PH,Fl4vs
54
54
  ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
55
55
  ```
@@ -127,6 +127,7 @@ Dynamic: license-file
127
127
  | 12 | **DNS Zone Transfer (AXFR)** | `dnspython` — tries AXFR against each authoritative NS |
128
128
  | 13 | **Web Content Discovery** | `aiohttp` — robots.txt / sitemap / security.txt + built-in path wordlist |
129
129
  | 14 | **Shodan Host** | Full `api.shodan.io` host lookup (ports, CPEs, CVEs, tags) — needs `api_keys.shodan`; falls back to InternetDB hint without a key |
130
+ | 15 | **Email Security** | `dnspython` — SPF, DKIM, DMARC, DNSSEC and CAA analyzer with per-item verdict (`ok`/`weak`/`missing`) |
130
131
 
131
132
  ### `[2]` Brute Force
132
133
 
@@ -148,6 +149,9 @@ Dynamic: license-file
148
149
  - **CVE analysis** per service via **NVD API v2.0** — parses product + version
149
150
  from the banner and matches by **CPE** (`virtualMatchString`), falling back to
150
151
  keyword search; rate-limited and deduplicated per banner
152
+ - **Risk scoring** — each CVE is enriched with **CVSS** base score/severity,
153
+ **EPSS** exploitation probability and a **CISA KEV** flag; host risk is the
154
+ highest severity found (KEV forces critical). Filter with `--min-cvss` / `--kev-only`
151
155
  - **Host discovery** (ARP scan) by CIDR — e.g. `192.168.0.0/24`
152
156
  - Automatic risk classification based on CVEs found
153
157
 
@@ -161,6 +165,12 @@ Dynamic: license-file
161
165
  | 4 | **Base64 decode** | base64 → UTF-8 with auto-padding |
162
166
  | 5 | **JWT decode** | Header + payload without signature verification |
163
167
 
168
+ ### `[5]` Audit — one target, consolidated profile
169
+
170
+ Orchestrates the recon modules (and, with `--active --authorize`, the port scan)
171
+ into a **single report** with an executive summary. Passive by default; stage
172
+ selection via `--only` / `--skip`. See the [CLI examples](#non-interactive-cli-scriptable).
173
+
164
174
  ---
165
175
 
166
176
  ## Installation
@@ -221,13 +231,22 @@ gyntoolkit recon axfr example.com # zone transfer attempt
221
231
  gyntoolkit recon webscan http://example.com # web content discovery
222
232
  gyntoolkit recon shodan 1.1.1.1 # Shodan host (needs api_keys.shodan)
223
233
  gyntoolkit recon ssl example.com --port 443 # TLS cert
224
- gyntoolkit scan 127.0.0.1 --type fast # port scan + CVEs
234
+ gyntoolkit scan 127.0.0.1 --type fast # port scan + CVEs (CVSS/EPSS/KEV)
235
+ gyntoolkit scan 10.0.0.5 --min-cvss 7.0 --kev-only # only high-risk / actively exploited
225
236
  gyntoolkit scan 192.168.0.0/24 # host discovery (CIDR)
237
+ gyntoolkit audit example.com # full passive profile → one report
238
+ gyntoolkit audit 10.0.0.5 --active --authorize # + active port scan (authorized)
226
239
  gyntoolkit utils hash "text" --algo sha1
227
240
  gyntoolkit utils jwt <token>
228
241
  gyntoolkit wordlist --terms alice,fluffy,acme --years 1990,2020 --leet --save wl.txt
229
242
  ```
230
243
 
244
+ `audit` runs a pipeline over one target and consolidates everything into a single
245
+ report with an executive summary. It is **passive by default** (WHOIS, DNS, geo,
246
+ subdomains, TLS, HTTP fingerprint, InternetDB, HIBP, email security); the active
247
+ port scan only runs with `--active --authorize`. Narrow it with `--only a,b` or
248
+ `--skip a,b`.
249
+
231
250
  Global flags (place after the subcommand): `-o/--output DIR` saves a report,
232
251
  `-f/--format {json,html,csv,md}` picks its format, `-q/--quiet` silences stdout,
233
252
  `--lang {en,pt}` overrides the language.
@@ -6,7 +6,7 @@
6
6
  ██████╗██╗ ██╗███╗ ██╗ ████████╗ ██████╗ ██████╗ ██╗ ██╗ ██╗██╗████████╗
7
7
  ██╔════╝╚██╗ ██╔╝████╗ ██║ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██║ ██╔╝██║╚══██╔══╝
8
8
  ██║ ███╗╚████╔╝ ██╔██╗ ██║ ██║ ██║ ██║██║ ██║██║ █████╔╝ ██║ ██║
9
- ██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.2
9
+ ██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.3
10
10
  ╚██████╔╝ ██║ ██║ ╚████║ ██║ ╚██████╔╝╚██████╔╝███████╗██║ ██╗██║ ██║ by: PH,Fl4vs
11
11
  ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
12
12
  ```
@@ -84,6 +84,7 @@
84
84
  | 12 | **DNS Zone Transfer (AXFR)** | `dnspython` — tries AXFR against each authoritative NS |
85
85
  | 13 | **Web Content Discovery** | `aiohttp` — robots.txt / sitemap / security.txt + built-in path wordlist |
86
86
  | 14 | **Shodan Host** | Full `api.shodan.io` host lookup (ports, CPEs, CVEs, tags) — needs `api_keys.shodan`; falls back to InternetDB hint without a key |
87
+ | 15 | **Email Security** | `dnspython` — SPF, DKIM, DMARC, DNSSEC and CAA analyzer with per-item verdict (`ok`/`weak`/`missing`) |
87
88
 
88
89
  ### `[2]` Brute Force
89
90
 
@@ -105,6 +106,9 @@
105
106
  - **CVE analysis** per service via **NVD API v2.0** — parses product + version
106
107
  from the banner and matches by **CPE** (`virtualMatchString`), falling back to
107
108
  keyword search; rate-limited and deduplicated per banner
109
+ - **Risk scoring** — each CVE is enriched with **CVSS** base score/severity,
110
+ **EPSS** exploitation probability and a **CISA KEV** flag; host risk is the
111
+ highest severity found (KEV forces critical). Filter with `--min-cvss` / `--kev-only`
108
112
  - **Host discovery** (ARP scan) by CIDR — e.g. `192.168.0.0/24`
109
113
  - Automatic risk classification based on CVEs found
110
114
 
@@ -118,6 +122,12 @@
118
122
  | 4 | **Base64 decode** | base64 → UTF-8 with auto-padding |
119
123
  | 5 | **JWT decode** | Header + payload without signature verification |
120
124
 
125
+ ### `[5]` Audit — one target, consolidated profile
126
+
127
+ Orchestrates the recon modules (and, with `--active --authorize`, the port scan)
128
+ into a **single report** with an executive summary. Passive by default; stage
129
+ selection via `--only` / `--skip`. See the [CLI examples](#non-interactive-cli-scriptable).
130
+
121
131
  ---
122
132
 
123
133
  ## Installation
@@ -178,13 +188,22 @@ gyntoolkit recon axfr example.com # zone transfer attempt
178
188
  gyntoolkit recon webscan http://example.com # web content discovery
179
189
  gyntoolkit recon shodan 1.1.1.1 # Shodan host (needs api_keys.shodan)
180
190
  gyntoolkit recon ssl example.com --port 443 # TLS cert
181
- gyntoolkit scan 127.0.0.1 --type fast # port scan + CVEs
191
+ gyntoolkit scan 127.0.0.1 --type fast # port scan + CVEs (CVSS/EPSS/KEV)
192
+ gyntoolkit scan 10.0.0.5 --min-cvss 7.0 --kev-only # only high-risk / actively exploited
182
193
  gyntoolkit scan 192.168.0.0/24 # host discovery (CIDR)
194
+ gyntoolkit audit example.com # full passive profile → one report
195
+ gyntoolkit audit 10.0.0.5 --active --authorize # + active port scan (authorized)
183
196
  gyntoolkit utils hash "text" --algo sha1
184
197
  gyntoolkit utils jwt <token>
185
198
  gyntoolkit wordlist --terms alice,fluffy,acme --years 1990,2020 --leet --save wl.txt
186
199
  ```
187
200
 
201
+ `audit` runs a pipeline over one target and consolidates everything into a single
202
+ report with an executive summary. It is **passive by default** (WHOIS, DNS, geo,
203
+ subdomains, TLS, HTTP fingerprint, InternetDB, HIBP, email security); the active
204
+ port scan only runs with `--active --authorize`. Narrow it with `--only a,b` or
205
+ `--skip a,b`.
206
+
188
207
  Global flags (place after the subcommand): `-o/--output DIR` saves a report,
189
208
  `-f/--format {json,html,csv,md}` picks its format, `-q/--quiet` silences stdout,
190
209
  `--lang {en,pt}` overrides the language.
@@ -5,9 +5,10 @@ Este ``__init__`` re-exporta a API pública para manter compatibilidade com
5
5
  ``import gyntoolkit as g`` (usado pelo lab E2E) e pelo entry point do pyproject.
6
6
  """
7
7
 
8
- __version__ = "2.2.0"
8
+ __version__ = "2.3.0"
9
9
 
10
10
  from . import i18n, ui
11
+ from .audit import run_audit
11
12
  from .brute import (
12
13
  cupp_generate,
13
14
  http_bruteforce,
@@ -30,6 +31,7 @@ from .core import (
30
31
  show_menu,
31
32
  )
32
33
  from .export import export_csv, export_html, export_json, export_md, save_report
34
+ from .mailsec import DEFAULT_DKIM_SELECTORS, mailsec_report
33
35
  from .recon import (
34
36
  DNS_TYPES,
35
37
  TECH_SIGNATURES,
@@ -76,6 +78,10 @@ __all__ = [
76
78
  "escolher_tipo_dns", "DNS_TYPES", "TECH_SIGNATURES",
77
79
  # web discovery
78
80
  "web_discovery", "COMMON_PATHS",
81
+ # mail security
82
+ "mailsec_report", "DEFAULT_DKIM_SELECTORS",
83
+ # audit
84
+ "run_audit",
79
85
  # wordlist
80
86
  "generate_wordlist",
81
87
  # brute
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ """Audit: orquestrador de engajamento — 1 alvo, perfil consolidado.
3
+
4
+ Roda um pipeline de etapas (recon passivo por padrão; scan ativo só com
5
+ ``active=True`` + ``authorized=True``) e agrega tudo num único relatório com
6
+ resumo executivo. Reaproveita os módulos existentes (recon/scan/mailsec).
7
+
8
+ Etapas são resilientes: a falha de uma registra ``{"error": ...}`` na sua seção
9
+ sem derrubar o resto.
10
+ """
11
+
12
+ import asyncio
13
+ import ipaddress
14
+ import socket
15
+ from datetime import datetime, timezone
16
+ from typing import Any
17
+
18
+ from .core import log, sanitize_input
19
+
20
+ # Etapas passivas (seguras) e ativas (intrusivas). Ordem define a saída.
21
+ PASSIVE_STAGES = ("whois", "dns", "geo", "subdomains", "ssl", "httpfp",
22
+ "internetdb", "hibp", "mailsec")
23
+ ACTIVE_STAGES = ("scan",)
24
+ ALL_STAGES = PASSIVE_STAGES + ACTIVE_STAGES
25
+
26
+ # Etapas que só fazem sentido p/ domínio (não p/ IP literal).
27
+ _DOMAIN_ONLY = {"whois", "subdomains", "hibp", "mailsec"}
28
+
29
+
30
+ def _is_ip(target: str) -> bool:
31
+ try:
32
+ ipaddress.ip_address(target)
33
+ return True
34
+ except ValueError:
35
+ return False
36
+
37
+
38
+ def _resolve(target: str) -> str | None:
39
+ try:
40
+ return socket.gethostbyname(target)
41
+ except socket.gaierror:
42
+ return None
43
+
44
+
45
+ # --------------------------------------------------------------------------
46
+ # Runners por etapa.
47
+ # --------------------------------------------------------------------------
48
+ def _passive_stage(name: str, target: str, ctx: dict) -> Any:
49
+ """Executa uma etapa passiva (síncrona: requests/dns). Roda em thread."""
50
+ from . import recon
51
+ from .mailsec import mailsec_report
52
+
53
+ is_ip = ctx["is_ip"]
54
+ if name == "whois":
55
+ return {"whois": str(recon.whois_lookup(target))}
56
+ if name == "dns":
57
+ return {t: recon.dns_lookup(target, t) for t in ("A", "MX", "NS", "TXT")}
58
+ if name == "geo":
59
+ return recon.geo_ip(target)
60
+ if name == "subdomains":
61
+ subs = recon.subdomain_enum(target)
62
+ return {"total": len(subs), "subdomains": subs}
63
+ if name == "ssl":
64
+ return recon.ssl_inspect(target, 443)
65
+ if name == "httpfp":
66
+ scheme = "http" if is_ip else "https"
67
+ return recon.http_fingerprint(f"{scheme}://{target}")
68
+ if name == "internetdb":
69
+ return recon.internetdb_lookup(target)
70
+ if name == "hibp":
71
+ return {"breaches": recon.hibp_breaches(target)}
72
+ if name == "mailsec":
73
+ return mailsec_report(target)
74
+ raise ValueError(f"unknown passive stage: {name}")
75
+
76
+
77
+ async def _run_one(name: str, target: str, ctx: dict) -> tuple[str, Any]:
78
+ """Executa uma etapa com captura de erro. Passivas rodam em thread."""
79
+ try:
80
+ if name == "scan":
81
+ from .scan import perform_scan
82
+
83
+ ip = target if ctx["is_ip"] else (_resolve(target) or target)
84
+ results = await perform_scan(
85
+ ip, ctx["scan_type"], concurrency=ctx["concurrency"], nvd_api_key=ctx["nvd_key"],
86
+ )
87
+ return (name, {str(port): data for port, data in results.items()})
88
+ return (name, await asyncio.to_thread(_passive_stage, name, target, ctx))
89
+ except Exception as e: # noqa: BLE001 - etapa resiliente
90
+ log.warning("audit stage '%s' falhou: %s", name, e)
91
+ return (name, {"error": str(e)})
92
+
93
+
94
+ def _select_stages(is_ip: bool, active: bool, only: list[str] | None,
95
+ skip: list[str] | None) -> list[str]:
96
+ """Resolve a lista final de etapas conforme tipo de alvo e flags."""
97
+ stages = list(PASSIVE_STAGES) + (list(ACTIVE_STAGES) if active else [])
98
+ if only:
99
+ wanted = set(only)
100
+ stages = [s for s in ALL_STAGES if s in wanted and (active or s not in ACTIVE_STAGES)]
101
+ if skip:
102
+ stages = [s for s in stages if s not in set(skip)]
103
+ if is_ip:
104
+ stages = [s for s in stages if s not in _DOMAIN_ONLY]
105
+ return stages
106
+
107
+
108
+ def _summarize(stages_out: dict) -> dict:
109
+ """Resumo executivo: contagens e destaques das seções coletadas."""
110
+ summary: dict[str, Any] = {}
111
+ scan = stages_out.get("scan")
112
+ if isinstance(scan, dict) and "error" not in scan:
113
+ open_ports = list(scan.keys())
114
+ cves = [c for port in scan.values() for c in port.get("vulnerabilidades", [])]
115
+ kev = [c for c in cves if c.get("kev")]
116
+ summary["open_ports"] = len(open_ports)
117
+ summary["cves"] = len(cves)
118
+ summary["kev_cves"] = len(kev)
119
+ subs = stages_out.get("subdomains")
120
+ if isinstance(subs, dict) and "total" in subs:
121
+ summary["subdomains"] = subs["total"]
122
+ mail = stages_out.get("mailsec")
123
+ if isinstance(mail, dict) and "summary" in mail:
124
+ summary["mailsec_weak"] = mail["summary"].get("weak", 0) + mail["summary"].get("missing", 0)
125
+ return summary
126
+
127
+
128
+ async def run_audit(
129
+ target: str,
130
+ active: bool = False,
131
+ authorized: bool = False,
132
+ only: list[str] | None = None,
133
+ skip: list[str] | None = None,
134
+ concurrency: int = 100,
135
+ nvd_key: str = "",
136
+ scan_type: str = "fast",
137
+ ) -> dict:
138
+ """Roda o audit e devolve o relatório consolidado.
139
+
140
+ Etapas ativas (scan) só rodam com ``active=True`` **e** ``authorized=True`` —
141
+ autorização é exigida p/ qualquer ação intrusiva.
142
+ """
143
+ target = sanitize_input(target, r"[A-Za-z0-9.:/-]")
144
+ is_ip = _is_ip(target)
145
+ run_active = active and authorized
146
+ ctx = {"is_ip": is_ip, "concurrency": concurrency, "nvd_key": nvd_key, "scan_type": scan_type}
147
+
148
+ stages = _select_stages(is_ip, run_active, only, skip)
149
+ results = await asyncio.gather(*[_run_one(s, target, ctx) for s in stages])
150
+ stages_out = dict(results)
151
+ # reordena conforme ALL_STAGES p/ saída estável
152
+ ordered = {s: stages_out[s] for s in ALL_STAGES if s in stages_out}
153
+
154
+ return {
155
+ "target": target,
156
+ "is_ip": is_ip,
157
+ "active": run_active,
158
+ "active_requested_without_auth": active and not authorized,
159
+ "generated": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
160
+ "summary": _summarize(ordered),
161
+ "stages": ordered,
162
+ }
@@ -10,6 +10,7 @@ from pathlib import Path
10
10
  from colorama import Fore, Style
11
11
 
12
12
  from . import i18n, ui
13
+ from .audit import run_audit
13
14
  from .brute import (
14
15
  cupp_generate,
15
16
  http_bruteforce,
@@ -20,6 +21,7 @@ from .brute import (
20
21
  from .config import CONFIG
21
22
  from .core import is_admin_windows, sanitize_input, show_menu
22
23
  from .export import save_report
24
+ from .mailsec import mailsec_report
23
25
  from .recon import (
24
26
  dns_lookup,
25
27
  escolher_tipo_dns,
@@ -63,13 +65,44 @@ def _normalize_scan_type(raw: str, default: str) -> str:
63
65
 
64
66
 
65
67
  def _scan_risk_label(risco: str) -> str:
66
- return i18n.t("scan.risk_high") if risco == "high" else i18n.t("scan.risk_low")
68
+ return i18n.t(f"scan.risk_{risco}") if risco in ("critical", "high", "medium", "low") else risco
67
69
 
68
70
 
69
71
  def _scan_service_label(service: str) -> str:
70
72
  return i18n.t("scan.service_unknown") if service == "unknown" else service
71
73
 
72
74
 
75
+ def _fmt_cve(v: dict) -> str:
76
+ """Formata um CVE enriquecido p/ célula de tabela: ID (CVSS, EPSS%, KEV)."""
77
+ parts = []
78
+ if v.get("cvss") is not None:
79
+ parts.append(f"CVSS {v['cvss']}")
80
+ if v.get("epss") is not None:
81
+ parts.append(f"EPSS {round(v['epss'] * 100, 1)}%")
82
+ if v.get("kev"):
83
+ parts.append("KEV")
84
+ suffix = f" ({', '.join(parts)})" if parts else ""
85
+ return f"{v['id']}{suffix}"
86
+
87
+
88
+ def _mailsec_detail(name: str, c: dict) -> str:
89
+ """Resumo curto de uma checagem de email security p/ a tabela."""
90
+ if name == "spf":
91
+ d = c.get("record") or "—"
92
+ if c.get("notes"):
93
+ d += " · " + "; ".join(c["notes"])
94
+ return d[:70]
95
+ if name == "dmarc":
96
+ return f"p={c.get('policy') or '—'}"
97
+ if name == "dkim":
98
+ return ", ".join(c.get("selectors_found") or []) or "no selector matched"
99
+ if name == "dnssec":
100
+ return f"DNSKEY={c.get('dnskey')} DS={c.get('ds')}"
101
+ if name == "caa":
102
+ return f"{len(c.get('records') or [])} record(s)"
103
+ return ""
104
+
105
+
73
106
  def _offer_export(data, basename: str, title: str) -> None:
74
107
  """Oferece salvar ``data`` em JSON/HTML conforme config. No-op se vazio."""
75
108
  if not data:
@@ -98,6 +131,7 @@ async def main_flow():
98
131
  i18n.t("menu.main.brute"),
99
132
  i18n.t("menu.main.scan"),
100
133
  i18n.t("menu.main.utils"),
134
+ i18n.t("menu.main.audit"),
101
135
  ])
102
136
 
103
137
  if choice == 0:
@@ -120,6 +154,7 @@ async def main_flow():
120
154
  i18n.t("menu.info.axfr"),
121
155
  i18n.t("menu.info.webscan"),
122
156
  i18n.t("menu.info.shodan"),
157
+ i18n.t("menu.info.mailsec"),
123
158
  ])
124
159
 
125
160
  if sub_choice == 1:
@@ -317,6 +352,18 @@ async def main_flow():
317
352
  ui.print_kv(i18n.t("label.shodan", t=target), info)
318
353
  _offer_export(info, f"shodan-{target}", f"Shodan {target}")
319
354
 
355
+ elif sub_choice == 15:
356
+ domain = sanitize_input(input(_cyan("prompt.root_domain")), r"[A-Za-z0-9.-]")
357
+ with ui.status(i18n.t("status.mailsec", d=domain)):
358
+ r = mailsec_report(domain)
359
+ rows = [
360
+ (name.upper(), i18n.t(f"mailsec.verdict_{c['verdict']}"), _mailsec_detail(name, c))
361
+ for name, c in r["checks"].items()
362
+ ]
363
+ ui.print_table(i18n.t("label.mailsec", d=domain),
364
+ i18n.t("table.mailsec").split("|"), rows)
365
+ _offer_export(r, f"mailsec-{domain}", f"Email Security {domain}")
366
+
320
367
  elif choice == 2: # Brute Force
321
368
  b = CONFIG["brute"]
322
369
  sub_choice = show_menu(i18n.t("menu.brute.title"), [
@@ -456,10 +503,13 @@ async def main_flow():
456
503
  rows = [
457
504
  (port, _scan_service_label(data["service"]), _scan_risk_label(data["risco"]),
458
505
  (data["banner"] or "")[:60],
459
- ", ".join(data["vulnerabilidades"]) or "—")
506
+ ", ".join(_fmt_cve(v) for v in data["vulnerabilidades"]) or "—")
460
507
  for port, data in results.items()
461
508
  ]
462
- row_styles = ["red" if data["risco"] == "high" else "green" for data in results.values()]
509
+ row_styles = [
510
+ "red" if data["risco"] in ("critical", "high") else "green"
511
+ for data in results.values()
512
+ ]
463
513
  if rows:
464
514
  ui.print_table(i18n.t("label.scan_results", h=host),
465
515
  i18n.t("table.scan").split("|"),
@@ -511,6 +561,16 @@ async def main_flow():
511
561
  print(f" {Fore.YELLOW}payload:{Style.RESET_ALL} {json.dumps(r['payload'], indent=2)}")
512
562
  print(f" {Fore.YELLOW}signature:{Style.RESET_ALL} {r['signature']}")
513
563
 
564
+ elif choice == 5: # Audit (perfil consolidado — passivo no menu)
565
+ target = sanitize_input(input(_cyan("prompt.target")))
566
+ ui.notice(i18n.t("msg.audit_passive_note"))
567
+ with ui.status(i18n.t("status.audit", t=target)):
568
+ report = await run_audit(target) # menu = passivo; scan ativo via CLI
569
+ ran = ", ".join(report["stages"].keys())
570
+ ui.print_kv(i18n.t("label.audit", t=target),
571
+ {**report["summary"], i18n.t("audit.stages_run"): ran})
572
+ _offer_export(report, f"audit-{target}", f"Audit {target}")
573
+
514
574
  _pause()
515
575
 
516
576
 
@@ -58,6 +58,12 @@ def _h_recon(args: argparse.Namespace) -> tuple[Any, str, str]:
58
58
  if a == "axfr":
59
59
  r = recon.zone_transfer(args.target)
60
60
  return (r, f"axfr-{args.target}", f"Zone Transfer {args.target}")
61
+ if a == "mailsec":
62
+ from .mailsec import mailsec_report
63
+
64
+ sels = [s for s in (args.selectors or "").split(",") if s.strip()] or None
65
+ r = mailsec_report(args.target, selectors=sels)
66
+ return (r, f"mailsec-{args.target}", f"Email Security {args.target}")
61
67
  if a == "webscan":
62
68
  from pathlib import Path as _P
63
69
 
@@ -107,7 +113,10 @@ def _h_scan(args: argparse.Namespace) -> tuple[Any, str, str]:
107
113
  f"Host discovery {args.target}")
108
114
  concurrency = args.concurrency or CONFIG["scan"].get("concurrency", 100)
109
115
  nvd_key = CONFIG.get("api_keys", {}).get("nvd", "")
110
- results = asyncio.run(perform_scan(args.target, args.type, concurrency=concurrency, nvd_api_key=nvd_key))
116
+ results = asyncio.run(perform_scan(
117
+ args.target, args.type, concurrency=concurrency, nvd_api_key=nvd_key,
118
+ min_cvss=args.min_cvss, kev_only=args.kev_only,
119
+ ))
111
120
  # chaves int → str p/ JSON estável
112
121
  report = {str(port): data for port, data in results.items()}
113
122
  return ({args.target: report}, f"scan-{args.target}", f"Scan {args.target}")
@@ -190,7 +199,24 @@ def _run_wordlist(args: argparse.Namespace) -> int:
190
199
  return 0
191
200
 
192
201
 
193
- _DISPATCH = {"recon": _h_recon, "scan": _h_scan, "utils": _h_utils, "brute": _h_brute}
202
+ def _h_audit(args: argparse.Namespace) -> tuple[Any, str, str]:
203
+ from .audit import run_audit
204
+
205
+ only = [s for s in (args.only or "").split(",") if s.strip()] or None
206
+ skip = [s for s in (args.skip or "").split(",") if s.strip()] or None
207
+ concurrency = CONFIG["scan"].get("concurrency", 100)
208
+ nvd_key = CONFIG.get("api_keys", {}).get("nvd", "")
209
+ report = asyncio.run(run_audit(
210
+ args.target, active=args.active, authorized=args.authorize,
211
+ only=only, skip=skip, concurrency=concurrency, nvd_key=nvd_key, scan_type=args.type,
212
+ ))
213
+ if report.get("active_requested_without_auth"):
214
+ print("[!] --active ignored without --authorize (scan skipped).", file=sys.stderr)
215
+ return (report, f"audit-{args.target}", f"Audit {args.target}")
216
+
217
+
218
+ _DISPATCH = {"recon": _h_recon, "scan": _h_scan, "utils": _h_utils,
219
+ "brute": _h_brute, "audit": _h_audit}
194
220
 
195
221
 
196
222
  def build_parser() -> argparse.ArgumentParser:
@@ -229,6 +255,9 @@ def build_parser() -> argparse.ArgumentParser:
229
255
  web_p = recon_sub.add_parser("webscan", parents=[io])
230
256
  web_p.add_argument("target", help="Base URL to enumerate.")
231
257
  web_p.add_argument("--wordlist", default=None, help="Path to a custom paths wordlist.")
258
+ mail_p = recon_sub.add_parser("mailsec", parents=[io])
259
+ mail_p.add_argument("target", help="Domain to analyze (SPF/DKIM/DMARC/DNSSEC/CAA).")
260
+ mail_p.add_argument("--selectors", default="", help="Comma-separated DKIM selectors to test.")
232
261
  tr_p = recon_sub.add_parser("traceroute", parents=[io])
233
262
  tr_p.add_argument("target")
234
263
  tr_p.add_argument("--max-hops", dest="max_hops", type=int,
@@ -241,6 +270,10 @@ def build_parser() -> argparse.ArgumentParser:
241
270
  scan_p.add_argument("target", help="IP/host, or CIDR (e.g. 192.168.0.0/24) for discovery.")
242
271
  scan_p.add_argument("--type", choices=("fast", "full"), default=CONFIG["scan"]["default_type"])
243
272
  scan_p.add_argument("--concurrency", type=int, default=0, help="Simultaneous probes (0 = config default).")
273
+ scan_p.add_argument("--min-cvss", dest="min_cvss", type=float, default=0.0,
274
+ help="Only show CVEs with CVSS base score >= this value.")
275
+ scan_p.add_argument("--kev-only", dest="kev_only", action="store_true",
276
+ help="Only show CVEs in the CISA KEV catalog.")
244
277
 
245
278
  # utils
246
279
  utils_p = sub.add_parser("utils", help="Crypto helpers.")
@@ -274,6 +307,19 @@ def build_parser() -> argparse.ArgumentParser:
274
307
  http_p.add_argument("--workers", type=int, default=0)
275
308
  http_p.add_argument("--authorize", action="store_true", help="Confirm you have written authorization.")
276
309
 
310
+ # audit (orquestrador: 1 alvo → perfil consolidado)
311
+ audit_p = sub.add_parser("audit", parents=[io],
312
+ help="Full target profile: passive recon (+ active scan with --active).")
313
+ audit_p.add_argument("target", help="Domain or IP to profile.")
314
+ audit_p.add_argument("--active", action="store_true",
315
+ help="Include the active port scan (requires --authorize).")
316
+ audit_p.add_argument("--authorize", action="store_true",
317
+ help="Confirm written authorization for the active scan.")
318
+ audit_p.add_argument("--type", choices=("fast", "full"), default="fast",
319
+ help="Scan type for the active stage.")
320
+ audit_p.add_argument("--only", default="", help="Comma-separated stages to run exclusively.")
321
+ audit_p.add_argument("--skip", default="", help="Comma-separated stages to skip.")
322
+
277
323
  # wordlist (gerador nativo; saída em linhas, não JSON)
278
324
  wl_p = sub.add_parser("wordlist", help="Generate a custom wordlist (CUPP-style, offline).")
279
325
  wl_p.add_argument("--terms", required=True, help="Comma-separated base words (name, pet, company, ...).")
@@ -39,7 +39,7 @@ LOGO = f"""{Fore.GREEN}
39
39
  ██████╗██╗ ██╗███╗ ██╗ ████████╗ ██████╗ ██████╗ ██╗ ██╗ ██╗██╗████████╗
40
40
  ██╔════╝╚██╗ ██╔╝████╗ ██║ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██║ ██╔╝██║╚══██╔══╝
41
41
  ██║ ███╗╚████╔╝ ██╔██╗ ██║ ██║ ██║ ██║██║ ██║██║ █████╔╝ ██║ ██║
42
- ██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.2
42
+ ██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.3
43
43
  ╚██████╔╝ ██║ ██║ ╚████║ ██║ ╚██████╔╝╚██████╔╝███████╗██║ ██╗██║ ██║ by: PH,Fl4vs
44
44
  ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
45
45
  {Style.RESET_ALL}"""