Full security platform with web dashboard, 16 Flask blueprints, 26 modules, autonomous AI agent, WebUSB hardware support, and Archon Android companion app. Includes Hash Toolkit, debug console, anti-stalkerware shield, Metasploit/RouterSploit integration, WireGuard VPN, OSINT reconnaissance, and multi-backend LLM support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1454 lines
60 KiB
Python
1454 lines
60 KiB
Python
"""
|
|
AUTARCH Agent Hal Module
|
|
AI-powered security automation for defense and penetration testing
|
|
|
|
Uses LLM integration for intelligent security operations including:
|
|
- MITM attack detection and monitoring
|
|
- Automated Metasploit module execution via natural language
|
|
- Network security analysis
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import re
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
# Module metadata
|
|
NAME = "Agent Hal"
|
|
DESCRIPTION = "AI-powered security automation"
|
|
AUTHOR = "darkHal Security Group"
|
|
VERSION = "2.0"
|
|
CATEGORY = "core"
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
from core.banner import Colors, clear_screen, display_banner
|
|
from core.config import get_config
|
|
from core.llm import LLM, LLMError
|
|
from core.pentest_tree import PentestTree, NodeStatus, PTTNodeType
|
|
from core.pentest_pipeline import PentestPipeline, detect_source_type
|
|
from core.pentest_session import PentestSession, PentestSessionState
|
|
|
|
|
|
class AgentHal:
|
|
"""AI-powered security automation agent."""
|
|
|
|
def __init__(self):
|
|
self.config = get_config()
|
|
self.llm = None
|
|
self.msf = None
|
|
self.msf_connected = False
|
|
self.pentest_session = None
|
|
self.pentest_pipeline = None
|
|
|
|
def print_status(self, message: str, status: str = "info"):
|
|
colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
|
|
symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
|
|
print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")
|
|
|
|
def run_cmd(self, cmd: str, timeout: int = 30) -> Tuple[bool, str]:
|
|
"""Run a shell command and return output."""
|
|
try:
|
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
return result.returncode == 0, result.stdout.strip()
|
|
except subprocess.TimeoutExpired:
|
|
return False, "Command timed out"
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
def _ensure_llm_loaded(self) -> bool:
|
|
"""Ensure LLM is loaded and ready."""
|
|
if self.llm is None:
|
|
self.llm = LLM()
|
|
|
|
if not self.llm.is_loaded:
|
|
self.print_status("Loading LLM model...", "info")
|
|
try:
|
|
self.llm.load_model(verbose=True)
|
|
return True
|
|
except LLMError as e:
|
|
self.print_status(f"Failed to load LLM: {e}", "error")
|
|
return False
|
|
return True
|
|
|
|
def _ensure_msf_connected(self) -> bool:
|
|
"""Ensure MSF RPC is connected via the centralized interface."""
|
|
if self.msf is None:
|
|
try:
|
|
from core.msf_interface import get_msf_interface
|
|
self.msf = get_msf_interface()
|
|
except ImportError:
|
|
self.print_status("MSF interface not available", "error")
|
|
return False
|
|
|
|
# Use the interface's connection management
|
|
connected, msg = self.msf.ensure_connected(auto_prompt=False)
|
|
if connected:
|
|
self.msf_connected = True
|
|
self.print_status("Connected to MSF RPC", "success")
|
|
return True
|
|
else:
|
|
self.print_status(f"Failed to connect to MSF: {msg}", "error")
|
|
return False
|
|
|
|
# ==================== MITM DETECTION ====================
|
|
|
|
def mitm_detection_menu(self):
|
|
"""MITM attack detection submenu."""
|
|
while True:
|
|
clear_screen()
|
|
display_banner()
|
|
|
|
print(f"{Colors.RED}{Colors.BOLD} MITM Detection{Colors.RESET}")
|
|
print(f"{Colors.DIM} Detect Man-in-the-Middle attacks on your network{Colors.RESET}")
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
|
print()
|
|
|
|
print(f" {Colors.GREEN}[1]{Colors.RESET} Full MITM Scan (All Checks)")
|
|
print(f" {Colors.GREEN}[2]{Colors.RESET} ARP Spoofing Detection")
|
|
print(f" {Colors.GREEN}[3]{Colors.RESET} DNS Spoofing Detection")
|
|
print(f" {Colors.GREEN}[4]{Colors.RESET} SSL/TLS Stripping Detection")
|
|
print(f" {Colors.GREEN}[5]{Colors.RESET} Rogue DHCP Detection")
|
|
print(f" {Colors.GREEN}[6]{Colors.RESET} Gateway Anomaly Check")
|
|
print()
|
|
print(f" {Colors.CYAN}[7]{Colors.RESET} Continuous Monitoring Mode")
|
|
print()
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
|
print()
|
|
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
|
|
|
|
if choice == "0":
|
|
break
|
|
elif choice == "1":
|
|
self.full_mitm_scan()
|
|
elif choice == "2":
|
|
self.detect_arp_spoofing()
|
|
elif choice == "3":
|
|
self.detect_dns_spoofing()
|
|
elif choice == "4":
|
|
self.detect_ssl_stripping()
|
|
elif choice == "5":
|
|
self.detect_rogue_dhcp()
|
|
elif choice == "6":
|
|
self.check_gateway_anomaly()
|
|
elif choice == "7":
|
|
self.continuous_monitoring()
|
|
|
|
if choice in ["1", "2", "3", "4", "5", "6"]:
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def full_mitm_scan(self):
|
|
"""Run all MITM detection checks."""
|
|
print(f"\n{Colors.BOLD}Full MITM Scan{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
issues = []
|
|
|
|
# ARP Spoofing
|
|
print(f"{Colors.CYAN}[1/5] Checking for ARP spoofing...{Colors.RESET}")
|
|
arp_issues = self._check_arp_spoofing()
|
|
issues.extend(arp_issues)
|
|
|
|
# DNS Spoofing
|
|
print(f"{Colors.CYAN}[2/5] Checking for DNS spoofing...{Colors.RESET}")
|
|
dns_issues = self._check_dns_spoofing()
|
|
issues.extend(dns_issues)
|
|
|
|
# SSL Stripping
|
|
print(f"{Colors.CYAN}[3/5] Checking for SSL stripping indicators...{Colors.RESET}")
|
|
ssl_issues = self._check_ssl_stripping()
|
|
issues.extend(ssl_issues)
|
|
|
|
# Rogue DHCP
|
|
print(f"{Colors.CYAN}[4/5] Checking for rogue DHCP servers...{Colors.RESET}")
|
|
dhcp_issues = self._check_rogue_dhcp()
|
|
issues.extend(dhcp_issues)
|
|
|
|
# Gateway Anomaly
|
|
print(f"{Colors.CYAN}[5/5] Checking gateway for anomalies...{Colors.RESET}")
|
|
gw_issues = self._check_gateway()
|
|
issues.extend(gw_issues)
|
|
|
|
# Results
|
|
print(f"\n{Colors.CYAN}{'─' * 50}{Colors.RESET}")
|
|
print(f"{Colors.BOLD}Scan Results{Colors.RESET}\n")
|
|
|
|
if issues:
|
|
print(f"{Colors.RED}[!] Found {len(issues)} potential issue(s):{Colors.RESET}\n")
|
|
for issue in issues:
|
|
severity_color = Colors.RED if issue['severity'] == 'HIGH' else Colors.YELLOW
|
|
print(f" {severity_color}[{issue['severity']}]{Colors.RESET} {issue['type']}")
|
|
print(f" {issue['description']}")
|
|
if issue.get('details'):
|
|
print(f" {Colors.DIM}{issue['details']}{Colors.RESET}")
|
|
print()
|
|
else:
|
|
print(f"{Colors.GREEN}[+] No MITM indicators detected{Colors.RESET}")
|
|
print(f"{Colors.DIM} Network appears clean{Colors.RESET}")
|
|
|
|
def _check_arp_spoofing(self) -> List[Dict]:
|
|
"""Check for ARP spoofing indicators."""
|
|
issues = []
|
|
|
|
# Get ARP table
|
|
success, output = self.run_cmd("arp -a")
|
|
if not success:
|
|
success, output = self.run_cmd("ip neigh show")
|
|
|
|
if success and output:
|
|
# Parse ARP entries
|
|
mac_to_ips = {}
|
|
lines = output.split('\n')
|
|
|
|
for line in lines:
|
|
# Extract MAC and IP
|
|
mac_match = re.search(r'([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}', line)
|
|
ip_match = re.search(r'(\d{1,3}\.){3}\d{1,3}', line)
|
|
|
|
if mac_match and ip_match:
|
|
mac = mac_match.group().lower()
|
|
ip = ip_match.group()
|
|
|
|
if mac not in mac_to_ips:
|
|
mac_to_ips[mac] = []
|
|
mac_to_ips[mac].append(ip)
|
|
|
|
# Check for duplicate MACs (potential ARP spoofing)
|
|
for mac, ips in mac_to_ips.items():
|
|
if len(ips) > 1:
|
|
issues.append({
|
|
'type': 'ARP Spoofing Detected',
|
|
'severity': 'HIGH',
|
|
'description': f'Multiple IPs share same MAC address',
|
|
'details': f'MAC {mac} -> IPs: {", ".join(ips)}'
|
|
})
|
|
|
|
return issues
|
|
|
|
def detect_arp_spoofing(self):
|
|
"""Detailed ARP spoofing detection."""
|
|
print(f"\n{Colors.BOLD}ARP Spoofing Detection{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
# Get ARP table
|
|
print(f"{Colors.CYAN}[*] Fetching ARP table...{Colors.RESET}")
|
|
success, output = self.run_cmd("arp -a")
|
|
if not success:
|
|
success, output = self.run_cmd("ip neigh show")
|
|
|
|
if success and output:
|
|
print(f"\n{Colors.CYAN}Current ARP Table:{Colors.RESET}")
|
|
print(output)
|
|
|
|
issues = self._check_arp_spoofing()
|
|
|
|
if issues:
|
|
print(f"\n{Colors.RED}[!] ARP Spoofing Indicators Found:{Colors.RESET}")
|
|
for issue in issues:
|
|
print(f" {issue['description']}")
|
|
print(f" {Colors.DIM}{issue['details']}{Colors.RESET}")
|
|
else:
|
|
print(f"\n{Colors.GREEN}[+] No ARP spoofing detected{Colors.RESET}")
|
|
|
|
# Get gateway MAC
|
|
print(f"\n{Colors.CYAN}[*] Checking gateway MAC...{Colors.RESET}")
|
|
success, gw = self.run_cmd("ip route | grep default | awk '{print $3}'")
|
|
if success and gw:
|
|
print(f" Gateway IP: {gw}")
|
|
success, gw_mac = self.run_cmd(f"arp -n {gw} | grep -v Address | awk '{{print $3}}'")
|
|
if success and gw_mac:
|
|
print(f" Gateway MAC: {gw_mac}")
|
|
|
|
def _check_dns_spoofing(self) -> List[Dict]:
|
|
"""Check for DNS spoofing indicators."""
|
|
issues = []
|
|
|
|
# Check resolv.conf for suspicious DNS
|
|
success, output = self.run_cmd("cat /etc/resolv.conf")
|
|
if success:
|
|
dns_servers = re.findall(r'nameserver\s+(\S+)', output)
|
|
|
|
# Known safe DNS servers
|
|
safe_dns = ['8.8.8.8', '8.8.4.4', '1.1.1.1', '1.0.0.1', '9.9.9.9', '208.67.222.222']
|
|
|
|
for dns in dns_servers:
|
|
if dns.startswith('127.') or dns.startswith('192.168.') or dns.startswith('10.'):
|
|
# Local DNS - could be legitimate or malicious
|
|
pass
|
|
elif dns not in safe_dns:
|
|
issues.append({
|
|
'type': 'Suspicious DNS Server',
|
|
'severity': 'MEDIUM',
|
|
'description': f'Unknown DNS server configured',
|
|
'details': f'DNS: {dns}'
|
|
})
|
|
|
|
# Test DNS resolution consistency
|
|
test_domains = ['google.com', 'cloudflare.com']
|
|
for domain in test_domains:
|
|
success1, ip1 = self.run_cmd(f"dig +short {domain} @8.8.8.8 | head -1")
|
|
success2, ip2 = self.run_cmd(f"dig +short {domain} | head -1")
|
|
|
|
if success1 and success2 and ip1 and ip2:
|
|
if ip1 != ip2:
|
|
issues.append({
|
|
'type': 'DNS Resolution Mismatch',
|
|
'severity': 'HIGH',
|
|
'description': f'DNS returns different IP than Google DNS',
|
|
'details': f'{domain}: Local={ip2}, Google={ip1}'
|
|
})
|
|
|
|
return issues
|
|
|
|
def detect_dns_spoofing(self):
|
|
"""Detailed DNS spoofing detection."""
|
|
print(f"\n{Colors.BOLD}DNS Spoofing Detection{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
# Show current DNS config
|
|
print(f"{Colors.CYAN}[*] Current DNS Configuration:{Colors.RESET}")
|
|
success, output = self.run_cmd("cat /etc/resolv.conf | grep -v '^#'")
|
|
if success:
|
|
print(output)
|
|
|
|
# Test DNS resolution
|
|
print(f"\n{Colors.CYAN}[*] Testing DNS Resolution Consistency:{Colors.RESET}")
|
|
test_domains = ['google.com', 'cloudflare.com', 'microsoft.com']
|
|
|
|
for domain in test_domains:
|
|
success1, ip1 = self.run_cmd(f"dig +short {domain} @8.8.8.8 | head -1")
|
|
success2, ip2 = self.run_cmd(f"dig +short {domain} | head -1")
|
|
|
|
if success1 and success2:
|
|
match = "MATCH" if ip1 == ip2 else "MISMATCH"
|
|
color = Colors.GREEN if ip1 == ip2 else Colors.RED
|
|
print(f" {domain}:")
|
|
print(f" Local DNS: {ip2}")
|
|
print(f" Google DNS: {ip1}")
|
|
print(f" Status: {color}{match}{Colors.RESET}")
|
|
|
|
issues = self._check_dns_spoofing()
|
|
if issues:
|
|
print(f"\n{Colors.RED}[!] DNS Issues Found:{Colors.RESET}")
|
|
for issue in issues:
|
|
print(f" {issue['description']}: {issue['details']}")
|
|
|
|
def _check_ssl_stripping(self) -> List[Dict]:
|
|
"""Check for SSL stripping indicators."""
|
|
issues = []
|
|
|
|
# Check if HSTS is being honored
|
|
test_sites = ['https://www.google.com', 'https://www.cloudflare.com']
|
|
|
|
for site in test_sites:
|
|
success, output = self.run_cmd(f"curl -sI -m 5 {site} | head -1")
|
|
if success:
|
|
if 'HTTP/1.1 200' not in output and 'HTTP/2' not in output:
|
|
issues.append({
|
|
'type': 'HTTPS Connection Issue',
|
|
'severity': 'MEDIUM',
|
|
'description': f'Unexpected response from HTTPS site',
|
|
'details': f'{site}: {output}'
|
|
})
|
|
|
|
# Check for SSL certificate issues
|
|
success, output = self.run_cmd("curl -sI -m 5 https://www.google.com 2>&1 | grep -i 'certificate'")
|
|
if success and 'certificate' in output.lower():
|
|
issues.append({
|
|
'type': 'SSL Certificate Warning',
|
|
'severity': 'HIGH',
|
|
'description': 'SSL certificate issues detected',
|
|
'details': output
|
|
})
|
|
|
|
return issues
|
|
|
|
def detect_ssl_stripping(self):
|
|
"""Detailed SSL stripping detection."""
|
|
print(f"\n{Colors.BOLD}SSL/TLS Stripping Detection{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
test_sites = [
|
|
('https://www.google.com', 'Google'),
|
|
('https://www.cloudflare.com', 'Cloudflare'),
|
|
('https://www.github.com', 'GitHub'),
|
|
]
|
|
|
|
print(f"{Colors.CYAN}[*] Testing HTTPS Connections:{Colors.RESET}\n")
|
|
|
|
for url, name in test_sites:
|
|
print(f" Testing {name}...")
|
|
|
|
# Check HTTP redirect
|
|
http_url = url.replace('https://', 'http://')
|
|
success, output = self.run_cmd(f"curl -sI -m 5 -o /dev/null -w '%{{http_code}} %{{redirect_url}}' {http_url}")
|
|
|
|
if success:
|
|
parts = output.split()
|
|
code = parts[0] if parts else "000"
|
|
redirect = parts[1] if len(parts) > 1 else ""
|
|
|
|
if code in ['301', '302', '307', '308'] and redirect.startswith('https://'):
|
|
print(f" {Colors.GREEN}[+] HTTP->HTTPS redirect working{Colors.RESET}")
|
|
else:
|
|
print(f" {Colors.YELLOW}[!] No HTTPS redirect (Code: {code}){Colors.RESET}")
|
|
|
|
# Check HTTPS directly
|
|
success, output = self.run_cmd(f"curl -sI -m 5 {url} 2>&1 | head -1")
|
|
if success and ('200' in output or 'HTTP/2' in output):
|
|
print(f" {Colors.GREEN}[+] HTTPS connection successful{Colors.RESET}")
|
|
else:
|
|
print(f" {Colors.RED}[!] HTTPS connection failed: {output}{Colors.RESET}")
|
|
|
|
# Check certificate
|
|
domain = url.replace('https://', '').replace('/', '')
|
|
success, cert_info = self.run_cmd(f"echo | openssl s_client -connect {domain}:443 -servername {domain} 2>/dev/null | openssl x509 -noout -dates 2>/dev/null")
|
|
if success and cert_info:
|
|
print(f" {Colors.GREEN}[+] Valid SSL certificate{Colors.RESET}")
|
|
print(f" {Colors.DIM}{cert_info.replace(chr(10), ' | ')}{Colors.RESET}")
|
|
print()
|
|
|
|
def _check_rogue_dhcp(self) -> List[Dict]:
|
|
"""Check for rogue DHCP servers."""
|
|
issues = []
|
|
|
|
# Get current DHCP server
|
|
success, output = self.run_cmd("cat /var/lib/dhcp/dhclient.leases 2>/dev/null | grep 'dhcp-server-identifier' | tail -1")
|
|
if not success:
|
|
success, output = self.run_cmd("journalctl -u NetworkManager --no-pager -n 50 2>/dev/null | grep -i 'dhcp' | grep -i 'server'")
|
|
|
|
# This is a basic check - full rogue DHCP detection requires nmap or specialized tools
|
|
return issues
|
|
|
|
def detect_rogue_dhcp(self):
|
|
"""Detect rogue DHCP servers."""
|
|
print(f"\n{Colors.BOLD}Rogue DHCP Detection{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
print(f"{Colors.CYAN}[*] Current DHCP Information:{Colors.RESET}")
|
|
|
|
# Check DHCP leases
|
|
success, output = self.run_cmd("cat /var/lib/dhcp/dhclient.leases 2>/dev/null | tail -30")
|
|
if success and output:
|
|
print(output)
|
|
else:
|
|
# Try NetworkManager
|
|
success, output = self.run_cmd("nmcli device show | grep -i 'dhcp\\|gateway\\|dns'")
|
|
if success:
|
|
print(output)
|
|
|
|
print(f"\n{Colors.YELLOW}[!] Note: Full rogue DHCP detection requires nmap:{Colors.RESET}")
|
|
print(f" {Colors.DIM}nmap --script broadcast-dhcp-discover{Colors.RESET}")
|
|
|
|
# Offer to run nmap scan
|
|
run_nmap = input(f"\n{Colors.WHITE}Run nmap DHCP discovery? (y/n): {Colors.RESET}").strip().lower()
|
|
if run_nmap == 'y':
|
|
print(f"\n{Colors.CYAN}[*] Running DHCP discovery...{Colors.RESET}")
|
|
success, output = self.run_cmd("nmap --script broadcast-dhcp-discover 2>/dev/null", timeout=60)
|
|
if success:
|
|
print(output)
|
|
else:
|
|
self.print_status("nmap not available or scan failed", "warning")
|
|
|
|
def _check_gateway(self) -> List[Dict]:
|
|
"""Check gateway for anomalies."""
|
|
issues = []
|
|
|
|
# Get default gateway
|
|
success, gateway = self.run_cmd("ip route | grep default | awk '{print $3}'")
|
|
if success and gateway:
|
|
# Ping gateway
|
|
success, output = self.run_cmd(f"ping -c 1 -W 2 {gateway}")
|
|
if not success:
|
|
issues.append({
|
|
'type': 'Gateway Unreachable',
|
|
'severity': 'HIGH',
|
|
'description': 'Default gateway is not responding',
|
|
'details': f'Gateway: {gateway}'
|
|
})
|
|
|
|
# Check gateway MAC consistency
|
|
success, mac = self.run_cmd(f"arp -n {gateway} | grep -v Address | awk '{{print $3}}'")
|
|
if success and mac:
|
|
# Check if MAC is from a known vendor (basic check)
|
|
if mac.startswith('00:00:00') or mac == '(incomplete)':
|
|
issues.append({
|
|
'type': 'Suspicious Gateway MAC',
|
|
'severity': 'MEDIUM',
|
|
'description': 'Gateway MAC address appears suspicious',
|
|
'details': f'Gateway {gateway} has MAC {mac}'
|
|
})
|
|
|
|
return issues
|
|
|
|
def check_gateway_anomaly(self):
|
|
"""Check for gateway anomalies."""
|
|
print(f"\n{Colors.BOLD}Gateway Anomaly Check{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
# Get gateway info
|
|
success, gateway = self.run_cmd("ip route | grep default | awk '{print $3}'")
|
|
if success and gateway:
|
|
print(f" Default Gateway: {gateway}")
|
|
|
|
# Get MAC
|
|
success, mac = self.run_cmd(f"arp -n {gateway} | grep -v Address | awk '{{print $3}}'")
|
|
if success and mac:
|
|
print(f" Gateway MAC: {mac}")
|
|
|
|
# Ping test
|
|
success, output = self.run_cmd(f"ping -c 3 -W 2 {gateway}")
|
|
if success:
|
|
print(f" {Colors.GREEN}[+] Gateway is reachable{Colors.RESET}")
|
|
# Extract latency
|
|
latency = re.search(r'rtt min/avg/max/mdev = ([\d.]+)/([\d.]+)', output)
|
|
if latency:
|
|
print(f" Latency: {latency.group(2)}ms avg")
|
|
else:
|
|
print(f" {Colors.RED}[!] Gateway is NOT reachable{Colors.RESET}")
|
|
|
|
# Traceroute
|
|
print(f"\n{Colors.CYAN}[*] Route to Internet:{Colors.RESET}")
|
|
success, output = self.run_cmd("traceroute -m 5 8.8.8.8 2>/dev/null", timeout=30)
|
|
if success:
|
|
print(output)
|
|
else:
|
|
print(f" {Colors.RED}[!] Could not determine default gateway{Colors.RESET}")
|
|
|
|
def continuous_monitoring(self):
|
|
"""Continuous MITM monitoring mode."""
|
|
print(f"\n{Colors.BOLD}Continuous MITM Monitoring{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
|
|
print(f"{Colors.DIM}Press Ctrl+C to stop monitoring{Colors.RESET}\n")
|
|
|
|
# Store baseline
|
|
success, baseline_arp = self.run_cmd("arp -a")
|
|
baseline_macs = {}
|
|
if success:
|
|
for line in baseline_arp.split('\n'):
|
|
mac_match = re.search(r'([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}', line)
|
|
ip_match = re.search(r'(\d{1,3}\.){3}\d{1,3}', line)
|
|
if mac_match and ip_match:
|
|
baseline_macs[ip_match.group()] = mac_match.group().lower()
|
|
|
|
print(f"{Colors.GREEN}[+] Baseline captured: {len(baseline_macs)} hosts{Colors.RESET}\n")
|
|
|
|
try:
|
|
check_count = 0
|
|
while True:
|
|
check_count += 1
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
|
|
# Get current ARP table
|
|
success, current_arp = self.run_cmd("arp -a")
|
|
if success:
|
|
current_macs = {}
|
|
for line in current_arp.split('\n'):
|
|
mac_match = re.search(r'([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}', line)
|
|
ip_match = re.search(r'(\d{1,3}\.){3}\d{1,3}', line)
|
|
if mac_match and ip_match:
|
|
current_macs[ip_match.group()] = mac_match.group().lower()
|
|
|
|
# Compare with baseline
|
|
for ip, mac in current_macs.items():
|
|
if ip in baseline_macs and baseline_macs[ip] != mac:
|
|
print(f"{Colors.RED}[{timestamp}] ALERT: MAC change detected!{Colors.RESET}")
|
|
print(f" IP: {ip}")
|
|
print(f" Old MAC: {baseline_macs[ip]}")
|
|
print(f" New MAC: {mac}")
|
|
print()
|
|
|
|
# Check for new hosts
|
|
new_hosts = set(current_macs.keys()) - set(baseline_macs.keys())
|
|
for ip in new_hosts:
|
|
print(f"{Colors.YELLOW}[{timestamp}] New host detected: {ip} ({current_macs[ip]}){Colors.RESET}")
|
|
|
|
print(f"\r{Colors.DIM}[{timestamp}] Check #{check_count} - {len(current_macs)} hosts{Colors.RESET}", end='', flush=True)
|
|
time.sleep(5)
|
|
|
|
except KeyboardInterrupt:
|
|
print(f"\n\n{Colors.CYAN}[*] Monitoring stopped{Colors.RESET}")
|
|
|
|
# ==================== MSF AUTOMATION ====================
|
|
|
|
def msf_automation_menu(self):
|
|
"""LLM-driven Metasploit automation menu."""
|
|
while True:
|
|
clear_screen()
|
|
display_banner()
|
|
|
|
print(f"{Colors.RED}{Colors.BOLD} MSF Automation (AI-Powered){Colors.RESET}")
|
|
print(f"{Colors.DIM} Use natural language to run Metasploit modules{Colors.RESET}")
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
|
print()
|
|
|
|
# Status
|
|
llm_status = f"{Colors.GREEN}Loaded{Colors.RESET}" if (self.llm and self.llm.is_loaded) else f"{Colors.RED}Not loaded{Colors.RESET}"
|
|
msf_status = f"{Colors.GREEN}Connected{Colors.RESET}" if self.msf_connected else f"{Colors.RED}Not connected{Colors.RESET}"
|
|
print(f" {Colors.DIM}LLM: {llm_status} | MSF: {msf_status}{Colors.RESET}")
|
|
print()
|
|
|
|
print(f" {Colors.GREEN}[1]{Colors.RESET} Describe What You Want To Do")
|
|
print(f" {Colors.GREEN}[2]{Colors.RESET} Quick Scan Target")
|
|
print(f" {Colors.GREEN}[3]{Colors.RESET} Exploit Suggester")
|
|
print(f" {Colors.GREEN}[4]{Colors.RESET} Post-Exploitation Helper")
|
|
print()
|
|
print(f" {Colors.CYAN}[C]{Colors.RESET} Connect to MSF")
|
|
print(f" {Colors.CYAN}[L]{Colors.RESET} Load LLM Model")
|
|
print()
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
|
print()
|
|
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
|
|
|
|
if choice == "0":
|
|
break
|
|
elif choice == "1":
|
|
self.natural_language_msf()
|
|
elif choice == "2":
|
|
self.quick_scan_target()
|
|
elif choice == "3":
|
|
self.exploit_suggester()
|
|
elif choice == "4":
|
|
self.post_exploitation_helper()
|
|
elif choice == "c":
|
|
self._ensure_msf_connected()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
elif choice == "l":
|
|
self._ensure_llm_loaded()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def natural_language_msf(self):
|
|
"""Process natural language requests for MSF operations."""
|
|
print(f"\n{Colors.BOLD}Natural Language MSF Control{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
|
|
print(f"{Colors.DIM}Describe what you want to do in plain English.{Colors.RESET}")
|
|
print(f"{Colors.DIM}Examples:{Colors.RESET}")
|
|
print(f"{Colors.DIM} - 'Scan 192.168.1.1 for open ports'{Colors.RESET}")
|
|
print(f"{Colors.DIM} - 'Check if target is vulnerable to EternalBlue'{Colors.RESET}")
|
|
print(f"{Colors.DIM} - 'Run SMB version scanner on 10.0.0.0/24'{Colors.RESET}")
|
|
print()
|
|
|
|
# Ensure LLM is loaded
|
|
if not self._ensure_llm_loaded():
|
|
return
|
|
|
|
# Ensure MSF is connected
|
|
if not self._ensure_msf_connected():
|
|
print(f"{Colors.YELLOW}[!] MSF not connected. Will show commands but cannot execute.{Colors.RESET}")
|
|
|
|
# Get user request
|
|
request = input(f"{Colors.WHITE}What do you want to do? {Colors.RESET}").strip()
|
|
if not request:
|
|
return
|
|
|
|
print(f"\n{Colors.CYAN}[*] Analyzing request...{Colors.RESET}")
|
|
|
|
# Build prompt for LLM
|
|
system_prompt = """You are a Metasploit expert assistant. Your job is to translate user requests into specific Metasploit module recommendations.
|
|
|
|
When the user describes what they want to do, respond with:
|
|
1. The recommended MSF module path (e.g., auxiliary/scanner/smb/smb_version)
|
|
2. Required options (e.g., RHOSTS, RPORT)
|
|
3. A brief explanation of what the module does
|
|
|
|
Format your response as JSON:
|
|
{
|
|
"module_type": "auxiliary|exploit|post",
|
|
"module_path": "full/module/path",
|
|
"options": {"RHOSTS": "value", "THREADS": "10"},
|
|
"explanation": "Brief description"
|
|
}
|
|
|
|
If you cannot determine an appropriate module, respond with:
|
|
{"error": "reason"}
|
|
|
|
Common modules:
|
|
- Port scan: auxiliary/scanner/portscan/tcp
|
|
- SMB version: auxiliary/scanner/smb/smb_version
|
|
- MS17-010 check: auxiliary/scanner/smb/smb_ms17_010
|
|
- SSH version: auxiliary/scanner/ssh/ssh_version
|
|
- HTTP version: auxiliary/scanner/http/http_version
|
|
- FTP version: auxiliary/scanner/ftp/ftp_version
|
|
- Vuln scan: auxiliary/scanner/smb/smb_ms08_067
|
|
"""
|
|
|
|
try:
|
|
# Clear history for fresh context
|
|
self.llm.clear_history()
|
|
|
|
# Get LLM response
|
|
response = self.llm.chat(request, system_prompt=system_prompt)
|
|
|
|
# Try to parse JSON from response
|
|
try:
|
|
# Find JSON in response
|
|
json_match = re.search(r'\{[^{}]*\}', response, re.DOTALL)
|
|
if json_match:
|
|
module_info = json.loads(json_match.group())
|
|
else:
|
|
module_info = json.loads(response)
|
|
|
|
if 'error' in module_info:
|
|
print(f"\n{Colors.YELLOW}[!] {module_info['error']}{Colors.RESET}")
|
|
return
|
|
|
|
# Display recommendation
|
|
print(f"\n{Colors.GREEN}[+] Recommended Module:{Colors.RESET}")
|
|
print(f" Type: {module_info.get('module_type', 'unknown')}")
|
|
print(f" Path: {module_info.get('module_path', 'unknown')}")
|
|
print(f"\n{Colors.CYAN}Options:{Colors.RESET}")
|
|
for opt, val in module_info.get('options', {}).items():
|
|
print(f" {opt}: {val}")
|
|
print(f"\n{Colors.DIM}Explanation: {module_info.get('explanation', 'N/A')}{Colors.RESET}")
|
|
|
|
# Ask to execute
|
|
if self.msf and self.msf.is_connected:
|
|
execute = input(f"\n{Colors.WHITE}Execute this module? (y/n): {Colors.RESET}").strip().lower()
|
|
if execute == 'y':
|
|
self._execute_msf_module(module_info)
|
|
|
|
except json.JSONDecodeError:
|
|
# LLM didn't return valid JSON, show raw response
|
|
print(f"\n{Colors.CYAN}LLM Response:{Colors.RESET}")
|
|
print(response)
|
|
|
|
except LLMError as e:
|
|
self.print_status(f"LLM error: {e}", "error")
|
|
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def _execute_msf_module(self, module_info: Dict):
|
|
"""Execute an MSF module based on LLM recommendation."""
|
|
try:
|
|
module_type = module_info.get('module_type', 'auxiliary')
|
|
module_path = module_info.get('module_path', '')
|
|
options = module_info.get('options', {})
|
|
|
|
# Ensure full module path format (type/path)
|
|
if not module_path.startswith(module_type + '/'):
|
|
full_path = f"{module_type}/{module_path}"
|
|
else:
|
|
full_path = module_path
|
|
|
|
print(f"\n{Colors.CYAN}[*] Executing {full_path}...{Colors.RESET}")
|
|
|
|
# Use the interface's run_module method
|
|
result = self.msf.run_module(full_path, options)
|
|
|
|
if result.success:
|
|
print(f"{Colors.GREEN}[+] Module executed successfully{Colors.RESET}")
|
|
if result.findings:
|
|
print(f"\n{Colors.CYAN}Findings:{Colors.RESET}")
|
|
for finding in result.findings[:10]:
|
|
print(f" {finding}")
|
|
if result.info:
|
|
for info in result.info[:5]:
|
|
print(f" {Colors.DIM}{info}{Colors.RESET}")
|
|
else:
|
|
print(f"{Colors.YELLOW}[!] {result.get_summary()}{Colors.RESET}")
|
|
|
|
except Exception as e:
|
|
self.print_status(f"Execution failed: {e}", "error")
|
|
|
|
def quick_scan_target(self):
|
|
"""Quick scan a target using MSF."""
|
|
print(f"\n{Colors.BOLD}Quick Target Scan{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
target = input(f"{Colors.WHITE}Enter target (IP or range): {Colors.RESET}").strip()
|
|
if not target:
|
|
return
|
|
|
|
if not self._ensure_msf_connected():
|
|
self.print_status("Cannot scan without MSF connection", "error")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
return
|
|
|
|
print(f"\n{Colors.CYAN}[*] Running quick scan on {target}...{Colors.RESET}\n")
|
|
|
|
# Run common scanners
|
|
scanners = [
|
|
('auxiliary/scanner/portscan/tcp', {'RHOSTS': target, 'PORTS': '21-25,80,443,445,3389,8080'}),
|
|
('auxiliary/scanner/smb/smb_version', {'RHOSTS': target}),
|
|
('auxiliary/scanner/ssh/ssh_version', {'RHOSTS': target}),
|
|
]
|
|
|
|
for module_path, options in scanners:
|
|
try:
|
|
print(f" Running {module_path}...")
|
|
result = self.msf.run_module(module_path, options)
|
|
if result.success:
|
|
print(f" {Colors.GREEN}Completed{Colors.RESET}")
|
|
for finding in result.findings[:3]:
|
|
print(f" {finding}")
|
|
else:
|
|
print(f" {Colors.YELLOW}{result.get_summary()}{Colors.RESET}")
|
|
except Exception as e:
|
|
print(f" {Colors.RED}Failed: {e}{Colors.RESET}")
|
|
|
|
print(f"\n{Colors.GREEN}[+] Quick scan completed.{Colors.RESET}")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def exploit_suggester(self):
|
|
"""Use LLM to suggest exploits based on target info."""
|
|
print(f"\n{Colors.BOLD}Exploit Suggester{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
|
|
print(f"{Colors.DIM}Describe your target and I'll suggest exploits.{Colors.RESET}\n")
|
|
|
|
if not self._ensure_llm_loaded():
|
|
return
|
|
|
|
print(f"{Colors.WHITE}Enter target information:{Colors.RESET}")
|
|
print(f"{Colors.DIM}(OS, services, versions, open ports, etc.){Colors.RESET}")
|
|
target_info = input(f"{Colors.WHITE}> {Colors.RESET}").strip()
|
|
|
|
if not target_info:
|
|
return
|
|
|
|
print(f"\n{Colors.CYAN}[*] Analyzing target...{Colors.RESET}")
|
|
|
|
system_prompt = """You are a penetration testing expert. Based on the target information provided, suggest relevant Metasploit exploits and auxiliary modules.
|
|
|
|
Consider:
|
|
1. Operating system vulnerabilities
|
|
2. Service-specific exploits
|
|
3. Common misconfigurations
|
|
4. Post-exploitation opportunities
|
|
|
|
Format your response as a prioritized list with:
|
|
- Module path
|
|
- CVE (if applicable)
|
|
- Success likelihood (High/Medium/Low)
|
|
- Brief description
|
|
|
|
Focus on practical, commonly successful exploits."""
|
|
|
|
try:
|
|
self.llm.clear_history()
|
|
response = self.llm.chat(f"Target information: {target_info}", system_prompt=system_prompt)
|
|
print(f"\n{Colors.GREEN}Exploit Suggestions:{Colors.RESET}\n")
|
|
print(response)
|
|
except LLMError as e:
|
|
self.print_status(f"LLM error: {e}", "error")
|
|
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def post_exploitation_helper(self):
|
|
"""LLM-assisted post-exploitation guidance."""
|
|
print(f"\n{Colors.BOLD}Post-Exploitation Helper{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
|
|
print(f"{Colors.DIM}Get guidance on post-exploitation steps.{Colors.RESET}\n")
|
|
|
|
if not self._ensure_llm_loaded():
|
|
return
|
|
|
|
print(f"{Colors.WHITE}Describe your current access:{Colors.RESET}")
|
|
print(f"{Colors.DIM}(Shell type, privileges, OS, what you've found){Colors.RESET}")
|
|
access_info = input(f"{Colors.WHITE}> {Colors.RESET}").strip()
|
|
|
|
if not access_info:
|
|
return
|
|
|
|
print(f"\n{Colors.CYAN}[*] Generating post-exploitation plan...{Colors.RESET}")
|
|
|
|
system_prompt = """You are a post-exploitation expert. Based on the current access described, provide a structured post-exploitation plan.
|
|
|
|
Include:
|
|
1. Privilege escalation techniques (if not already root/SYSTEM)
|
|
2. Persistence mechanisms
|
|
3. Credential harvesting opportunities
|
|
4. Lateral movement options
|
|
5. Data exfiltration considerations
|
|
6. Relevant Metasploit post modules
|
|
|
|
Be specific with commands and module paths. Prioritize by likelihood of success."""
|
|
|
|
try:
|
|
self.llm.clear_history()
|
|
response = self.llm.chat(f"Current access: {access_info}", system_prompt=system_prompt)
|
|
print(f"\n{Colors.GREEN}Post-Exploitation Plan:{Colors.RESET}\n")
|
|
print(response)
|
|
except LLMError as e:
|
|
self.print_status(f"LLM error: {e}", "error")
|
|
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
# ==================== PENTEST PIPELINE ====================
|
|
|
|
def pentest_pipeline_menu(self):
|
|
"""PentestGPT-style structured penetration testing menu."""
|
|
while True:
|
|
clear_screen()
|
|
display_banner()
|
|
|
|
print(f"{Colors.RED}{Colors.BOLD} Pentest Pipeline (AI-Powered){Colors.RESET}")
|
|
print(f"{Colors.DIM} Structured penetration testing with task tree{Colors.RESET}")
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
|
print()
|
|
|
|
# Status
|
|
llm_status = f"{Colors.GREEN}Ready{Colors.RESET}" if (self.llm and self.llm.is_loaded) else f"{Colors.DIM}Not loaded{Colors.RESET}"
|
|
msf_status = f"{Colors.GREEN}Connected{Colors.RESET}" if self.msf_connected else f"{Colors.DIM}Offline{Colors.RESET}"
|
|
session_status = f"{Colors.GREEN}{self.pentest_session.target}{Colors.RESET}" if self.pentest_session else f"{Colors.DIM}None{Colors.RESET}"
|
|
print(f" {Colors.DIM}LLM: {llm_status} | MSF: {msf_status}{Colors.RESET}")
|
|
print(f" {Colors.DIM}Session: {session_status}{Colors.RESET}")
|
|
print()
|
|
|
|
print(f" {Colors.GREEN}[1]{Colors.RESET} New Pentest Session")
|
|
print(f" {Colors.GREEN}[2]{Colors.RESET} Resume Saved Session")
|
|
print(f" {Colors.GREEN}[3]{Colors.RESET} List Saved Sessions")
|
|
print(f" {Colors.GREEN}[4]{Colors.RESET} Delete Session")
|
|
print()
|
|
if self.pentest_session:
|
|
print(f" {Colors.CYAN}[S]{Colors.RESET} Show Task Tree")
|
|
print()
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
|
print()
|
|
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
|
|
|
|
if choice == "0":
|
|
break
|
|
elif choice == "1":
|
|
self._start_new_pentest_session()
|
|
elif choice == "2":
|
|
self._resume_pentest_session()
|
|
elif choice == "3":
|
|
self._list_pentest_sessions()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
elif choice == "4":
|
|
self._delete_pentest_session()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
elif choice == "s" and self.pentest_session:
|
|
print(f"\n{self.pentest_session.tree.render_text()}")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def _start_new_pentest_session(self):
|
|
"""Start a new pentest session with target."""
|
|
print(f"\n{Colors.BOLD}New Pentest Session{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
|
|
|
|
target = input(f"{Colors.WHITE}Enter target (IP, hostname, or range): {Colors.RESET}").strip()
|
|
if not target:
|
|
return
|
|
|
|
notes = input(f"{Colors.WHITE}Notes (optional): {Colors.RESET}").strip()
|
|
|
|
# Ensure LLM is loaded
|
|
if not self._ensure_llm_loaded():
|
|
return
|
|
|
|
# Create session
|
|
self.pentest_session = PentestSession(target)
|
|
self.pentest_session.notes = notes
|
|
self.pentest_session.start()
|
|
|
|
# Create pipeline
|
|
self.pentest_pipeline = PentestPipeline(
|
|
self.llm, target, self.pentest_session.tree
|
|
)
|
|
|
|
self.print_status(f"Session created: {self.pentest_session.session_id}", "success")
|
|
self.print_status("Generating initial plan...", "info")
|
|
|
|
# Generate initial plan
|
|
try:
|
|
plan = self.pentest_pipeline.get_initial_plan()
|
|
|
|
print(f"\n{Colors.GREEN}Initial Plan:{Colors.RESET}")
|
|
if plan.get('first_action'):
|
|
print(f" First action: {plan['first_action']}")
|
|
if plan.get('reasoning'):
|
|
print(f" Reasoning: {plan['reasoning']}")
|
|
|
|
if plan.get('commands'):
|
|
print(f"\n{Colors.CYAN}Suggested Commands:{Colors.RESET}")
|
|
for i, cmd in enumerate(plan['commands'], 1):
|
|
print(f" {i}. {Colors.GREEN}{cmd['tool']}{Colors.RESET}: {json.dumps(cmd['args'])}")
|
|
print(f" Expect: {cmd.get('expect', 'N/A')}")
|
|
|
|
print(f"\n{Colors.DIM}Task tree initialized with {len(self.pentest_session.tree.nodes)} nodes{Colors.RESET}")
|
|
except Exception as e:
|
|
self.print_status(f"Plan generation error: {e}", "warning")
|
|
print(f"{Colors.DIM}You can still use the session manually{Colors.RESET}")
|
|
|
|
input(f"\n{Colors.WHITE}Press Enter to enter session...{Colors.RESET}")
|
|
|
|
# Enter interactive loop
|
|
self._pentest_interactive_loop()
|
|
|
|
def _resume_pentest_session(self):
|
|
"""Resume a saved session."""
|
|
sessions = PentestSession.list_sessions()
|
|
if not sessions:
|
|
self.print_status("No saved sessions found", "warning")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
return
|
|
|
|
print(f"\n{Colors.BOLD}Saved Sessions:{Colors.RESET}\n")
|
|
resumable = []
|
|
for i, s in enumerate(sessions, 1):
|
|
state_color = Colors.YELLOW if s['state'] == 'paused' else Colors.DIM
|
|
print(f" {Colors.GREEN}[{i}]{Colors.RESET} {s['target']} ({state_color}{s['state']}{Colors.RESET})")
|
|
stats = s.get('tree_stats', {})
|
|
print(f" Steps: {s['steps']} | Findings: {s['findings']} | "
|
|
f"Tasks: {stats.get('todo', 0)} todo, {stats.get('completed', 0)} done")
|
|
print(f" {Colors.DIM}{s['session_id']}{Colors.RESET}")
|
|
resumable.append(s)
|
|
|
|
print(f"\n {Colors.DIM}[0]{Colors.RESET} Cancel")
|
|
choice = input(f"\n{Colors.WHITE} Select session: {Colors.RESET}").strip()
|
|
|
|
if choice == "0" or not choice:
|
|
return
|
|
|
|
try:
|
|
idx = int(choice) - 1
|
|
if 0 <= idx < len(resumable):
|
|
session_id = resumable[idx]['session_id']
|
|
self.pentest_session = PentestSession.load_session(session_id)
|
|
self.pentest_session.resume()
|
|
|
|
if not self._ensure_llm_loaded():
|
|
return
|
|
|
|
self.pentest_pipeline = PentestPipeline(
|
|
self.llm, self.pentest_session.target,
|
|
self.pentest_session.tree
|
|
)
|
|
|
|
self.print_status(f"Resumed session: {self.pentest_session.target}", "success")
|
|
input(f"\n{Colors.WHITE}Press Enter to enter session...{Colors.RESET}")
|
|
self._pentest_interactive_loop()
|
|
except (ValueError, FileNotFoundError) as e:
|
|
self.print_status(f"Error: {e}", "error")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def _list_pentest_sessions(self):
|
|
"""List all saved sessions."""
|
|
sessions = PentestSession.list_sessions()
|
|
if not sessions:
|
|
self.print_status("No saved sessions", "info")
|
|
return
|
|
|
|
print(f"\n{Colors.BOLD}Saved Sessions ({len(sessions)}):{Colors.RESET}\n")
|
|
for s in sessions:
|
|
state_color = {
|
|
'running': Colors.GREEN, 'paused': Colors.YELLOW,
|
|
'completed': Colors.CYAN, 'error': Colors.RED,
|
|
}.get(s['state'], Colors.DIM)
|
|
print(f" {s['target']:25} {state_color}{s['state']:10}{Colors.RESET} "
|
|
f"Steps: {s['steps']:3} Findings: {s['findings']}")
|
|
print(f" {Colors.DIM}{s['session_id']}{Colors.RESET}")
|
|
print()
|
|
|
|
def _delete_pentest_session(self):
|
|
"""Delete a saved session."""
|
|
sessions = PentestSession.list_sessions()
|
|
if not sessions:
|
|
self.print_status("No saved sessions", "info")
|
|
return
|
|
|
|
print(f"\n{Colors.BOLD}Delete Session:{Colors.RESET}\n")
|
|
for i, s in enumerate(sessions, 1):
|
|
print(f" {Colors.GREEN}[{i}]{Colors.RESET} {s['target']} ({s['state']})")
|
|
|
|
print(f"\n {Colors.DIM}[0]{Colors.RESET} Cancel")
|
|
choice = input(f"\n{Colors.WHITE} Select: {Colors.RESET}").strip()
|
|
|
|
try:
|
|
idx = int(choice) - 1
|
|
if 0 <= idx < len(sessions):
|
|
sid = sessions[idx]['session_id']
|
|
confirm = input(f"{Colors.YELLOW}Delete {sid}? (y/n): {Colors.RESET}").strip().lower()
|
|
if confirm == 'y':
|
|
session = PentestSession.load_session(sid)
|
|
session.delete()
|
|
self.print_status("Session deleted", "success")
|
|
if self.pentest_session and self.pentest_session.session_id == sid:
|
|
self.pentest_session = None
|
|
self.pentest_pipeline = None
|
|
except (ValueError, FileNotFoundError) as e:
|
|
self.print_status(f"Error: {e}", "error")
|
|
|
|
def _pentest_interactive_loop(self):
|
|
"""Interactive pentest session loop (PentestGPT-style)."""
|
|
session = self.pentest_session
|
|
pipeline = self.pentest_pipeline
|
|
settings = self.config.get_pentest_settings()
|
|
max_steps = settings['max_pipeline_steps']
|
|
|
|
while session.state == PentestSessionState.RUNNING:
|
|
clear_screen()
|
|
stats = session.tree.get_stats()
|
|
print(f"{Colors.RED}{Colors.BOLD}[Pentest Session: {session.target}]{Colors.RESET}")
|
|
print(f"{Colors.DIM}[Step {session.step_count}/{max_steps}] "
|
|
f"[Tree: {stats['total']} nodes, {stats.get('todo', 0)} todo, "
|
|
f"{stats.get('completed', 0)} done]{Colors.RESET}")
|
|
print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
|
|
print()
|
|
|
|
# Show next recommended task
|
|
next_todo = session.tree.get_next_todo()
|
|
if next_todo:
|
|
print(f" {Colors.CYAN}Next task:{Colors.RESET} {next_todo.label} (P{next_todo.priority})")
|
|
print()
|
|
|
|
print(f" {Colors.DIM}Commands:{Colors.RESET}")
|
|
print(f" {Colors.GREEN}next{Colors.RESET} - Process tool output (paste results)")
|
|
print(f" {Colors.GREEN}exec{Colors.RESET} - Auto-execute next recommended action")
|
|
print(f" {Colors.GREEN}discuss{Colors.RESET} - Ask a question (doesn't affect tree)")
|
|
print(f" {Colors.GREEN}google{Colors.RESET} - Provide external research findings")
|
|
print(f" {Colors.GREEN}tree{Colors.RESET} - Display current task tree")
|
|
print(f" {Colors.GREEN}status{Colors.RESET} - Show session status")
|
|
print(f" {Colors.GREEN}pause{Colors.RESET} - Save session and return to menu")
|
|
print(f" {Colors.GREEN}done{Colors.RESET} - Complete session and generate report")
|
|
print()
|
|
|
|
try:
|
|
cmd = input(f"{Colors.WHITE} > {Colors.RESET}").strip().lower()
|
|
except (EOFError, KeyboardInterrupt):
|
|
cmd = "pause"
|
|
|
|
if cmd == "next":
|
|
self._handle_next(pipeline, session)
|
|
elif cmd == "exec":
|
|
self._handle_exec(pipeline, session)
|
|
elif cmd == "discuss":
|
|
self._handle_discuss(pipeline)
|
|
elif cmd == "google":
|
|
self._handle_google(pipeline, session)
|
|
elif cmd == "tree":
|
|
print(f"\n{session.tree.render_text()}")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
elif cmd == "status":
|
|
self._handle_status(session)
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
elif cmd == "pause":
|
|
session.pause()
|
|
self.print_status("Session paused and saved", "success")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
break
|
|
elif cmd == "done":
|
|
self._handle_done(session)
|
|
break
|
|
|
|
# Check step limit
|
|
if session.step_count >= max_steps:
|
|
self.print_status(f"Step limit ({max_steps}) reached. Session paused.", "warning")
|
|
session.pause()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
break
|
|
|
|
def _handle_next(self, pipeline, session):
|
|
"""Handle 'next' command - process pasted tool output."""
|
|
print(f"\n{Colors.CYAN}Paste tool output below (empty line to finish):{Colors.RESET}")
|
|
lines = []
|
|
while True:
|
|
try:
|
|
line = input()
|
|
if line == "":
|
|
if lines:
|
|
break
|
|
else:
|
|
lines.append(line)
|
|
except EOFError:
|
|
break
|
|
|
|
if not lines:
|
|
return
|
|
|
|
raw_output = '\n'.join(lines)
|
|
source_type = detect_source_type(raw_output)
|
|
self.print_status(f"Detected source: {source_type}", "info")
|
|
self.print_status("Processing through pipeline...", "info")
|
|
|
|
result = pipeline.process_output(raw_output, source_type)
|
|
|
|
# Log to session
|
|
session.log_event('tool_output', {
|
|
'source_type': source_type,
|
|
'parsed_summary': result['parsed'].get('summary', ''),
|
|
})
|
|
session.log_pipeline_result(
|
|
result['parsed'].get('summary', ''),
|
|
result['reasoning'].get('reasoning', ''),
|
|
result.get('commands', []),
|
|
)
|
|
|
|
# Add findings to session
|
|
for finding in result['parsed'].get('findings', []):
|
|
if '[VULN]' in finding or '[CRED]' in finding:
|
|
severity = 'high' if '[VULN]' in finding else 'critical'
|
|
session.add_finding(finding, finding, severity)
|
|
|
|
# Display results
|
|
print(f"\n{Colors.GREEN}--- Parsed ---{Colors.RESET}")
|
|
print(f" Summary: {result['parsed'].get('summary', 'N/A')}")
|
|
if result['parsed'].get('findings'):
|
|
print(f" Findings:")
|
|
for f in result['parsed']['findings']:
|
|
color = Colors.RED if '[VULN]' in f else Colors.YELLOW if '[CRED]' in f else Colors.WHITE
|
|
print(f" {color}- {f}{Colors.RESET}")
|
|
|
|
print(f"\n{Colors.GREEN}--- Reasoning ---{Colors.RESET}")
|
|
print(f" Next task: {result.get('next_task', 'N/A')}")
|
|
print(f" Reasoning: {result['reasoning'].get('reasoning', 'N/A')}")
|
|
if result['reasoning'].get('tree_updates'):
|
|
print(f" Tree updates: {len(result['reasoning']['tree_updates'])}")
|
|
|
|
if result.get('commands'):
|
|
print(f"\n{Colors.GREEN}--- Suggested Commands ---{Colors.RESET}")
|
|
for i, cmd in enumerate(result['commands'], 1):
|
|
print(f" {i}. {Colors.CYAN}{cmd['tool']}{Colors.RESET}: {json.dumps(cmd['args'])}")
|
|
print(f" Expect: {cmd.get('expect', 'N/A')}")
|
|
if result.get('fallback'):
|
|
print(f"\n {Colors.DIM}Fallback: {result['fallback']}{Colors.RESET}")
|
|
|
|
session.save()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def _handle_exec(self, pipeline, session):
|
|
"""Handle 'exec' command - auto-execute next recommended action."""
|
|
next_todo = session.tree.get_next_todo()
|
|
if not next_todo:
|
|
self.print_status("No pending tasks in the tree", "warning")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
return
|
|
|
|
self.print_status(f"Generating commands for: {next_todo.label}", "info")
|
|
|
|
# Generate commands
|
|
context = ""
|
|
if next_todo.details:
|
|
context = next_todo.details
|
|
gen_result = pipeline.generator.generate(
|
|
next_todo.label, session.target, context=context
|
|
)
|
|
|
|
if not gen_result.get('commands'):
|
|
self.print_status("No executable commands generated", "warning")
|
|
if gen_result.get('raw_response'):
|
|
print(f"\n{Colors.DIM}{gen_result['raw_response']}{Colors.RESET}")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
return
|
|
|
|
# Mark task as in progress
|
|
session.tree.update_node(next_todo.id, status=NodeStatus.IN_PROGRESS)
|
|
|
|
# Execute with confirmation
|
|
print(f"\n{Colors.BOLD}Commands to execute:{Colors.RESET}")
|
|
all_output = []
|
|
for i, cmd in enumerate(gen_result['commands'], 1):
|
|
print(f"\n {i}. {Colors.CYAN}{cmd['tool']}{Colors.RESET}: {json.dumps(cmd['args'])}")
|
|
print(f" Expect: {cmd.get('expect', 'N/A')}")
|
|
|
|
choice = input(f" {Colors.WHITE}Execute? (y/n/skip): {Colors.RESET}").strip().lower()
|
|
if choice == 'n':
|
|
break
|
|
elif choice == 'skip':
|
|
continue
|
|
|
|
# Execute the command
|
|
output = self._execute_pipeline_action(cmd)
|
|
if output:
|
|
all_output.append(output)
|
|
print(f"\n{Colors.DIM}Output:{Colors.RESET}")
|
|
# Show truncated output
|
|
display = output[:500]
|
|
if len(output) > 500:
|
|
display += f"\n... ({len(output)} chars total)"
|
|
print(display)
|
|
|
|
# Process collected output through pipeline
|
|
if all_output:
|
|
combined = '\n'.join(all_output)
|
|
self.print_status("Processing results through pipeline...", "info")
|
|
result = pipeline.process_output(combined)
|
|
session.log_pipeline_result(
|
|
result['parsed'].get('summary', ''),
|
|
result['reasoning'].get('reasoning', ''),
|
|
result.get('commands', []),
|
|
)
|
|
|
|
print(f"\n{Colors.GREEN}Next task: {result.get('next_task', 'N/A')}{Colors.RESET}")
|
|
print(f"Reasoning: {result['reasoning'].get('reasoning', 'N/A')}")
|
|
|
|
session.save()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def _execute_pipeline_action(self, action: dict) -> Optional[str]:
|
|
"""Execute a single pipeline action. Returns output string or None."""
|
|
tool_name = action.get('tool', '')
|
|
args = action.get('args', {})
|
|
|
|
try:
|
|
if tool_name == 'shell':
|
|
command = args.get('command', '')
|
|
timeout = args.get('timeout', 30)
|
|
success, output = self.run_cmd(command, timeout=timeout)
|
|
return output
|
|
|
|
elif tool_name.startswith('msf_'):
|
|
from core.tools import get_tool_registry
|
|
registry = get_tool_registry()
|
|
result = registry.execute(tool_name, **args)
|
|
if isinstance(result, dict):
|
|
return result.get('result', str(result))
|
|
return str(result)
|
|
|
|
else:
|
|
self.print_status(f"Unknown tool: {tool_name}", "warning")
|
|
return None
|
|
|
|
except Exception as e:
|
|
self.print_status(f"Execution error: {e}", "error")
|
|
return f"Error: {e}"
|
|
|
|
def _handle_discuss(self, pipeline):
|
|
"""Handle 'discuss' command - ad-hoc question."""
|
|
question = input(f"\n{Colors.WHITE}Question: {Colors.RESET}").strip()
|
|
if not question:
|
|
return
|
|
|
|
self.print_status("Thinking...", "info")
|
|
response = pipeline.discuss(question)
|
|
print(f"\n{Colors.GREEN}{response}{Colors.RESET}")
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def _handle_google(self, pipeline, session):
|
|
"""Handle 'google' command - inject external research."""
|
|
print(f"\n{Colors.CYAN}Paste research findings below (empty line to finish):{Colors.RESET}")
|
|
lines = []
|
|
while True:
|
|
try:
|
|
line = input()
|
|
if line == "" and lines:
|
|
break
|
|
lines.append(line)
|
|
except EOFError:
|
|
break
|
|
|
|
if not lines:
|
|
return
|
|
|
|
info = '\n'.join(lines)
|
|
self.print_status("Injecting information into pipeline...", "info")
|
|
result = pipeline.inject_information(info, source="research")
|
|
|
|
session.log_event('research_injected', {'info': info[:200]})
|
|
session.log_pipeline_result(
|
|
result['parsed'].get('summary', ''),
|
|
result['reasoning'].get('reasoning', ''),
|
|
result.get('commands', []),
|
|
)
|
|
|
|
print(f"\n{Colors.GREEN}Updated reasoning:{Colors.RESET}")
|
|
print(f" Next task: {result.get('next_task', 'N/A')}")
|
|
print(f" Reasoning: {result['reasoning'].get('reasoning', 'N/A')}")
|
|
|
|
session.save()
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
def _handle_status(self, session):
|
|
"""Display current session status."""
|
|
stats = session.tree.get_stats()
|
|
print(f"\n{Colors.BOLD}Session Status{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
|
|
print(f" Target: {session.target}")
|
|
print(f" Session: {session.session_id}")
|
|
print(f" State: {session.state.value}")
|
|
print(f" Steps: {session.step_count}")
|
|
print(f" Findings: {len(session.findings)}")
|
|
print(f"\n Task Tree:")
|
|
print(f" Total: {stats['total']}")
|
|
print(f" Todo: {stats.get('todo', 0)}")
|
|
print(f" Active: {stats.get('in_progress', 0)}")
|
|
print(f" Done: {stats.get('completed', 0)}")
|
|
print(f" N/A: {stats.get('not_applicable', 0)}")
|
|
|
|
if session.findings:
|
|
print(f"\n {Colors.YELLOW}Key Findings:{Colors.RESET}")
|
|
for f in session.findings[-5:]:
|
|
sev = f.get('severity', 'medium').upper()
|
|
print(f" [{sev}] {f['title']}")
|
|
|
|
def _handle_done(self, session):
|
|
"""Handle 'done' command - complete session and generate report."""
|
|
summary = input(f"\n{Colors.WHITE}Session summary (optional): {Colors.RESET}").strip()
|
|
session.complete(summary)
|
|
|
|
report = session.export_report()
|
|
print(f"\n{report}")
|
|
|
|
# Save report to file
|
|
from core.paths import get_reports_dir
|
|
report_path = get_reports_dir()
|
|
report_file = report_path / f"pentest_{session.session_id}.txt"
|
|
with open(report_file, 'w') as f:
|
|
f.write(report)
|
|
|
|
self.print_status(f"Report saved: {report_file}", "success")
|
|
self.pentest_session = None
|
|
self.pentest_pipeline = None
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|
|
|
# ==================== MAIN MENU ====================
|
|
|
|
def show_menu(self):
|
|
clear_screen()
|
|
display_banner()
|
|
|
|
print(f"{Colors.RED}{Colors.BOLD} Agent Hal{Colors.RESET}")
|
|
print(f"{Colors.DIM} AI-powered security automation{Colors.RESET}")
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
|
print()
|
|
|
|
# Status line
|
|
llm_status = f"{Colors.GREEN}Ready{Colors.RESET}" if (self.llm and self.llm.is_loaded) else f"{Colors.DIM}Not loaded{Colors.RESET}"
|
|
msf_status = f"{Colors.GREEN}Connected{Colors.RESET}" if self.msf_connected else f"{Colors.DIM}Offline{Colors.RESET}"
|
|
print(f" {Colors.DIM}LLM: {llm_status} | MSF: {msf_status}{Colors.RESET}")
|
|
print()
|
|
|
|
print(f" {Colors.RED}Defense{Colors.RESET}")
|
|
print(f" {Colors.GREEN}[1]{Colors.RESET} MITM Detection")
|
|
print()
|
|
print(f" {Colors.RED}Offense{Colors.RESET}")
|
|
print(f" {Colors.GREEN}[2]{Colors.RESET} MSF Automation (AI)")
|
|
print(f" {Colors.GREEN}[3]{Colors.RESET} Pentest Pipeline (AI)")
|
|
print()
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
|
print()
|
|
|
|
def run(self):
|
|
while True:
|
|
self.show_menu()
|
|
try:
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
|
|
|
|
if choice == "0":
|
|
break
|
|
elif choice == "1":
|
|
self.mitm_detection_menu()
|
|
elif choice == "2":
|
|
self.msf_automation_menu()
|
|
elif choice == "3":
|
|
self.pentest_pipeline_menu()
|
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
break
|
|
|
|
|
|
def run():
|
|
AgentHal().run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|