#!/usr/bin/env python3
#This script setups a target to scan, adds a task to scan the target with specified options, runs a scan and then returns a report
#By default this script uses a unix socket to connect to greenbone. to connect via ssh, uncomment line 357 and comment out line 355

#can read user and password from a .env file as follows:
#USERN=admin   #gvm user
#PASS=suchASECRETpassword   #gvm password
#mail_host='smtp.gmail.com'
#mail_port=5087
#mail_user="xxx@gmail.com"
#mail_pass="xxx"
#mail_from='xxx@gmail.com'
#https://supabase.com/docs/reference/python/initializing
# SUPABASE_URL=<>
# SUPABASE_KEY=<>
#https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
#https://stackoverflow.com/questions/46160886/how-to-send-smtp-email-for-office365-with-python-using-tls-ssl
#ms_tenant_id="<>"
#ms_app_id="<>"
#ms_app_secret_val="<>"

#import requirements
import os, sys, getpass, time, getopt, subprocess
from O365 import Account
from O365.utils.token import FileSystemTokenBackend
from datetime import date
from simple_term_menu import TerminalMenu #https://github.com/IngoMeyer441/simple-term-menu
from dotenv import load_dotenv
from lxml import etree
from gvm.connections import (
    DEFAULT_TIMEOUT,
    SSHConnection,
    TLSConnection,
    UnixSocketConnection,
)
from gvm.protocols.gmp import GMP #https://github.com/greenbone/python-gvm
from gvm.transforms import EtreeTransform
from gvm.xml import pretty_print
from gvm.errors import GvmError
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Optional
import re
from supabase import create_client, Client

load_dotenv() #load the .env file

has_port_argument = False
has_config_argument = False
has_scanner_argument = False

#setup mail settings from .env
# mail_host = ""
# if("mail_host" in os.environ):
#     mail_host = os.environ["mail_host"]
# mail_port = 0
# if("mail_port" in os.environ):
#     mail_port = int(os.environ["mail_port"])
# mail_user = ""
# if("mail_user" in os.environ):
#     mail_user = os.environ["mail_user"]
# mail_pass = ""
# if("mail_pass" in os.environ):
#     mail_pass = os.environ["mail_pass"]
# mail_from = ""
# if("mail_from" in os.environ):
#     mail_from = os.environ["mail_from"]
    

# if(len(mail_host) > 0 and mail_port > 0 and len(mail_user) > 0 and len(mail_pass) > 0 and len(mail_from) > 0):
#     mail_configured = True
# else:
#     mail_configured = False

#process cli options
if(len(sys.argv) < 2):
    print("Must supply target. For help and usage, use --help")
    sys.exit(2)
else:
    args = sys.argv[1:]
    if(len(args) < 2):
        args = args[0].split(" ", 1) #split args from target host
    target_list=args[0].split(":")
    args = args[1:] #trim off the target
    if(len(args) == 1): #if we only have one string after the target host
        #its probably all of the arguments combined, so split it using a regex look-behind to keep the tokens intact
        #https://stackoverflow.com/questions/4998629/split-string-with-multiple-delimiters-in-python
        args_string = " "+args[0] #add an extra space incase the first token is a "-" option to make the split work nicely 
        delimiters = "--", " -d", " -p", " -c", " -s", " -n", " -k", " -h", "-z"
        regex_pattern = '|'.join('(?={})'.format(re.escape(delim)) for delim in delimiters)
        args_string = re.split(regex_pattern, args_string)
        #loop through split arguments and add them to the arg list
        args = []
        for new_arg in args_string:
            new_arg = new_arg.strip()#clean up whitspaces from the split string
            if(len(new_arg) > 0):
                args.append(new_arg)

#define getopt options used to parse cli options
try:
    options, remainder = getopt.getopt(args, 'hdkzp:c:s:m:', ["help", "defaults", "skipscan", "supabase", "portlist=", "config=", "scanner=", "mail="])
except getopt.error as err:
    print(str(err))
    sys.exit(2)

