# First Project Port Scanner 
# Cameron Miller


import socket
import time
from concurrent.futures import ThreadPoolExecutor
import ipaddress
from tracemalloc import start
import psutil
import subprocess
import re
import csv


def grab_banner(ip, port, target):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(1)

    try:
        s.connect((ip, port))

        # HTTP servers usually require the client to send a request first.
        if port in (80, 8000, 8080):
            request = (
                "GET / HTTP/1.1\r\n"
                f"Host: {target}\r\n"
                "Connection: close\r\n"
                "\r\n"
            )
            s.sendall(request.encode())

        data = s.recv(1024)

        if not data:
            return "No banner"

        return data.decode(errors="ignore").strip()

    except (socket.timeout, OSError):
        return "No banner"

    finally:
        s.close()


def scan_host(target = None, start = None, end = None, compact = False):
    open_ports = []
    closed_ports = []
    filtered_ports = []

    if target is None:
        target = input("Host name or IP address: ").strip()

    try:
        ip = socket.gethostbyname(target)
        print(f"Scanning {ip}")
    except socket.gaierror:
        print("Invalid hostname or IP address.")
        return [], [], []

    if start is None or end is None:
        try:
            start = int(input("Enter the starting port number: "))
            end = int(input("Enter the ending port number: "))
        except ValueError:
            print("Port numbers must be whole numbers.")
            return [], [], []

    if start < 1 or end > 65535 or start > end:
        print("Enter a valid port range from 1 to 65535.")
        return

    def scan_port(port):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(0.5)

        try:
            result = s.connect_ex((ip, port))

            if result == 0:
                try:
                    service = socket.getservbyport(port, "tcp")
                except OSError:
                    service = "Unknown"

                banner = grab_banner(ip, port, target)
                open_ports.append((port, service, banner))

            elif result == 10061:
                closed_ports.append(port)

            else:
                filtered_ports.append(port)

        except (socket.timeout, OSError):
            filtered_ports.append(port)

        finally:
            s.close()

    start_time = time.time()
    ports = range(start, end + 1)

    with ThreadPoolExecutor(max_workers=50) as executor:
        list(executor.map(scan_port, ports))

    open_ports.sort()
    closed_ports.sort()
    filtered_ports.sort()

    end_time = time.time()

    if not compact:
        print("\n" + "=" * 100)
        print(" " * 43 + "PORT SCANNER")
        print("=" * 100)

        print(f"Target:          {target}")
        print(f"IP Address:      {ip}")
        print(f"Port Range:      {start}-{end}")

        print("\n" + "-" * 100)
        print(f"{'PORT':<10}{'STATE':<12}{'SERVICE':<15}{'BANNER'}")
        print("-" * 100)

    else:
        print(f"\nHost: {target}")
        print("-" * 40)

    for port, service, banner in open_ports:
        clean_banner = banner.replace("\r", " ").replace("\n", " ")

        print(
            f"{port:<10}"
            f"{'OPEN':<12}"
            f"{service:<15}"
            f"{clean_banner}"
        )

    if not open_ports:
        print(f"No open ports found on {target}.")

    ports_scanned = end - start + 1
    ports_recorded = len(open_ports) + len(closed_ports) + len(filtered_ports)

    if not compact:
        print("-" * 100)
        print(f"Open Ports:       {len(open_ports)}")
        print(f"Closed Ports:     {len(closed_ports)}")
        print(f"Filtered Ports:   {len(filtered_ports)}")
        print(f"Ports Scanned:    {ports_scanned}")
        print(f"Ports Recorded:   {ports_recorded}")
        print(f"Scan Time:        {end_time - start_time:.2f} seconds")
        print("=" * 100)

    else:
        print("-" * 40)
        print(f"{len(open_ports)} open port(s) found on {target}.")

    return open_ports, closed_ports, filtered_ports


def newHosts():
    hosts = []
    adapters = psutil.net_if_addrs()

    for adapter, addresses in adapters.items():
        for address in addresses:
            if address.family == socket.AF_INET:
                ip = address.address
                netmask = address.netmask

                if (ip.startswith("127.") or ip.startswith("169.254.") or not netmask):
                    continue

                network = ipaddress.ip_network(f"{ip}/{netmask}", strict=False)

                print(f"\nAdapter: {adapter}")
                print(f"IP Address: {ip}")
                print(f"Network: {network}")

                for host in network.hosts():
                    hosts.append(str(host))

                return hosts


def check_host(host):
    common_ports = [22, 80, 135, 139, 443, 445, 3389]

    for port in common_ports:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(0.3)

        try:
            result = s.connect_ex((host, port))

            if result == 0:
                return host

            elif result == 10061:
                return host

        except (socket.timeout, OSError):
            pass

        finally:
            s.close()

    return None


def activeHostsTCP(hosts):
    active_hosts = []

    with ThreadPoolExecutor(max_workers=50) as executor:
        results = executor.map(check_host, hosts)

    for result in results:
        if result is not None:
            active_hosts.append(result)

    return active_hosts


def discoverHostnames(activeHosts):
    hostInfo = []

    for host in activeHosts:
        try:
            hostname = socket.gethostbyaddr(host)[0]
        except socket.herror:
            hostname = "Unknown"

        hostInfo.append((host, hostname))

    return hostInfo


