

import time
import subprocess
import re
from stem import Signal
from stem.control import Controller
from datetime import datetime

# Settings
TOR_CONTROL_PORT = 9051
TOR_PASSWORD = 'yourepassword'  # Control password (str, unhashed; leave '' if no password)
MAX_PORTS = 100  # Max ports to prevent long scans
DEFAULT_PORTS = '21,22,23,25,53,80,110,111,135,139,143,443,993,995,1723,3306,3389,5900,8080,8443'  # 20 common ports
MAX_SCANS = 10  # Prevent too many scans

def renew_tor_circuit():
    """Change exit node by creating a new circuit"""
    try:
        with Controller.from_port(port=TOR_CONTROL_PORT) as controller:
            controller.authenticate(password=TOR_PASSWORD)
            controller.signal(Signal.NEWNYM)
            print("Request to change exit node sent. Waiting 10 seconds...")
            time.sleep(10)  # Wait for new circuit build
        print("Exit node changed!")
    except Exception as e:
        print(f"Error renewing circuit: {e}")
        print("Check TOR_PASSWORD or torrc settings (CookieAuthentication 0 + HashedControlPassword).")

def stealth_nmap_scan(target, ports_list, change_every_n_ports, version_detection, log_file):
    """Advanced nmap scan through Tor using torify, with node changes every N ports"""
    open_ports = []
    current_circuit_start = 0
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    try:
        # Split ports into chunks for node changes
        for i in range(0, len(ports_list), change_every_n_ports):
            chunk_ports = ports_list[i:i + change_every_n_ports]
            chunk_range = ','.join(map(str, chunk_ports))
            
            # Renew circuit for each chunk
            if i > current_circuit_start:
                renew_tor_circuit()
                current_circuit_start = i
            
            # Nmap command for this chunk
            cmd = [
                'torify', 'nmap', '-sT', '-T2', '--scan-delay', '1s',
                '-p', chunk_range, target
            ]
            if version_detection:
                cmd.insert(-3, '-sV')  # Add version detection
            
            print(f"Scanning chunk: ports {chunk_range}...")
            
            # Run nmap
            process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, universal_newlines=True)
            start_time = time.time()
            while process.poll() is None:
                if time.time() - start_time > 20:
                    print("... Scan in progress (Tor is slow, be patient)...")
                    start_time = time.time()
                time.sleep(5)
            
            stdout, stderr = process.communicate(timeout=300)  # 5 min timeout per chunk
            
            # Filter stderr: ignore LD_PRELOAD/torsocks warnings
            filtered_stderr = [line for line in stderr.splitlines() if 'LD_PRELOAD' not in line and 'libtorsocks' not in line]
            if filtered_stderr:
                print("Nmap warnings/errors:\n", '\n'.join(filtered_stderr))
            
            if stdout:
                print("Nmap output:\n", stdout)
            
            if process.returncode != 0:
                print(f"Nmap failed with code {process.returncode} for chunk {chunk_range}")
                continue
            
            # Parse output for open ports
            lines = stdout.splitlines()
            for line in lines:
                if re.match(r'\d+/\tcp\s+open\s+\w+', line):
                    match = re.match(r'(\d+)/tcp\s+open\s+(\w+)', line)
                    if match:
                        port = match.group(1)
                        service = match.group(2)
                        open_ports.append({'port': port, 'service': service, 'version': 'Detected' if version_detection else 'N/A'})
                        print(f"  Port {port}/tcp\tState: open\tService: {service}")
            
            # Log to file
            with open(log_file, 'a') as f:
                f.write(f"\n--- Scan Chunk at {timestamp} ---\nPorts: {chunk_range}\nOutput: {stdout}\n")
        
        # Host info (from first chunk or overall)
        host_match = re.search(r'Nmap scan report for (\S+)', stdout)  # Reuse last stdout
        if host_match:
            print(f'Host: {host_match.group(1)} (State: up)')
        
        return open_ports
    except subprocess.TimeoutExpired:
        print("Nmap scan timed out for chunk.")
        process.kill()
        return open_ports  # Return partial results
    except Exception as e:
        print(f"Error during nmap scan: {e}")
        return open_ports

# Main execution
if __name__ == "__main__":
    # Get target
    target = input("Enter the target IP or domain: ").strip()
    if not target:
        print("Please enter a valid IP or domain!")
        exit(1)
    
    # Get ports input
    ports_input = input("Enter ports to scan (comma-separated, e.g., 80,443,22; default 20 common): ").strip()
    if ports_input:
        ports_list = [int(p.strip()) for p in ports_input.split(',') if p.strip().isdigit()]
    else:
        ports_list = [int(p.strip()) for p in DEFAULT_PORTS.split(',') if p.strip().isdigit()]
    
    # Limit ports
    ports_list = ports_list[:MAX_PORTS]
    num_ports = len(ports_list)
    print(f"Scanning {num_ports} ports: {ports_list}")
    
    # Get node change frequency
    change_input = input("Change exit node every N ports? (e.g., 10; default 10 - more ports = longer time): ").strip()
    change_every_n_ports = int(change_input) if change_input.isdigit() else 10
    estimated_time = (num_ports / change_every_n_ports) * 1.5  # Rough estimate in minutes
    print(f"Node change every {change_every_n_ports} ports. Estimated time: ~{estimated_time:.1f} minutes (Tor is slow).")
    
    # Version detection?
    version_input = input("Enable version detection (-sV)? (y/n, default n): ").strip().lower()
    version_detection = version_input == 'y'
    
    # Number of full scans (rounds)
    num_scans_input = input("Number of full scans (default 1): ").strip()
    num_scans = min(int(num_scans_input) if num_scans_input.isdigit() else 1, MAX_SCANS)
    
    # Log file
    log_file = f"scan_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
    print(f"Output will be logged to {log_file}")
    
    for scan_round in range(num_scans):
        print(f"\n--- Full Scan Round {scan_round + 1} ---")
        open_ports = stealth_nmap_scan(target, ports_list, change_every_n_ports, version_detection, log_file)
        
        if open_ports:
            print(f"\nOpen ports found: {len(open_ports)}")
            for p in open_ports:
                print(f"- Port {p['port']}: {p['service']} (Version: {p['version']})")
        else:
            print("No open ports found.")
        
        if scan_round < num_scans - 1:
            time.sleep(5)  # Delay between full rounds