skip_scan = False
save_to_supabase = False
for opt, arg in options:
    arg = arg.replace("_", " ").replace("'", "").replace('"', "")
    if(opt in ("-d", "--defaults")):
        has_port_argument = True
        port_argument = "All IANA assigned TCP"
        has_config_argument = True
        config_argument = "Full and fast"
        has_scanner_argument = True
        scanner_argument = "OpenVAS Default"
    elif(opt in ("-p", "--portlist")):
        has_port_argument = True
        port_argument = arg
    elif(opt in ("-c", "--config")):
        has_config_argument = True
        config_argument = arg
    elif(opt in ("-s", "--scanner")):
        has_scanner_argument = True
        scanner_argument = arg
    elif(opt in ("-m", "--mail")):
        mail_to = arg.split(":")
    elif(opt in ("-k", "--skipscan")):
        skip_scan = True
    elif(opt in ("-z", "--supabase")):
        save_to_supabase = True
    else: #"--help"
        print("This script setups a target to scan, adds a task to scan the target with specified options, runs a scan and then returns a report")
        print("Usage:")
        print("python3 cli.py <target_host> --help --defaults --skipscan --portlist=All_IANA_assigned_TCP --config=Full_and_fast --scanner=OpenVAS_Default --mail=joshua@overdrive.co.za:jaco@overdrive.co.za")
        print("")
        print("Target host is required, all other arguments are optional. Target hosts can be specified as")
        print("a colon(:) seperated list of urls to scan")
        print("Skipscan can be used to skip a scan and instead load the last existing report if available")
        print("Portlist, Config and Scanner options depend on each installation and customisations.")
        print("Run the command without an option tag to see all the available options when adding a new host")
        print("Note: python doesnt like spaces in the argument values, so replace spaces with underscores(_'s).")
        print("The script will replace the underscores with spaces")
        print("The Mail option must be passed a colon(:) seperated list of email addresses")
        print("To configure a .env file, please check the Readme.md or the initial comments in the script file.")
        sys.exit()

#Draw and update a progress bar to the cli
#https://stackoverflow.com/questions/3173320/text-progress-bar-in-terminal-with-block-characters
def progres(count, total, status='', bar_len=50):
    filled_len = int(round(bar_len * count / float(total))) #figure out how many ='s we are at now

    percents = round(100.0 * count / float(total), 1) #turn that into a %
    bar = '=' * filled_len + '-' * (bar_len - filled_len) #draw out the bar value, aka ==-----

    fmt = '[%s] %s%s ...%s' % (bar, percents, '%', status)
    print('\b' * len(fmt), end='')  #clears the existing bar line
    sys.stdout.write(fmt) #write the new bar line
    sys.stdout.flush()

#Convert an xml tree to a dict, makes working with report results significantly easier
def elem2dict(node, attributes=True):
    """
    Convert an lxml.etree node tree into a dict. https://gist.github.com/jacobian/795571
    """
    result = {}
    if attributes: #loop over html attributes of the current element like src=doge.png in <img src=doge.png/>
        for item in node.attrib.items():
            key, result[key] = item

    #loop over all the immediate child dom elements under the current element
    for element in node.iterchildren():
        # Remove namespace prefix
        key = etree.QName(element).localname

        # Process element as tree element if the inner XML contains non-whitespace content
        if element.text and element.text.strip():
            value = element.text #if text, use a text value
        else:
            value = elem2dict(element) #else loop function on elements children
        
        #append data to result dict and return it
        if key in result:
            if type(result[key]) is list:
                result[key].append(value)
            else:
                result[key] = [result[key], value]
        else:
            result[key] = value
    return result


def get_options(gmp, call, cli_opt=False):
    """
    Gets a list of options via api call from greenbone and presents them as a selectable menu
    call can be one of [get_port_lists, get_scan_configs, get_scanners]
    gmp is the authenticated green bone library instance
    cli_opt takes a command line option and uses that instead of showing the selectable
    returns the internal gvm id for the option
    """
    if(call == "get_port_lists"):
        results = elem2dict(gmp.get_port_lists()) #get a list of all port lists and convert data to a dict
        count_index = "port_list_count"
        data_index = "port_list"
        nice_name = "Port Lists"
    elif(call == "get_scan_configs"):
        results = elem2dict(gmp.get_scan_configs()) #get a list of all scan configs and convert data to a dict
        count_index = "config_count"
        data_index = "config"
        nice_name = "Scan Config"
    elif(call == "get_scanners"):
        results = elem2dict(gmp.get_scanners()) #get a list of all the scanners and convert data to a dict
        count_index = "scanner_count"
        data_index = "scanner"
        nice_name = "Scanners"
    
    #process api responses
    options_dict = {}
    #steo down tree to the data we want
    data_count = results[count_index]
    if(data_index in results):
        results = results[data_index]
    else:
        results = []
    found_results = False
    #fix api inconsistency
    if(type(results) == dict):
        results=[results]
    
    #add found options to a dict
    for x in results:
        options_dict[x["name"]] = x["id"]
        found_results = True
    if(found_results == False):
        print(nice_name+" not found. Please ensure your GVM "+nice_name+" are configured correctly")
        sys.exit(2)
    # Show menu if not passed a cli argument
    if(cli_opt == False):
        options = list(options_dict.keys()) #set the selectable options
        terminal_menu = TerminalMenu(options,title="Select "+nice_name, skip_empty_entries=True) #define the tui menu
        menu_entry_index = terminal_menu.show() #show the tui menu
        selected_id = options_dict[options[menu_entry_index]] #get the users response and convert it to the options id
        print(nice_name+" selected: "+options[menu_entry_index])
    else: #if passed a cli otpion, use that options id instead of showing a menu
        selected_id = options_dict[cli_opt]
        print(nice_name+" selected: "+cli_opt)
    return selected_id

