4. Migliorie Grafiche con RICH e PYFIGLET

In questa lezione implementeremo dei metodi che ci consentiranno di migliorare l’aspetto grafico dell’interfaccia da riga di comando del nostro scanner. Ci serviranno dei moduli di terze parti, perciò installiamoli all’interno del nostro ambiente virtuale:

pip install pyfiglet rich

La libreria pyfiglet ci permette di generare ASCII Art e la utilizzaremo per mostrare la scritta "PScan" all’avvio del programma, mentre la libreria rich ci consente di formattare il testo mostrato in output dalla nostra interfaccia da riga di comando.


Creiamo tre nuovi metodi per la classe PScan

Iniziamo creando alcuni nuovi metodi della classe PScan di cui avremo bisogno:

  • Il primo metodo si occuperà di mostrare un messaggio alla fine della scansione
  • Il secondo metodo si occuperà di mostrare un messaggio iniziale utilizzando il decoratore @staticmethod
  • Il terzo metodo si occuperà di inizializzare il nostro scanner
class PScan:

    # ...

    def show_completion_message(self):
        pass

    @staticmethod
    def show_startup_message():
        pass

    def initialize(self):
        pass

L’implementazione del metodo initialize() nasce dalla necessità di scomporre il metodo run(), che dovrebbe occuparsi soltanto di avviare la scansione. Al suo interno lasciamo il primo ciclo for (con cui nella lezione precedente abbiamo fatto in modo che venga chiamato il metodo scan_port() per ciascuna delle porte presenti tra le chiavi del dizionario ports_info, che contiene le informazioni relative alle porte estratte dal file common_ports.json), spostiamo la parte iniziale della funzione per inserirla in initialize e spostiamo la parte finale per inserirla in show_completion_message(). In show_startup_message() aggiungiamo per il momento una funzione print:

class PScan:

    # ...

    def show_completion_message(self):
        print("Open Ports:")
        for port in self.open_ports:
            print(str(port), self.ports_info[port])
    
    @staticmethod
    def show_startup_message():
        print("Benvenuti su PScan!")
    
    def initialize(self):
        print("Programma scritto per solo scopo educativo!!!")
        target = input("Inserire Target: ")
        self.remote_host = self.get_host_ip_addr(target)
        self.get_ports_info()
    
    def run(self):
        for port in self.ports_info.keys():
            try:
                print(f"Scanning: {self.remote_host}:{port}")
                self.scan_port(port)
            except KeyboardInterrupt:
                print("\nExiting...")
                break

Chiamiamo show_startup_message() all’interno di initialize() e implementiamo la chiamata al metodo run() utilizzando le istruzioni try ed except facendo in modo che la scansione cominci quando l’utente preme il tasto RETURN. Importiamo anche il modulo sys in modo da fermare l’esecuzione del programma se l’utente preme la combinazione di tasti CTRL+C.

import sys
import socket
from utils import extract_json_data


class PScan:

    # ...

    def initialize(self):
        self.show_startup_message()
        self.get_ports_info()
        print("Programma scritto per solo scopo educativo!!!")
        target = input("Inserire Target: ")
        self.remote_host = self.get_host_ip_addr(target)
        try:
            input("\nPScan is ready. Press ENTER to run the scanner.")
        except KeyboardInterrupt:
            print("\nRoger that. Exiting.")
            sys.exit()
        else:
            self.run()

Utilizziamo sys anche per get_host_ip_addr() in modo che blocchi il programma in caso di errore e facciamo in modo che mostri l’indirizzo IP acquisito:

class PScan:

    # ...

    @staticmethod
    def get_host_ip_addr(target):
        try:
            ip_addr = socket.gethostbyname(target)
        except socket.gaierror as e:
            print(f"C'è stato un errore... {e}")
            sys.exit()
        print(f"\nIP Address acquired: {ip_addr}")
        return ip_addr

Spostiamo il print da initialize() a show_startup_message() e aggiungiamo ad initialize() le istruzioni try ed except anche per chiedere il target:

class PScan:

    # ...
    
    @staticmethod
    def show_startup_message():
        print("Benvenuti su PScan!")
        print("Programma scritto per solo scopo educativo!!!")
    
    def initialize(self):
        self.show_startup_message()
        self.get_ports_info()
        try:
            traget = input("Inserire Target: ")
        except KeyboardInterrupt:
            print("\nRoger that. Exiting.")
            sys.exit()

        self.remote_host = self.get_host_ip_addr(target)

        try:
            input("\nPScan is ready. Press ENTER to run the scanner.")
        except KeyboardInterrupt:
            print("\nRoger that. Exiting.")
            sys.exit()
        else:
            self.run()

Con il metodo initialize() stiamo quindi svolgendo le operazioni che seguono:

  • Mostriamo il messaggio di startup
  • Chiediamo le info sulle porte
  • Chiediamo il target all’utente
  • Otteneniamo l’indirizzo IP del nostro remote_host tramite il metodo get_host_ip_addr() a cui passiamo target come parametro
  • Chiediamo all’utente se intende avviare la scansione sul target: in tal caso alla pressione di ENTER viene avviato lo scanner implementato all’interno di run()

Facciamo in modo che il metodo run() mostri il messaggio di completamento con le porte aperte rilevate:

class PScan:

    # ...

    def run(self):
        for port in self.ports_info.keys():
            # ...
        self.show_completion_message()

Modifichiamo l’istruzione if __name__ == "__main__" in modo che per avviare lo scanner venga chiamato il metodo initialize():

if __name__ == "__main__":
    pscan = PScan()
    pscan.initialize()

Se avviamo adesso il nostro programma vedremo un messaggio che ci informa che abbiamo ottenuto l’indirizzo IP del target.


