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.
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/PKG-INFO +22 -3
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/README.md +21 -2
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/__init__.py +7 -1
- gyntoolkit-2.3.0/gyntoolkit/audit.py +162 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/cli.py +63 -3
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/commands.py +48 -2
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/core.py +1 -1
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/i18n.py +26 -2
- gyntoolkit-2.3.0/gyntoolkit/mailsec.py +159 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/scan.py +192 -17
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/PKG-INFO +22 -3
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/SOURCES.txt +5 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/pyproject.toml +1 -1
- gyntoolkit-2.3.0/tests/test_audit.py +94 -0
- gyntoolkit-2.3.0/tests/test_cve_enrich.py +116 -0
- gyntoolkit-2.3.0/tests/test_mailsec.py +109 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/LICENSE +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/__main__.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/brute.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/config.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/export.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/recon.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/ui.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/utils.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/web.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit/wordlist.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/dependency_links.txt +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/entry_points.txt +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/requires.txt +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/gyntoolkit.egg-info/top_level.txt +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/setup.cfg +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_brute.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_commands.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_config.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_export.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_i18n.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_scan_parse.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_shodan.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_utils.py +0 -0
- {gyntoolkit-2.2.0 → gyntoolkit-2.3.0}/tests/test_web.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 = [
|
|
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(
|
|
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
|
-
|
|
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.
|
|
42
|
+
██║ ██║ ╚██╔╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║ v2.3
|
|
43
43
|
╚██████╔╝ ██║ ██║ ╚████║ ██║ ╚██████╔╝╚██████╔╝███████╗██║ ██╗██║ ██║ by: PH,Fl4vs
|
|
44
44
|
╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
45
45
|
{Style.RESET_ALL}"""
|