def scanNetwork(active_hosts):
    if not active_hosts:
        print("No active hosts found.")
        return

    start = 1
    end = 1024

    print(f"\nScanning {len(active_hosts)} active hosts...")
    print(f"Port range: {start}-{end}")

    for i, host in enumerate(active_hosts, start=1):
        print(f"\nHost {i} of {len(active_hosts)}")
        scan_host(host, start, end)


def getMAC(ip):
    try:
        result = subprocess.run(["arp", "-a", ip], capture_output=True, text=True)
        match = re.search(r"([0-9a-fA-F]{2}-){5}[0-9a-fA-F]{2}", result.stdout)

        if match:
            return match.group().replace("-", ":")

    except OSError:
        pass

    return "Unknown"


def loadOUI():
    oui_lookup = {}

    with open("oui.csv", newline="", encoding="utf-8") as file:
        reader = csv.DictReader(file)

        for row in reader:
            oui = row["Assignment"].upper()
            vendor = row["Organization Name"]
            oui_lookup[oui] = vendor

    return oui_lookup


oui_lookup = loadOUI()


def getMACVendor(mac):
    if mac == "Unknown":
        return "Unknown"
    oui = mac.upper().replace(":", "")[:6]
    return oui_lookup.get(oui, "Unknown Vendor")


def main():
    active_hosts = []
    host_info = []

    print("Welcome to the C$ Port Scanner!")

    while True:
        print("\n====================================")
        print("          C$ PORT SCANNER")
        print("====================================")
        print("1. Scan a host")
        print("2. Discover active hosts")
        print("3. View discovered hosts")
        print("4. Scan Entire Network")
        print("5. MAC & Vendor Lookup")
        print("6. Exit")

        choice = input("\nEnter your choice: ")

        if choice == "1":
            if active_hosts:
                print("\nHow would you like to scan?")
                print("1. Scan a discovered host")
                print("2. Enter a hostname or IP manually")

                scan_choice = input("\nChoice: ")

                if scan_choice == "1":
                    print("\nDiscovered Hosts")
                    print("-" * 30)

                    for i, host in enumerate(active_hosts, start=1):
                        print(f"{i}. {host}")

                    host_choice = int(input("\nSelect a host: "))

                    if 1 <= host_choice <= len(active_hosts):
                        target = active_hosts[host_choice - 1]
                    else:
                        print("Invalid selection.")
                        continue

                elif scan_choice == "2":
                    target = input("\nHost name or IP address: ").strip()

                else:
                    print("Invalid choice.")
                    continue

            else:
                print("\nNo discovered hosts found.")
                print("Falling back to manual entry.")
                target = input("\nHost name or IP address: ").strip()

            scan_host(target)

        elif choice == "2":
            print("\nDiscovering active hosts...")
            active_hosts = activeHostsTCP(newHosts())
            print(f"\nFound {len(active_hosts)} active host(s).")

            if active_hosts:
                print("Resolving hostnames...\n")
                host_info = discoverHostnames(active_hosts)

                print(f"{'#':<5}{'IP ADDRESS':<18}{'HOSTNAME'}")
                print("-" * 55)

                for i, (ip, hostname) in enumerate(host_info, start=1):
                    print(f"{i:<5}{ip:<18}{hostname}")
            else:
                host_info = []
                print("No active hosts were found.")

        elif choice == "3":
            if not host_info:
                print("\nNo hosts have been discovered yet.")
                print("Run option 2 first.\n")
                continue

            print("\n" + "=" * 55)
            print("                 DISCOVERED HOSTS")
            print("=" * 55)

            print(f"{'#':<5}{'IP ADDRESS':<18}{'HOSTNAME'}")
            print("-" * 55)

            for i, (ip, hostname) in enumerate(host_info, start=1):
                print(f"{i:<5}{ip:<18}{hostname}")

            print("-" * 55)
            print(f"Total Active Hosts: {len(host_info)}")

        elif choice == "4":
            if not host_info:
                print("\nNo hosts have been discovered yet.")
                print("Run option 2 first.\n")
                continue

            try:
                start = int(input("\nEnter starting port: "))
                end = int(input("Enter ending port: "))
            except ValueError:
                print("Invalid port numbers.")
                continue

            print("\nScanning all discovered hosts...")

            for active_host in active_hosts:
                scan_host(active_host, start, end, compact=True)

        elif choice == "5":
            if not host_info:
                print("\nNo hosts have been discovered yet.")
                print("Run option 2 first.\n")
                continue

            print(
                f"{'#':<5}"
                f"{'IP ADDRESS':<18}"
                f"{'HOSTNAME':<25}"
                f"{'MAC ADDRESS':<20}"
                f"{'VENDOR'}"
            )
            print("-" * 100)

            for i, (ip, hostname) in enumerate(host_info, start=1):
                mac_address = getMAC(ip)
                vendor = getMACVendor(mac_address)

                print(
                    f"{i:<5}"
                    f"{ip:<18}"
                    f"{hostname:<25}"
                    f"{mac_address:<20}"
                    f"{vendor}"
                )

            print("-" * 100)
            print(f"Total Active Hosts: {len(host_info)}")

        elif choice == "6":
            print("Exiting the program.")
            break

        else:
            print("Invalid choice.")


if __name__ == "__main__":
    main()