Utilizziamo le librerie pyfiglet e rich per migliorare lo stile del nostro port scanner

Implementiamo i due package che abbiamo installato: importiamo pyfiglet e andiamo a modificare show_startup_message() in modo da utilizzarlo per mostrare il titolo del nostro programma. Importiamo anche la console di rich e aggiungiamola alla chiamata di print:

import pyfiglet
from rich.console import Console

console = Console()

class PScan:

    # ...
    
    @staticmethod
    def show_startup_message():
        ascii_art = pyfiglet.figlet_format("# PSCAN #")
        console.print(ascii_art)

Facciamo in modo che la scritta PSCAN sia verde e in grassetto a aggiungiamo una serie di cancelletti e una scritta:

def show_startup_message():
    ascii_art = pyfiglet.figlet_format("# PSCAN #")
    console.print(f"[bold green]{ascii_art}[/bold green]")
    console.print("#" * 55, style="bold green")
    console.print(
        "#" * 9, "Simple Multithread TCP Port SCanner", "#" * 9, style="bold green"
    )
    console.print("#" * 55, style="bold green")
    print()

Modifichiamo i messaggi di initialize() che utilizziamo per gestire gli Interrupt:

def initialize(self):
    # ...
    except KeyboardInterrupt:
        console.print("\nRoger that. Exiting.", style="bold red")
        sys.exit()
    # ...
    except KeyboardInterrupt:
        console.print("\nRoger that. Exiting.", style="bold red")
        sys.exit()
    else:
        self.run()

E modifichiamo anche i messaggi di get_host_ip_addr():

@staticmethod
def get_host_ip_addr(target):
    try:
        ip_addr = socket.gethostbyname(target)
    except socket.gaierror as e:
        print(f"C'è stato un errore... {e}")
        sys.exit()
	console.print(f"\nIP Address acquired: [bold blue]{ip_addr}[/bold blue]")
    return ip_addr


Come mostrare le porte aperte trovate in una tabella con il modulo rich

Personalizziamo il metodo show_completion_message() in modo che le porte aperte vengano mostrate in una tabella. Importiamo Table da rich.table e implementiamola definendone lo stile e definendo tre colonne, a cui aggiungiamo delle righe tramite il ciclo for:

from rich.table import Table

def show_completion_message(self):
    print()
    if self.open_ports:
        console.print("Scan Completed. Open Ports:", style="bold blue")
        table = Table(show_header=True, header_style="bold green")
        table.add_column("PORT", style="blue")
        table.add_column("STATE", style="blue", justify="center")
        table.add_column("SERVICE", style="blue")
        for port in self.open_ports:
            table.add_row(str(port), "OPEN", self.ports_info[port])
        console.print(table)
    else:
        console.print("No Open Ports Found on Target.", style="bold magenta")

Ecco il nostro codice completo:

import socket
import sys

import pyfiglet
from rich.console import Console
from rich.table import Table

from utils import extract_json_data

console = Console()


class PScan:

    PORTS_DATA_FILE = "./common_ports.json"

    def __init__(self):
        self.ports_info = {}
        self.open_ports = []
        self.remote_host = ""

    def get_ports_info(self):
        data = extract_json_data(PScan.PORTS_DATA_FILE)
        self.ports_info = {int(k): v for (k, v) in data.items()}

    def scan_port(self, port):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1)
        conn_status = sock.connect_ex((self.remote_host, port))
        if conn_status == 0:
            self.open_ports.append(port)
        sock.close()

    def show_completion_message(self):
        print()
        if self.open_ports:
            console.print("Scan Completed. Open Ports:", style="bold blue")
            table = Table(show_header=True, header_style="bold green")
            table.add_column("PORT", style="blue")
            table.add_column("STATE", style="blue", justify="center")
            table.add_column("SERVICE", style="blue")
            for port in self.open_ports:
                table.add_row(str(port), "OPEN", self.ports_info[port])
            console.print(table)
        else:
            console.print(f"No Open Ports Found on Target", style="bold magenta")

	@staticmethod
    def show_startup_message():
        ascii_art = pyfiglet.figlet_format("# PSCAN #")
        console.print(f"[bold green]{ascii_art}[/bold green]")
        console.print("#" * 55, style="bold green")
        console.print(
            "#" * 9, "Simple MultiThread TCP Port Scanner", "#" * 9, style="bold green"
        )
        console.print("#" * 55, style="bold green")
        print()

    @staticmethod
    def get_host_ip_addr(target):
        try:
            ip_addr = socket.gethostbyname(target)
        except socket.gaierror as e:
            console.print(f"{e}. Exiting.", style="bold red")
            sys.exit()
        console.print(f"\nIP address acquired: [bold blue]{ip_addr}[/bold blue]")
        return ip_addr

    def initialize(self):
        self.show_startup_message()
        self.get_ports_info()
        try:
            target = console.input("[bold blue]Target: ")
        except KeyboardInterrupt:
            console.print(f"\nRoger that! Exiting.", style="bold red")
            sys.exit()
        self.remote_host = self.get_host_ip_addr(target)
        try:
            input("\nPScan is ready. Press ENTER to run the scanner.")
        except KeyboardInterrupt:
            console.print(f"\nRoger that. Exiting.", style="bold red")
            sys.exit()
        else:
            self.run()

    def run(self):
		for port in self.ports_info.keys():
		    try:
		        print(f"Scanning: {self.remote_host}:{port}")
		        self.scan_port(port)
		    except KeyboardInterrupt:
		        print("\nExiting...")
		        break
		self.show_completion_message()

if __name__ == "__main__":
    pscan = PScan()
    pscan.initialize()