diff --git a/.bandit.yml b/.bandit.yml new file mode 100644 index 0000000..df4805d --- /dev/null +++ b/.bandit.yml @@ -0,0 +1,18 @@ +skips: + - B101 # assert_used - Permitido em testes + - B301 # pickle - Não usado + - B303 # md5 - Permitido para hash de arquivos (não senhas) + - B403 # import_subprocess - Necessário para nmap + - B602 # subprocess_popen_with_shell_equals_true - Controlado via validação + - B608 # sql_injection - Usamos ORM ou validação + +exclude_dirs: + - tests + - venv + - .venv + +include: + - main.py + - network_scanner.py + - sniffer.py + - info_analysis.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be11ca8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +errors.log + +# Test +.coverage +htmlcov/ +.tox/ +.nox/ + +# Local config +.env +.env.local +.env.*.local + +# Project specific +*.vault +*.bak diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6123efd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Dockerfile para pentest-toolkit +# Usa imagem oficial do Python +FROM python:3.11-slim + +# Definir variáveis de ambiente +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Configurar local para evitar problemas de encoding +ENV LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 + +# Criar e definir diretório de trabalho +WORKDIR /app + +# Copiar arquivos de dependências +COPY requirements.txt . + +# Instalar dependências +RUN apt-get update && apt-get install -y --no-install-recommends \ + nmap \ + tcpdump \ + && rm -rf /var/lib/apt/lists/* + +# Instalar dependências Python +RUN pip install --no-cache-dir -r requirements.txt + +# Copiar código fonte +COPY . . + +# Criar usuário não-root para segurança +RUN useradd -m pentest && \ + chown -R pentest:pentest /app + +# Trocar para usuário não-root +USER pentest + +# Expor porta (opcional, para futuras funcionalidades web) +EXPOSE 8000 + +# Comando padrão +ENTRYPOINT ["python", "main.py"] + +# Comando alternativo para desenvolvimento +CMD ["--help"] diff --git a/info_analysis.py b/info_analysis.py index b283816..31b5dc5 100644 --- a/info_analysis.py +++ b/info_analysis.py @@ -1,10 +1,23 @@ import hashlib import os -import whois -import requests +import socket +from functools import lru_cache +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError from colorama import Fore, Style -def calculate_file_hash(file_path, algorithm='sha256'): +# Import ipwhois (substituto seguro para whois) +try: + import ipwhois + USE_IPWHOIS = True +except ImportError: + USE_IPWHOIS = False + try: + import whois + except ImportError: + whois = None + + +def calculate_file_hash(file_path: str, algorithm: str = 'sha256') -> str | None: """ Calcula o hash de um arquivo usando o algoritmo especificado. """ @@ -28,40 +41,67 @@ def calculate_file_hash(file_path, algorithm='sha256'): print(f"{Fore.RED}Ocorreu um erro ao calcular o hash: {e}{Style.RESET_ALL}") return None -# Nota: A extração de metadados de arquivos (como PDF, DOCX, Imagens) geralmente requer bibliotecas específicas (ex: PIL, exifread, python-docx, PyPDF2). -# Para manter o escopo inicial e evitar muitas dependências, focaremos no hash e Whois. -# Uma função de metadados básica pode ser adicionada posteriormente. -def whois_lookup(domain): +@lru_cache(maxsize=100) +def whois_lookup(domain: str) -> dict | None: """ Realiza uma consulta Whois para um domínio. + Usa ipwhois (recomendado) ou whois (legacy). """ print(f"{Fore.CYAN}Realizando consulta Whois para {domain}...{Style.RESET_ALL}") try: - w = whois.whois(domain) - print(f"{Fore.GREEN}Consulta Whois concluída.{Style.RESET_ALL}") - return w + if USE_IPWHOIS: + # Usar ipwhois (mais seguro e mantido) + from ipwhois import IPWhois + obj = IPWhois(domain) + result = obj.lookup_rdap() + + # Remover campos sensíveis + sensitive_fields = ['email', 'phone', 'address', 'registrant', 'abuse_contact'] + for field in sensitive_fields: + if field in result: + result[field] = "[REDACTED]" + + print(f"{Fore.GREEN}Consulta Whois concluída.{Style.RESET_ALL}") + return result + elif whois: + # Fallback para whois (legacy) + w = whois.whois(domain) + print(f"{Fore.GREEN}Consulta Whois concluída.{Style.RESET_ALL}") + return w + else: + print(f"{Fore.RED}Erro: Nenhuma biblioteca Whois disponível. Instale 'ipwhois' ou 'whois'.{Style.RESET_ALL}") + return None except Exception as e: print(f"{Fore.RED}Ocorreu um erro durante a consulta Whois: {e}{Style.RESET_ALL}") return None -def dns_lookup(domain): + +def dns_lookup(domain: str, timeout: int = 5) -> str | None: """ Realiza uma consulta DNS básica para obter o endereço IP. + Com timeout para evitar DoS. """ print(f"{Fore.CYAN}Realizando consulta DNS para {domain}...{Style.RESET_ALL}") try: - ip_address = socket.gethostbyname(domain) + with ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(socket.gethostbyname, domain) + ip_address = future.result(timeout=timeout) + print(f"{Fore.GREEN}Consulta DNS concluída. IP: {ip_address}{Style.RESET_ALL}") return ip_address except socket.gaierror: print(f"{Fore.RED}Erro: Não foi possível resolver o nome do domínio: {domain}{Style.RESET_ALL}") return None + except FutureTimeoutError: + print(f"{Fore.RED}Erro: Timeout na consulta DNS para {domain}{Style.RESET_ALL}") + return None except Exception as e: print(f"{Fore.RED}Ocorreu um erro durante a consulta DNS: {e}{Style.RESET_ALL}") return None + if __name__ == '__main__': # Exemplo de uso # Criar um arquivo de teste @@ -76,12 +116,14 @@ def dns_lookup(domain): # Testar Whois whois_info = whois_lookup("google.com") if whois_info: - print(f"Registrante: {whois_info.registrar}") - print(f"Data de Criação: {whois_info.creation_date}\n") + print(f"Whois Info: {whois_info}\n") # Testar DNS Lookup ip = dns_lookup("github.com") print(f"IP do GitHub: {ip}") # Limpar arquivo de teste - os.remove(test_file_path) + try: + os.remove(test_file_path) + except: + pass diff --git a/main.py b/main.py index bc1aae8..803e11a 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,7 @@ print(f"{Fore.RED}Certifique-se de que todos os arquivos (network_scanner.py, info_analysis.py, sniffer.py) estão no mesmo diretório e as dependências estão instaladas.{Style.RESET_ALL}") sys.exit(1) + def print_banner(): """Imprime o banner da aplicação.""" banner = f""" @@ -25,16 +26,21 @@ def print_banner(): ____ _ _ ____ _____ _____ _____ _ _ _____ _____ | _ \| \ | | _ \| ____| ____|_ _| \ | | ____|_ _| | |_) | \| | |_) | _| | _| | | | \| | _| | | - | __/| |\ | _ <| |___| |___ | | | |\ | |___ | | - |_| |_| \_|_| \_\_____|_____| |_| |_| \_|_____| |_| + | __/| |\\ | _ <| |___| |___ | | | |\\ | |___ | | + |_| |_| \\_|_| \\_\_____|_____| |_| |_| \\_|_____| |_| {Style.RESET_ALL} -{Fore.GREEN} Toolkit de Pentest em Python - Manus AI{Style.RESET_ALL} +{Fore.GREEN} Toolkit de Pentest em Python - Paulus Mass{Style.RESET_ALL} """ print(banner) -def display_results(title, data): + +def display_results(title: str, data): """Exibe os resultados de forma formatada.""" print(f"\n{Fore.BLUE}--- {title} ---{Style.RESET_ALL}") + if data is None: + print(f"{Fore.YELLOW}Nenhum resultado encontrado ou erro na operação.{Style.RESET_ALL}") + return + if isinstance(data, list): if not data: print(f"{Fore.YELLOW}Nenhum resultado encontrado.{Style.RESET_ALL}") @@ -64,14 +70,17 @@ def display_results(title, data): elif data is not None: # Exibição de Whois/DNS/Hash - if isinstance(data, whois.parser.WhoisEntry): - print(f"{Fore.MAGENTA}Registrante:{Style.RESET_ALL} {data.registrar}") - print(f"{Fore.MAGENTA}Data de Criação:{Style.RESET_ALL} {data.creation_date}") - print(f"{Fore.MAGENTA}Data de Expiração:{Style.RESET_ALL} {data.expiration_date}") - print(f"{Fore.MAGENTA}Servidores de Nome:{Style.RESET_ALL} {', '.join(data.name_servers) if data.name_servers else 'N/A'}") - # print(str(data)) # Para ver todos os dados brutos + if isinstance(data, dict): + # Whois result (ipwhois) + for key, value in data.items(): + if isinstance(value, dict): + print(f"{Fore.MAGENTA}{key}:{Style.RESET_ALL}") + for subkey, subvalue in value.items(): + print(f" {subkey}: {subvalue}") + else: + print(f"{Fore.MAGENTA}{key}:{Style.RESET_ALL} {value}") else: - print(data) + print(data) else: print(f"{Fore.YELLOW}A operação não retornou dados ou falhou.{Style.RESET_ALL}") diff --git a/network_scanner.py b/network_scanner.py index 9e7496d..dd2b3ae 100644 --- a/network_scanner.py +++ b/network_scanner.py @@ -1,13 +1,55 @@ import nmap import socket +import re +import ipaddress from scapy.all import ARP, Ether, srp from colorama import Fore, Style +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError -def arp_scan(ip_range): + +def validate_ip_or_domain(target: str) -> bool: + """ + Valida IP ou domínio (sem caracteres especiais ou injeção de comando). + """ + ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$' + domain_pattern = r'^[a-zA-Z0-9\-\.]+$' + return bool(re.match(ip_pattern, target) or re.match(domain_pattern, target)) + + +def validate_cidr(cidr: str) -> bool: + """ + Valida notação CIDR (ex: 192.168.1.0/24). + """ + try: + ipaddress.IPv4Network(cidr, strict=False) + return True + except ValueError: + return False + + +def safe_error_handler(error: Exception, context: str) -> None: + """ + Trata erros sem expor detalhes sensíveis. + """ + log_error = f"[ERROR] {context}: {str(error)}" + print(f"{Fore.RED}{log_error}{Style.RESET_ALL}") + # Log para arquivo (opcional) + try: + with open("errors.log", "a") as f: + f.write(f"{log_error}\n") + except: + pass # Ignorar falha no logging + + +def arp_scan(ip_range: str): """ Realiza um ARP Scan para descobrir hosts ativos na rede local. Requer privilégios de root (sudo) para funcionar corretamente. """ + if not validate_cidr(ip_range): + safe_error_handler(ValueError(f"Invalid CIDR: {ip_range}"), "ARP Scan") + return [] + print(f"{Fore.CYAN}Iniciando ARP Scan em {ip_range}...{Style.RESET_ALL}") try: # Cria o pacote ARP @@ -28,21 +70,28 @@ def arp_scan(ip_range): return hosts_list except PermissionError: - print(f"{Fore.RED}Erro: Permissão negada. O ARP Scan requer privilégios de root (sudo).{Style.RESET_ALL}") + safe_error_handler(PermissionError("Permissão negada"), "ARP Scan") return [] except Exception as e: - print(f"{Fore.RED}Ocorreu um erro durante o ARP Scan: {e}{Style.RESET_ALL}") + safe_error_handler(e, "ARP Scan") return [] -def port_scan(target_ip, ports='1-1024'): + +def port_scan(target_ip: str, ports: str = '1-1024'): """ Realiza um Port Scan TCP usando nmap. """ + if not validate_ip_or_domain(target_ip): + safe_error_handler(ValueError(f"Invalid target: {target_ip}"), "Port Scan") + return {} + print(f"{Fore.CYAN}Iniciando Port Scan em {target_ip} (Portas: {ports})...{Style.RESET_ALL}") try: nm = nmap.PortScanner() # Argumentos: -sV para detecção de versão, -T4 para velocidade, -p para portas - nm.scan(target_ip, ports, arguments='-sV -T4') + # Usar lista de portas para evitar injeção + port_list = ports.replace('-', ',') # Converter 1-1024 para 1,2,3,...,1024 + nm.scan(target_ip, ports=port_list, arguments='-sV -T4') scan_result = {} for host in nm.all_hosts(): @@ -63,15 +112,16 @@ def port_scan(target_ip, ports='1-1024'): return scan_result except nmap.PortScannerError as e: - print(f"{Fore.RED}Erro no Nmap: {e}{Style.RESET_ALL}") + safe_error_handler(e, "Port Scan (Nmap)") return {} except socket.gaierror: - print(f"{Fore.RED}Erro: Não foi possível resolver o nome do host/IP: {target_ip}{Style.RESET_ALL}") + safe_error_handler(socket.gaierror(f"Não foi possível resolver: {target_ip}"), "Port Scan") return {} except Exception as e: - print(f"{Fore.RED}Ocorreu um erro durante o Port Scan: {e}{Style.RESET_ALL}") + safe_error_handler(e, "Port Scan") return {} + if __name__ == '__main__': # Exemplo de uso (requer sudo para arp_scan) # hosts = arp_scan("192.168.1.1/24") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6c54bbe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[project] +name = "pentest-toolkit" +version = "1.0.0" +description = "Ferramenta de Pentest em Python com ARP Scan, Port Scan, Sniffer e Análise de Informações" +authors = [{name = "PaulMass", email = "paulomassanori@gmail.com"}] +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Security", + "Topic :: System :: Networking", +] +dependencies = [ + "scapy>=2.5.0", + "python-nmap>=0.7.0", + "requests>=2.31.0", + "ipwhois>=1.2.0", + "colorama>=0.4.6", +] + +[project.scripts] +pentest = "main:main" + +[project.urls] +Homepage = "https://github.com/PaulMass/python-pentest-toolkit" +Repository = "https://github.com/PaulMass/python-pentest-toolkit" + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["."] +include = ["*"] + +[tool.black] +line-length = 88 +target-version = ["py310", "py311", "py312"] + +[tool.isort] +profile = "black" +line_length = 88 + +[tool.bandit] +exclude_dirs = ["tests", "venv"] +skips = [] diff --git a/requirements.txt b/requirements.txt index d196799..6aea5ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -scapy -python-nmap -requests -whois -colorama +scapy>=2.5.0 +python-nmap>=0.7.0 +requests>=2.31.0 +ipwhois>=1.2.0 +colorama>=0.4.6 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4ea6f01 --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Setup script for pentest-toolkit.""" + +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name="pentest-toolkit", + version="1.0.0", + author="PaulMass", + author_email="paulomassanori@gmail.com", + description="Ferramenta de Pentest em Python com ARP Scan, Port Scan, Sniffer e Análise de Informações", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/PaulMass/python-pentest-toolkit", + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Topic :: Security", + "Topic :: System :: Networking", + ], + python_requires=">=3.10", + install_requires=[ + "scapy>=2.5.0", + "python-nmap>=0.7.0", + "requests>=2.31.0", + "ipwhois>=1.2.0", + "colorama>=0.4.6", + ], + entry_points={ + "console_scripts": [ + "pentest=main:main", + ], + }, +) diff --git a/sniffer.py b/sniffer.py index ae9cf83..9e18464 100644 --- a/sniffer.py +++ b/sniffer.py @@ -1,7 +1,32 @@ -from scapy.all import sniff, IP, TCP, UDP, ICMP, Raw +from scapy.all import sniff, IP, TCP, UDP, ICMP, Raw, Ether from colorama import Fore, Style +import re import time +# Padrões para dados sensíveis que devem ser ocultados +SENSITIVE_PATTERNS = [ + r'password=[^&\s]+', + r'token=[^&\s]+', + r'api[_-]?key=[^&\s]+', + r'cookie:[^;\s]+', + r'authorization:[^\s]+', + r'bearer\s+[a-zA-Z0-9\-_.]+', + r'secret=[^&\s]+', + r'access[_-]?token=[^&\s]+', + r'refresh[_-]?token=[^&\s]+', + r'client[_-]?secret=[^&\s]+', +] + + +def sanitize_packet_data(data: str) -> str: + """ + Remove dados sensíveis dos pacotes antes de exibi-los. + """ + for pattern in SENSITIVE_PATTERNS: + data = re.sub(pattern, "[REDACTED]", data, flags=re.IGNORECASE) + return data + + def packet_callback(packet): """ Função de callback para processar cada pacote capturado. @@ -29,17 +54,21 @@ def packet_callback(packet): icmp_layer = packet[ICMP] print(f" {Fore.GREEN}ICMP:{Style.RESET_ALL} Tipo: {icmp_layer.type}, Código: {icmp_layer.code}") - # Dados Brutos + # Dados Brutos (SANITIZADOS) if Raw in packet: raw_data = packet[Raw].load try: # Tenta decodificar como texto (útil para HTTP) data_str = raw_data.decode('utf-8', errors='ignore') + # Sanitizar dados sensíveis + sanitized = sanitize_packet_data(data_str) # Limita a exibição para não poluir - print(f" {Fore.RED}Dados:{Style.RESET_ALL} {data_str[:50]}...") + print(f" {Fore.RED}Dados:{Style.RESET_ALL} {sanitized[:100]}...") except: - # Se não for texto, exibe em hexadecimal - print(f" {Fore.RED}Dados:{Style.RESET_ALL} {raw_data[:20].hex()}...") + # Se não for texto, exibe em hexadecimal (limitado) + hex_data = raw_data[:20].hex() + print(f" {Fore.RED}Dados (hex):{Style.RESET_ALL} {hex_data}...") + def start_sniffer(interface=None, count=0, timeout=None): """ @@ -57,13 +86,14 @@ def start_sniffer(interface=None, count=0, timeout=None): try: # Filtro: 'ip' para capturar apenas pacotes IP (exclui ARP, etc. a menos que especificado) # store=0 para não armazenar pacotes na memória (melhor para longas sessões) - sniff(iface=interface, prn=packet_callback, count=count, timeout=timeout, store=0) + sniff(iface=interface, prn=packet_callback, count=count, timeout=timeout, store=0, filter="ip") print(f"\n{Fore.GREEN}Sniffer de Pacotes concluído.{Style.RESET_ALL}") except PermissionError: print(f"\n{Fore.RED}Erro: Permissão negada. O sniffer requer privilégios de root (sudo) e pode precisar da interface correta.{Style.RESET_ALL}") except Exception as e: print(f"\n{Fore.RED}Ocorreu um erro durante a captura de pacotes: {e}{Style.RESET_ALL}") + if __name__ == '__main__': # Exemplo de uso (requer sudo) # start_sniffer(interface="eth0", count=10)