#normal way to send email
#https://stackoverflow.com/questions/64505/sending-mail-from-python-using-smtp
# def send_email(host, port, user, pwd, recipients, subject, body, html=None, mail_from=None):

#     FROM = mail_from if mail_from else user 
#     TO = recipients if isinstance(recipients, (list, tuple)) else [recipients]
#     SUBJECT = subject
#     TEXT = body
#     HTML = html

#     if not html:
#         # Prepare actual message
#         message = """From: %s\nTo: %s\nSubject: %s\n\n%s
#         """ % (FROM, ", ".join(TO), SUBJECT, TEXT)
#     else:
#                 # https://stackoverflow.com/questions/882712/sending-html-email-using-python#882770
#         msg = MIMEMultipart('alternative')
#         msg['Subject'] = SUBJECT
#         msg['From'] = FROM
#         msg['To'] = ", ".join(TO)

#         # Record the MIME types of both parts - text/plain and text/html.
#         # utf-8 -> https://stackoverflow.com/questions/5910104/python-how-to-send-utf-8-e-mail#5910530
#         part1 = MIMEText(TEXT, 'plain', "utf-8")
#         part2 = MIMEText(HTML, 'html', "utf-8")

#         # Attach parts into message container.
#         # According to RFC 2046, the last part of a multipart message, in this case
#         # the HTML message, is best and preferred.
#         msg.attach(part1)
#         msg.attach(part2)

#         message = msg.as_string()

#     port = int(port)
#     try:
#         if port in (465,):
#             server = smtplib.SMTP_SSL(host, port)
#         else:
#             server = smtplib.SMTP(host, port)

#         server.connect(host,port)
#         # optional
#         server.ehlo()

#         if port in (587,): 
#             server.starttls()
        
#         server.ehlo() #timesheets.php8 --mail=joshua@overdrive.co.za

#         server.login(user, pwd)
#         server.sendmail(FROM, TO, message)
#         server.close()
#         # logger.info("SENT_EMAIL to %s: %s" % (recipients, subject))
#     except Exception as ex:
#         return ex

#     return None

#send a ms outlook email using oath2 + O365
#https://stackoverflow.com/questions/46160886/how-to-send-smtp-email-for-office365-with-python-using-tls-ssl
def send_ms_email(mail_to, subject, message):
    #get variables from .env file
    ms_tenant_id = ""
    if("ms_tenant_id" in os.environ):
        ms_tenant_id = os.environ["ms_tenant_id"]
    ms_app_id = ""
    if("ms_app_id" in os.environ):
        ms_app_id = os.environ["ms_app_id"]
    ms_app_secret_val = ""
    if("ms_app_secret_val" in os.environ):
        ms_app_secret_val = os.environ["ms_app_secret_val"]

    #define scope to ask for from users accaount
    scopes =  ["IMAP.AccessAsUser.All", "POP.AccessAsUser.All", "SMTP.Send", "Mail.Send", "offline_access"]

    credentials = (ms_app_id, ms_app_secret_val)
    #get a refresh token file if not existing
    if(os.path.isfile("./o365_token.txt") == False):
        account = Account(credentials=credentials)
        result = account.authenticate(scopes=scopes)  # request a token for this scopes

    #read refesh token file
    tk = FileSystemTokenBackend(token_path=".", token_filename="o365_token.txt")

    #connect to mail server
    account = Account(credentials, auth_flow_type = 'authorization',token_backend=tk)
    m = account.new_message() #create new message object
    for address in mail_to: #add all the to email addresses
        m.to.add(address)
    m.subject = subject
    m.body = message
    m.send() #send the mail

#create connection object based on type of connection desired
def create_connection(
    connection_type,
    socketpath=None,
    timeout=None,
    hostname=None,
    port=None,
    certfile=None,
    keyfile=None,
    cafile=None,
    ssh_username=None,
    ssh_password=None,
    auto_accept_host: Optional[bool] = None,
    **kwargs,  # pylint: disable=unused-argument
):
    if "socket" in connection_type: #create a unix socket connection
        return UnixSocketConnection(timeout=timeout, path=socketpath)

    if "tls" in connection_type: #create a tls connection
        return TLSConnection(
            timeout=timeout,
            hostname=hostname,
            port=port,
            certfile=certfile,
            keyfile=keyfile,
            cafile=cafile,
        )

    return SSHConnection( #create a ssh connection
        timeout=timeout,
        hostname=hostname,
        port=port,
        username=ssh_username,
        password=ssh_password,
        auto_accept_host=auto_accept_host,
    )

#choose a connection method toaccess the greenbone instance

#Define the greenbone socket connection
socket_file = "/run/gvmd/gvmd.sock"
if(os.path.isfile("/tmp/gvm/gvmd/gvmd.sock") == True):
    socket_file = "/tmp/gvm/gvmd/gvmd.sock"
connection = create_connection(connection_type="socket", socketpath=socket_file, timeout=DEFAULT_TIMEOUT)
#create a ssh connection
#connection = create_connection(connection_type="ssh", hostname="127.0.0.1", port="22", username="ssh-user", ssh_password="password", auto_accept_host=True, timeout=DEFAULT_TIMEOUT)

transform = EtreeTransform()
with GMP(connection, transform=transform) as gmp: #instantiate gmp object using connection details defined above
    # Retrieve GMP version supported by the greenbopne instance
    version = gmp.get_version()
    print("Greenbone v"+elem2dict(version)["version"])

    try:
        #Get the greenbone instance user credentials from .env file or ask user for them
        for x in range(0, 3, 1): #try authenticate 3 times
            if(not 'USERN' in os.environ):
                username = input("Enter username:")
            else:
                username = os.environ['USERN']

            if('PASS' not in os.environ):
                password = getpass.getpass("Enter password:") #dont show password on screen
            else:
                password = os.environ['PASS']

            authenticated = gmp.authenticate(username, password) #authenticate to the greenbone instance
            
            password = ""
            if(authenticated.attrib["status"] != '400'):
                break
            elif(x != 2):
                print("Authentication Failed, please try again")
            else:
                print("Authentication Failed, quiting")
                print(elem2dict(authenticated))
                sys.exit(2)

        target_dict = {}
        task_dict = {}
        for target in target_list:
            print("Target: "+target)
            # Check if target exists
            targets = elem2dict(gmp.get_targets(filter_string=target)) #get list of all the targets
            #process api response
            target_count = targets["target_count"]
            if("target" in targets):
                targets = targets["target"]
            else:
                targets = []
                target_count = "0"
            #fix api inconsistency
            if(type(targets) == dict):
                targets=[targets]

            # Find target in response
            found_target = False
            for x in targets:
                if(x["name"] == target):
                    found_target = True
                    target_id = x["id"]

            #add target if not existing
            if(found_target == False):
                #if adding a target, ask user for configuration, or use value from cli arguments
                if(has_port_argument == True):
                    port_list_id = get_options(gmp, "get_port_lists", port_argument) # Get id of a port list. Default is the All IANA assigned TCP list
                else:
                    port_list_id = get_options(gmp, "get_port_lists", False) # Get id of a port list. Default is the All IANA assigned TCP list
            
                #create a new target host on greenbone instance and get its id. Use a broad spectrum for get alive tests to capture targets that are more slippery
                new_target = gmp.create_target(name=target,hosts=[target],port_list_id=port_list_id, alive_test="ICMP, TCP-ACK Service & ARP Ping")
                if(new_target.attrib["status"] == "201"):
                    target_id = new_target.attrib["id"]
                    print("Added new target: "+target)
                else:
                    print("Unable to add target: "+target)
                    sys.exit(2)
            else:
                print("Found configured target: "+target)
            target_dict[target] = target_id

        
            # Retrieve all tasks
            tasks = elem2dict(gmp.get_tasks()) #get list of tasks on greenbone instance
            #process api response
            task_count = tasks["task_count"]
            if("task" in tasks):
                tasks = tasks["task"]
            else:
                tasks = []
                task_count = "0"
            #fix api inconsistency
            if(type(tasks) == dict):
                tasks=[tasks]

            #check if a task for the target already exists
            found_task = False
            for x in tasks:
                if(x["name"] == target):
                    found_task = True
                    task_id = x["id"]

            #add task if not existing
            if(found_task == False):
                #if adding a task, ask user for configuration, or use value from cli arguments
                if(has_config_argument == True):
                    config_list_id = get_options(gmp, "get_scan_configs", config_argument) # Get id of a config list. Default is the Fast and Full
                else:
                    config_list_id = get_options(gmp, "get_scan_configs", False) # Get id of a config list. Default is the Fast and Full

                if(has_scanner_argument == True):
                    scanner_id = get_options(gmp, "get_scanners", scanner_argument) # Get id of a config list. Default is the Full and fast
                else:
                    scanner_id = get_options(gmp, "get_scanners", False) # Get id of a config list. Default is the Full and fast

                #create new task for target host
                new_task = elem2dict(gmp.create_task(name=target,target_id=target_id,config_id=config_list_id,scanner_id=scanner_id))
                if(new_task["status"] == "201"):
                    task_id = new_task["id"]
                    print("Added new task: "+target)
                else:
                    print("Unable to add task: "+target)
                    sys.exit(2)            
            else:
                print("Found task: "+target)
            task_dict[target] = task_id

        for target in target_list:
            target_id = target_dict[target]
            task_id = task_dict[target]
            #check if task is already running
            report_id = False
            is_running = elem2dict(gmp.get_task(task_id=task_id))['task']
            if((skip_scan == False) or (is_running["status"] == "Running")):
                if(is_running["status"] == "Running"):
                    print("Task '"+is_running["name"]+"' is already running")
                    report_id = is_running["current_report"]["report"]["id"]
                else:
                    print("Running task '"+is_running["name"]+"'")
                    run_task = elem2dict(gmp.start_task(task_id=task_id)) #start a new scan
                    report_id = run_task['report_id']
                    is_running = elem2dict(gmp.get_task(task_id=task_id))['task'] #refresh task info to get latest flags
            else:
                if("last_report" in is_running):
                    report_id = is_running["last_report"]["report"]["id"]

            #Wait for scan to complete
            print("Waiting for scan to complete")
            err_count=0
            while is_running["in_use"] == "1": #loop while scan is in progress
                #Try to gracefully recover from errors 10 times (usually timeout errors)
                try:
                    is_running = elem2dict(gmp.get_task(task_id=task_id))['task'] #refresh task info to get latest flags
                    progres(float(is_running["progress"]), 100, status=is_running["status"], bar_len=40) #update the progress bar
                except Exception as err:
                    err_count = err_count+1 #increment error count
                    if(err_count < 10):
                        print(str(err))
                    else:
                        print("Error limit reached while checking scan status, closing script. This won't stop the scan itself, ")
                        print("it will however not catch the scan results in order to generate and mail a report unless this ")
                        print("script is latched! To latch on again, just run the command again.")
                        print(str(err))
                        sys.exit(2)
                if(is_running["in_use"] == "1"): #dont wait if we are done
                    time.sleep(20) #scans are quite slow, so wait 20seconds inbetween refreshes
            if(skip_scan == False):
                progres(100, 100, status="Scan complete", bar_len=40) #update the progress bar
                print() #Add in a new line
            else:
                print("Skipping Scan")
            

            if(report_id != False):
                print("Getting scan results")
                report_formats = elem2dict(gmp.get_report_formats(details=True)) #get id of available report formats from greenbone instance
                #process api response
                if("report_format" in report_formats):
                    report_formats = report_formats["report_format"]
                else:
                    report_formats = []
                #fix api inconsistency
                if(type(report_formats) == dict):
                    report_formats=[report_formats]

                #find the xml report format id
                found_report_formats = False
                for x in report_formats:
                    if(x["name"] == "XML"):
                        found_report_formats = True
                        report_format_id = x["id"]
                if(found_report_formats == False):
                    print("Report formats not found. Please ensure your GVM report formats are configured correctly")
                    sys.exit(2)

                #Get full report in the xml format from the greenbone instance, using the id from above
                report = elem2dict(gmp.get_report(
                    report_id=report_id,
                    report_format_id=report_format_id,
                    ignore_pagination=True,
                    details=True,
                ))
                #process api response
                if("report" in report):
                    report = report["report"]
                elif(report["status"] == "400"):
                    print("Unable to get report. Either the report does not contain any results or the necessary tools for creating the report are not installed.")
                    sys.exit(2)

                if not report:
                    print("Report is empty. Either the report does not contain any results or the necessary tools for creating the report are not installed.")
                    sys.exit(2)

                #generate html report
                ip_list = []
                print("Generating report")
                if("hosts" in report["report"] and int(report["report"]["hosts"]["count"]) > 0):
                    #Workout summary counts...different scans give inconsistent responses here
                    summary_headers = "<th>Host</th>"
                    hosts = report["report"]["host"]
                    #fix api inconsistency
                    if(type(hosts) == dict):
                        hosts=[hosts]
                    for host in hosts:
                        ip_list.append(host["ip"])
                        summary_rows = """<td>"""+report["task"]["name"]+""" ("""+host["ip"]+""")</td>"""
                        #fix api inconsistency in scan summary
                        if("result_count" in host and type(host["result_count"]) == dict):
                            wanted_headers = ["high", "medium", "low", "log", "false_positive"]
                            for x in wanted_headers:
                                if(x in host["result_count"]):
                                    summary_headers = summary_headers+"<th>"+x.capitalize()+"</th>"
                                    if("count" in host["result_count"][x]):
                                        summary_rows = summary_rows+"""<td>"""+host["result_count"][x]["count"]+"""</td>"""
                                    else:
                                        summary_rows = summary_rows+"""<td>"""+host["result_count"][x]["page"]+"""</td>"""
                        else:
                            summary_headers = summary_headers+"<th>Vulnerabilities</th>"
                            summary_rows = summary_rows+"""<td>"""+report["report"]["result_count"]+"""</td>"""

                    #initial section links
                    contents_html = """
                        <h4><a href="#overview">1. Result Overview</a></h4>
                        <h4><a href="#results">2. Results per Host</a></h4>"""
                    
                    #initial report sections
                    sections_html = """
                    <div id="overview" class="section">
                        <h2>1. Result Overview</h2>
                        <table>
                            <tr>
                                """+summary_headers+"""
                            </tr>
                            <tr>
                                """+summary_rows+"""
                            </tr>
                        </table>
                    </div>

                    <div id="results" class="section">
                        <h2>2. Results per Host</h2>
                        <table>
                            <tr>
                                <th>Host</th>
                                <th>Scan Start</th>
                                <th>Scan End</th>
                            </tr>
                            <tr>
                                <td>"""+report["task"]["name"]+""" ("""+', '.join(ip_list)+""")</td>
                                <td>"""+" ".join(report["report"]["scan_start"].split("T"))[:-1]+"""</td>
                                <td>"""+" ".join(report["report"]["scan_end"].split("T"))[:-1]+"""</td>
                            </tr>
                        </table>
                    </div>
                    """
                    

                    # Loop through result sections and build the html for them, sorting them into their respective severety levels
                    
                    #https://stackoverflow.com/questions/37094205/can-one-insert-a-collapsible-list-in-an-outlook-email
                    toggle_css = """
                        .close {
                            display: none;
                        }
                        """
                    high_sev_section = []
                    high_sev_contents = []
                    medium_sev_section = []
                    medium_sev_contents = []
                    low_sev_section = []
                    low_sev_contents = []
                    false_positive_section = []
                    false_positive_contents= []
                    log_sev_section = []
                    log_sev_contents = []

                    results = report["report"]["results"]["result"]
                    #fix api inconsitencies
                    if(type(results) == dict):
                        results = [results]
                    loop_index = 0
                    while loop_index < len(results):
                        current_result = results[loop_index] #data for current section
                        #workout section number
                        subsection = 0
                        current_count = 0
                        if(current_result["threat"] == "High"):
                            subsection = 1
                            current_count = len(high_sev_section)+1
                        elif(current_result["threat"] == "Medium"):
                            subsection = 2
                            current_count = len(medium_sev_section)+1
                        elif(current_result["threat"] == "Low"):
                            subsection = 3
                            current_count = len(low_sev_section)+1
                        elif(current_result["threat"] == "Log"):
                            subsection = 4
                            current_count = len(log_sev_section)+1
                        elif(current_result["threat"] == "False"):
                            subsection = 5
                            current_count = len(false_positive_section)+1

                        unique_section_id = "section2_"+str(subsection)+"_"+str(current_count) #unique section id for section linking


                        #Add section to summary table
                        new_content = """<a href="#"""+unique_section_id+"\">2."+str(subsection)+"."+str(current_count)+" "+current_result["name"]+" ("+current_result["host"]+": "+current_result["port"]+")"+"""</a>"""

                        #Process result information
                        tags = current_result["nvt"]["tags"].split("|") #get nvt tag values
                        tag_loop_index = 0
                        result_details = {}
                        while tag_loop_index < len(tags): #loop through nvt tag values and add them to a dict
                            current_row = tags[tag_loop_index].split("=")
                            result_details[current_row[0]] = current_row[1].replace("\n", "</br>")
                            tag_loop_index += 1

                        #process vulnerability references
                        ref_loop_index = 0
                        references = ""
                        if("refs" in current_result["nvt"]):
                            #fix api inconsistency
                            if(type(current_result["nvt"]["refs"]["ref"]) == dict):
                                current_result["nvt"]["refs"]["ref"] = [current_result["nvt"]["refs"]["ref"]]
                            #loop through referances and add them to referances html string
                            while ref_loop_index < len(current_result["nvt"]["refs"]["ref"]):
                                current_row = current_result["nvt"]["refs"]["ref"][ref_loop_index]
                                #fix api inconsistencies
                                key = ""
                                valkey = ""
                                if("_type" in current_row):
                                    key="_type"
                                    valkey="_id"
                                elif("type" in current_row):
                                    key="type"
                                    valkey="id"
                                if(current_row[key] == "url"):
                                    references = references+"<a href='"+current_row[valkey]+"'>"+current_row[valkey]+"</a></br>"
                                else:
                                    references = references+current_row[valkey]+"</br>"
                                ref_loop_index += 1

                        #build section
                        current_section = """
                            <div id='"""+unique_section_id+"""' class="section">
                                <h4>
                                    2."""+str(subsection)+"."+str(current_count)+" "+current_result["threat"]+": "+current_result["name"]+" ("+current_result["host"]+": "+current_result["port"]+")"+"""
                                </h4>
                                <table>
                                    <tr>
                                        <th>
                                            """+current_result["threat"]+"""(CVSS: """+current_result["nvt"]["cvss_base"]+""")
                                            </br>NVT: """+current_result["nvt"]["name"]+"""
                                        <th>
                                    </tr>
                                    <tr>
                                        <td>
                                            <b>Summary</b>
                                            </br>"""+result_details["summary"]+"""
                                        <td>
                                    </tr>
                                    <tr>
                                        <td>
                                            <b>Details</b>
                                            </br>"""+current_result["description"]+"""
                                        <td>
                                    </tr>
                                    <tr>
                                        <td>
                                            <b>Quality of Detection (QoD)</b>: """+current_result["qod"]["value"]+"""%
                                        <td>
                                    </tr>
                                    <tr>
                                        <td>
                                            <b>Insight</b>
                                            </br>"""+result_details["insight"]+"""
                                        <td>
                                    </tr>
                                    <tr>
                                        <td>
                                            <b>Solution ("""+result_details["solution_type"]+""")</b>
                                            </br>"""+result_details["solution"]+"""
                                        <td>
                                    </tr>
                                    <tr>
                                        <td>
                                            <b>More information: </b>
                                            """+("</br>For more information, check the following URLs:" if references != "" else "N/A")+"""
                                            """+references+"""
                                        <td>
                                    </tr>
                                <table>
                            </div>"""
                        #add built sections and content links to the appropriate category
                        if(current_result["threat"] == "High"):
                            high_sev_section.append(current_section)
                            high_sev_contents.append(new_content)
                        elif(current_result["threat"] == "Medium"):
                            medium_sev_section.append(current_section)
                            medium_sev_contents.append(new_content)
                        elif(current_result["threat"] == "Low"):
                            low_sev_section.append(current_section)
                            low_sev_contents.append(new_content)
                        elif(current_result["threat"] == "Log"):
                            log_sev_section.append(current_section)
                            log_sev_contents.append(new_content)
                        elif(current_result["threat"] == "False"):
                            false_positive_section.append(current_section)
                            false_positive_contents.append(new_content)
                        loop_index += 1

                    #add built sections and content links to html
                    #add High Threats
                    sections_html = sections_html+"""<h3>2.1 High Threats</h3>"""+(''.join(high_sev_section))
                    contents_html = contents_html+"""<h4>2.1 High Threats</h4>"""+(''.join(high_sev_contents))

                    #add Medium Threats
                    sections_html = sections_html+"""<h3>2.2 Medium Threats</h3>"""+(''.join(medium_sev_section))
                    contents_html = contents_html+"""<h4>2.2 Medium Threats</h4>"""+(''.join(medium_sev_contents))

                    #add Low Threats
                    sections_html = sections_html+"""<h3>2.3 Low Threats</h3>"""+(''.join(low_sev_section))
                    contents_html = contents_html+"""<h4>2.3 Low Threats</h4>"""+(''.join(low_sev_contents))

                    #add Log Threats
                    sections_html = sections_html+"""<h3>2.4 Log Threats</h3>"""+(''.join(log_sev_section))
                    contents_html = contents_html+"""<h4>2.4 Log Threats</h4>"""+(''.join(log_sev_contents))

                    #add False Positives
                    sections_html = sections_html+"""<h3>2.5 False Positives</h3>"""+(''.join(false_positive_section))
                    contents_html = contents_html+"""<h4>2.5 False Positives</h4>"""+(''.join(false_positive_contents))

                    
                    #build main html output
                    html_out = """<!DOCTYPE html>
                    <html lang="en">
                        <head>
                            <meta charset="UTF-8">
                            <meta name="viewport" content="width=device-width, initial-scale=1.0">
                            <title>Security Scan Report: """+report["task"]["name"]+"""</title>
                            <style>
                                body {
                                    font-family: Arial, sans-serif;
                                    margin: 20px;
                                    padding: 20px;
                                    background-color: #f5f5f5;
                                    color: #333;
                                }
                                h1, h2, h3, h4 {
                                    color: #004080;
                                }
                                .container {
                                    background-color: #ffffff;
                                    padding: 20px;
                                    border-radius: 8px;
                                    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
                                }
                                .toc {
                                    background-color: #e6f2ff;
                                    padding: 10px;
                                    border-radius: 5px;
                                }
                                .toc a {
                                    color: #004080;
                                    text-decoration: none;
                                    display: block;
                                    padding: 5px 0;
                                }
                                table {
                                    width: 100%;
                                    border-collapse: collapse;
                                    margin-top: 20px;
                                }
                                th, td {
                                    border: 1px solid #004080;
                                    padding: 10px;
                                    text-align: left;
                                }
                                th {
                                    background-color: #e6f2ff;
                                }
                                """+toggle_css+"""
                            </style>
                        </head>
                        <body>
                            <div class="container">
                                <h1>Security Scan Report</h1>
                                <p><strong>Date: </strong>"""+report["creation_time"].split("T")[0]+"""</p>
                                <p>This document reports on the results of an automatic security scan. All dates are displayed 
                                    using the timezone "Coordinated Universal Time", which is abbreviated "UTC". The
                                    task was <b>'"""+report["task"]["name"]+"""'</b>. The scan started at <b>"""+" ".join(report["report"]["scan_start"].split("T"))[:-1]+""" UTC</b> and ended
                                    at <b>"""+" ".join(report["report"]["scan_end"].split("T"))[:-1]+""" UTC</b>. The report first summarises the results found. Then, for
                                    each host, the report describes every issue found. Please consider the advice given in each
                                    description, in order to rectify the issue.
                                </p>
                                
                                <h2>Contents</h2>
                                <div class="toc">
                                    """+contents_html+"""
                                </div>
                                
                                """+sections_html+"""
                        </body>
                    </html>"""
                    
                    #write report html to file for debugging
                    #f = open("scan_results.html", "w")
                    #f.write(html_out)
                    #f.close()

                    print("Done!")

                    # if(len(mail_to) > 0 and mail_configured == True):
                    #     ex = send_email(host=mail_host, port=mail_port, user=mail_user, pwd=mail_pass, mail_from=mail_from, recipients=mail_to, subject=report["task"]["name"]+" Security Scan", body=html_out, html=html_out)
                    #     if ex: 
                    #         print("Report sending failed: %s" % ex)
                    #     else:
                    #         print("Report sent")
                    # else:
                    #     print("Mail not configured or no addresses to send to")

                    #send ms outlook email
                    if(len(mail_to) > 0):
                        result = send_ms_email(mail_to=mail_to, subject=report["task"]["name"]+" Security Scan", message=html_out)
                        print("Report sent!")

                    #save data to supabase
                    if(save_to_supabase == True):
                        supabase_url = ""
                        if("SUPABASE_URL" in os.environ):
                            supabase_url = os.environ["SUPABASE_URL"]
                        supabase_pass = ""
                        if("SUPABASE_KEY" in os.environ):
                            supabase_url = os.environ["SUPABASE_KEY"]
                        supabase: Client = create_client(supabase_url, supabase_url)
                        insert_data = {
                            "created_at": " ".join(report["report"]["scan_start"].split("T"))[:-1],
                            "type": 'greenbone',
                            "command": " ".join(sys.argv),
                            "start_at": " ".join(report["report"]["scan_start"].split("T"))[:-1],
                            "end_at": " ".join(report["report"]["scan_end"].split("T"))[:-1],
                            "results": html_out, 
                            "to_start": False, 
                            "intervals": False,
                            "result_summary": False
                        }
                        response = (supabase.table("scan_que").insert(insert_data).execute())
                else:
                    print("Target host "+target+" not reachable! No results to report on. Perhaps try changing the targets alive test setting on greenbone")
            else:
                print("Unable to find a report. Please run a scan or check on greenbone for an issue")
    except GvmError as e:
        print("An error occurred", e)