#!/bin/bash

# =============================================================================
# launch_script Scope
# =============================================================================
# 1. Check the requirements needed to run NodeZero and generated warnings or errors if those checks fail. 
# 2. Clean up previous NodeZero artifacts that may be left behind from previous runs.
# 3. Run independently as a check script on a NodeZero host system
# =============================================================================

# Check if interactive shell is available
USE_INTERACTIVE_SHELL=false
if [[ $- == *i* ]]; then
    USE_INTERACTIVE_SHELL=true
fi





# Formatting Variables
if [[ $USE_INTERACTIVE_SHELL == true ]]; then
    BOLD=$(tput bold)
    NORMAL=$(tput sgr0)
    RED=$(tput setaf 1)
    GREEN=$(tput setaf 2)
    YELLOW=$(tput setaf 3)
    MAGENTA=$(tput setaf 5)
    CYAN=$(tput setaf 6)
    FANCY=$(echo -e "cuu1\nel" | tput -S)
else
    BOLD=""
    NORMAL=""
    RED=""
    GREEN=""
    YELLOW=""
    MAGENTA=""
    CYAN=""
    FANCY=""
fi

# Global Variables
if [[ $SUDO_REQ == YES ]]; then
    DOCKER="sudo docker"
else
    DOCKER=docker
fi
UNAMES=$(uname -s)
UNAMER=$(uname -r)

PROXY_CONFIGURED=false
docker_proxy_config="$HOME/.docker/config.json"
# Check for proxy settings in default location ~/.docker/config.json

if [ -f $docker_proxy_config ]; then
    if [ $(cat $docker_proxy_config | grep -io '}' | wc -l) -ge 2  ]; then

        # Check if proxy is set in default config
        if [ $(grep "httpsProxy" $docker_proxy_config | wc -l) -eq 1 ]; then

            # Find proxy and extract value
            proxy=`grep -o 'httpsProxy"\: "[^"]*' $docker_proxy_config | sed "s/.*\"//g"`
            if [ ! -z "$proxy" ]; then
                gateway_proxy_config='"proxies": {
                    "default": {
                        "httpProxy": "'${proxy}'",
                        "httpsProxy": "'${proxy}'",
                        "noProxy": "localhost,127.0.0.1,::1,172.16.0.0/16,10.0.0.0/8,192.168.0.0/16"
                    }
                }'
            fi
            # Proxy is empty
        fi
        # did not find "httpsProxy" in file
    fi
    # File does not have json content, or is empty
fi
# No ~/.docker/config.json file found

unset DOCKER_CONFIG_PATH
DOCKER_CONFIG_PATH=$(mktemp -d -t docker-XXXX)



REGISTRY_URL="gateway.horizon3ai.com"
REPOSITORY="docker/library"
IMAGE="$REGISTRY_URL/$REPOSITORY/hello-world:latest"
# If consolidated endpoint is NOT configured, use default time API for now
TIME_API="https://api.horizon3ai.com/v1/time"

# Proxy not found or empty
if [ -z "${gateway_proxy_config}" ]; then
    cat > $DOCKER_CONFIG_PATH/config.json << EOF
{
    "HttpHeaders": {
        "X-Meta-Docker": "True"
    },
    "auths": {
        "gateway.horizon3ai.com": {}
    }
}
EOF

# Proxy was found
else
    cat > $DOCKER_CONFIG_PATH/config.json << EOF
{
    "HttpHeaders": {
        "X-Meta-Docker": "True"
    },
    "auths": {
        "gateway.horizon3ai.com": {}
    },
    $gateway_proxy_config
}
EOF
fi



DOCKER_RUN="$DOCKER --config $DOCKER_CONFIG_PATH"

# Create utility functions
function HeaderMsg {
    echo -e "\n${BOLD}[${CYAN}#${NORMAL}${BOLD}] ${CYAN}$@${NORMAL}"
}


function PassMsg {
    echo -e "${BOLD}[${GREEN}+${NORMAL}${BOLD}] ${NORMAL}${GREEN}PASSED: $@${NORMAL}"
}


function GenMsg {
    echo -e "${BOLD}[${GREEN}+${NORMAL}${BOLD}]${NORMAL}${GREEN} $@${NORMAL}"
}


function FailMsg {
    echo -e "${BOLD}[${RED}!${NORMAL}${BOLD}] ${NORMAL}${RED}FAILED: $@${NORMAL}"
}


function WarnMsg {
    echo -e "${BOLD}[${YELLOW}!${NORMAL}${BOLD}] ${NORMAL}${YELLOW}WARNING: $@${NORMAL}"
}


function AskMsg {
    read -p "${BOLD}[${MAGENTA}-${NORMAL}${BOLD}] ${NORMAL}${MAGENTA}CONFIRM: $@ (y|n) ${NORMAL}" -n 1 -r </dev/tty
}


function ExitMsg {
    echo -e "\n${BOLD}[${CYAN}#${NORMAL}${BOLD}] ${CYAN}$@${NORMAL}"
}


# Check if a proxy is configured and setup
function ProxyChk {
    HeaderMsg "Checking Proxy settings and configurations:"

    DOCKER_CONFIG=false
    DOCKER_DAEMON=false
    DOCKER_SERVICE_PROXY=false
    ENV_VARS=false
    ENV_PROXIES=false
    PROXY_CONFIGURED=false

    # Check $HOME/.docker/config.json or $DOCKER_CONFIG_PATH/config.json
    if [ -f ${DOCKER_CONFIG_PATH}/config.json ]; then

        # Check content of file for proxy setting
        docker_config_path_proxies=`sudo cat ${DOCKER_CONFIG_PATH}/config.json | grep -io proxy | wc -l`
        GenMsg "Found ${docker_config_path_proxies} of 3 proxy settings in ${DOCKER_CONFIG_PATH}/config.json"
        if [ $docker_config_path_proxies -ge 3 ]; then
            DOCKER_CONFIG=true
        fi
    fi

    # check /etc/docker/daemon.json
    if [ -f /etc/docker/daemon.json ]; then

        # Check content of file for proxy setting
        docker_daemon_proxies=`sudo cat /etc/docker/daemon.json | grep -io proxy | wc -l`
        GenMsg "Found ${docker_daemon_proxies} of 3 proxy settings in /etc/docker/daemon.json"
        if [ $docker_daemon_proxies -ge 3 ]; then
            DOCKER_DAEMON=true
        fi
    fi

    # check /etc/systemd/system/docker.service.d/http-proxy.conf
    if sudo [ -f /etc/systemd/system/docker.service.d/http-proxy.conf ]; then

        # Check content of file for proxy setting
        docker_service_proxies=`sudo cat /etc/systemd/system/docker.service.d/http-proxy.conf | grep -io proxy | wc -l`
        GenMsg "Found ${docker_service_proxies} of 6 proxy settings in /etc/systemd/system/docker.service.d/http-proxy.conf"
        if [ $docker_service_proxies -ge 6 ]; then
            DOCKER_SERVICE_PROXY=true
        fi
    fi

    # Check /etc/environment
    if [ -f /etc/environment ]; then

        # Check content of file for proxy setting
        env_proxies=`sudo cat /etc/environment | grep -ic proxy`
        GenMsg "Found ${env_proxies} of 6 proxy settings in /etc/environment"
        if [ $env_proxies -ge 6 ]; then
            ENV_PROXIES=true
        fi
    fi

    # Check if shell environment has http_proxy is set
    env_shell_proxies=`env | grep -ic 'http_proxy\|https_proxy\|no_proxy'`
    GenMsg "Found ${env_shell_proxies} of 6 proxy settings in env"
    if [ $env_shell_proxies -ge 6 ]; then
        ENV_VARS=true
    fi

    if [ "$DOCKER_CONFIG" = true ] && [ "$DOCKER_DAEMON" = true ] && [ "$DOCKER_SERVICE_PROXY" = true ] && [ "$ENV_PROXIES" = true ] && [ "$ENV_VARS" = true ]; then
        PROXY_CONFIGURED=true
        PassMsg "Proxy is detected"
    elif [ "$DOCKER_CONFIG" = false ] && [ "$DOCKER_DAEMON" = false ] && [ "$DOCKER_SERVICE_PROXY" = false ] && [ "$ENV_PROXIES" = false ] && [ "$ENV_VARS" = false ]; then
        PROXY_CONFIGURED=false
        PassMsg "Proxy is NOT configured"
    else
        # Dont exit, there could be multiple proxies or commented out etc...
        FailMsg "Proxy is not configured correctly, Please refer to https://docs.horizon3.ai/proxy-setup for guidance on setting a proxy"
    fi
}


# Validate supported OS is being used and set OS specific variables
function OsChk {

    HeaderMsg "Checking Operating System:"

    case $UNAMES in
        Linux*)
            OS="Linux"
            PassMsg "$OS is a supported Operating System."

            ArchChk

            HeaderMsg "Gathering environmental variables to conduct further checks:"
            if [[ $UNAMER =~ microsoft ]]; then
                FREE_SPACE_KB=$(df -Pk /init | awk 'NR==2 { print $4 }')
            else
                DOCKER_PATH=$($DOCKER info 2>/dev/null| grep -i "Docker Root Dir" | sed 's/Docker Root Dir:\ //')
                FREE_SPACE_KB=$(df -Pk $DOCKER_PATH | awk 'NR==2 { print $4 }')
            fi
            SPACE_DOC="https://docs.docker.com/config/pruning/"
            FS_DOC="https://docs.docker.com/docker-for-windows/#file-sharing"
            MEM_B=$(free -b | awk 'NR==2 { print $2 }')
            CPU=$(grep -c ^processor /proc/cpuinfo)
            PassMsg "All environmental variables set and proceeding with next checks."

            TimeChk
            ;;
        Darwin*)
            OS="macOS"
            PassMsg "$OS is a supported Operating System."

            ArchChk

            HeaderMsg "Gathering environmental variables to conduct further checks:"
            DOCKER_RAW="$HOME/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw"
            if [[ ! -f $DOCKER_RAW ]]; then
                WarnMsg "A Docker configuration file is not in it's default location, an attempt to find it will be made that may ask for access to folders. If disallowed to search, the disk size validation check will not complete and report as a failed check."
                DOCKER_RAW=$(find $HOME/Library/Containers -name Docker.raw 2>/dev/null | grep .)
                EXIT=$?
                if [[ $EXIT == 1 ]]; then
                    FailMsg "Unable to locate the Docker.raw file to validate enough HDD space is available. Please ensure Docker is installed properly and retry. See https://docs.docker.com/docker-for-mac/install/ for more information on how to install Docker Desktop for macOS."
                    exit 1
                fi
            fi
            USED_SPACE_KB=$(ls -ksn $DOCKER_RAW | awk '{ print $1 }')
            MAX_SPACE_KB=$(( $(ls -ksn $DOCKER_RAW | awk '{ print $6 }') / 1024 ))
            FREE_SPACE_KB=$(( $MAX_SPACE_KB - $USED_SPACE_KB ))
            SPACE_DOC="https://docs.docker.com/docker-for-mac/space/"
            FS_DOC="https://docs.docker.com/docker-for-mac/#file-sharing"

            MEM_B=$(sysctl -a | grep hw.memsize_usable | awk 'NR==1{ print $2 }')
            if [[ -z "$MEM_B" ]]; then
                MEM_B=$(sysctl -a | grep hw.mem | awk 'NR==1{ print $2 }')
            fi
            CPU=$(sysctl -a | grep hw.logicalcpu_max | awk '{ print $2 }')
            if [[ -z "$MEM_B" ]]; then
                FailMsg "Unable to determine system properties. Please ensure sysctl can be found on the system PATH."
                exit 1
            fi
            PassMsg "All environmental variables set and proceeding with next checks."

            TimeChk
            ;;
        *)
            FailMsg "$UNAMES is not a currently supported Operating System. Please provide feedback to add this Operating System."
            exit 1
            ;;
    esac
}


# Check time against UTC for max of 5 minutes of drift
# Test time drift by disabling ntp and adjusting time using:
# timedatectl set-ntp off
# date -s "now - 12 minutes"
function TimeChk {

    HeaderMsg "Checking host time against current UTC time:"

    TIME=$(curl -k -s -m 3 $TIME_API)
    EXIT=$?
    if [[ $EXIT != 0 ]]; then
        sleep 30
        TIME=$(curl -k -s -m 3 $TIME_API)
    	EXIT=$?
    	if [[ $EXIT != 0 ]]; then
            WarnMsg "Unable to determine current UTC time to validate time is within a 5 minute variance. The URL $TIME_API was unreachable."
            return 0
        fi
	fi

    VALID_TIME=$(echo "$TIME" | grep EpochTime)
    EXIT=$?
    if [[ $EXIT != 0 ]]; then
        WarnMsg "Unable to determine current UTC time to validate time is within a 5 minute variance. The URL $TIME_API did not return a valid timestamp."
        return 0
    fi

    declare -i UTC=$(echo "$TIME" | sed -n 's/.*"EpochTime"\s*:\s*"\([0-9]\{10\}\).*/\1/p')
    declare -i LOCAL=$(date +%s)

    ABSDIFF=$(( UTC > LOCAL ? UTC - LOCAL : LOCAL - UTC ))
    if [[ $ABSDIFF -le 300 ]]; then
        PassMsg "System time is within 5 minutes of UTC time."
    else
        FailMsg "The system time is off by more than 5 minutes from UTC time. Please set or resync your system time before running again. For additional information, see: (https://docs.horizon3.ai/t/time-oos)"
        exit 1
    fi
}


# Check if you can run the docker command
function DockerChk {

    HeaderMsg "Checking Docker functionality by running the hello-world test container:"

    CleanUp

    declare -i DOCKER_VERSION=$(docker -v | awk '{ print $3 }' | awk -F '.' '{ print $1 }')
    if [[ $DOCKER_VERSION -ge 20 ]]; then
        PassMsg "Docker version installed meets the minimum required version 20.10."
    else
        FailMsg "Docker major version $DOCKER_VERSION is unsupported, please upgrade Docker to the minimum required version 20.10."
        exit 1
    fi

    local retry_count=0
    local max_retries=5
    local EXIT=0

    $DOCKER_RUN run --rm --name h3-test $IMAGE &> /dev/null
    EXIT=$?

    while [[ $EXIT -eq 125 && $retry_count -lt $max_retries ]]; do
        FailMsg "Failed to validate Docker. Retrying Docker check (attempt $retry_count)..."

        retry_count=$((retry_count + 1))
        sleep 2  # Wait for 2 second before retrying

        $DOCKER_RUN run --rm --name h3-test $IMAGE &> /dev/null
        EXIT=$?
    done

    case $EXIT in
        0*)
            PassMsg "Docker is installed and functioning properly."
            PermChk
            ;;
        125*)
            FailMsg "Failed to validate Docker. Verify the Docker daemon is started and retry."
            return 1
            ;;
        126*)
            
            SudoChk
            
            ;;
        127*)
            FailMsg "Failed to validate Docker. Verify that Docker is installed and retry."
            return 1
            ;;
        *)
            FailMsg "Failed to validate Docker. Please ensure Docker is installed and this user has permission to execute."
            return 1
            ;;
    esac

    CleanUp
}


# Retry Docker check with "docker hub sourced hello-world" image if the initial check fails
function RetryDockerChk {

    HeaderMsg "Retrying Docker functionality check with docker hub sourced hello-world image:"

    # Deprecation message
    WarnMsg "Deprecation Notice: Docker Hub (docker.io) sourced images will no longer be available after the May 2025 release."
    WarnMsg "Please update your network allowlist configuration per https://docs.horizon3.ai/quickstart/network_requirements/ before May 2025."

    REGISTRY_URL="docker.io"
    REPOSITORY="library"
    IMAGE="$REGISTRY_URL/$REPOSITORY/hello-world:latest"

    $DOCKER_RUN run --rm --name h3-test $IMAGE &> /dev/null
    EXIT=$?

    case $EXIT in
        0*)
            PassMsg "Docker is installed and functioning properly with the hello-world image."
            PermChk
            ;;
        125*)
            FailMsg "Failed to validate Docker with the hello-world image. Verify the Docker daemon is started and retry."
            exit 1
            ;;
        126*)
            
            SudoChk
            
            ;;
        127*)
            FailMsg "Failed to validate Docker with the hello-world image. Verify that Docker is installed and retry."
            exit 1
            ;;
        *)
            FailMsg "Failed to validate Docker with the hello-world image. Please ensure Docker is installed and this user has permission to execute."
            exit 1
            ;;
    esac

    CleanUp

}


function DockerProxyCheck {

    if [ "$PROXY_CONFIGURED" = true ]; then
        HeaderMsg "Checking Docker proxy settings:"

        local retry_count=0
        local max_retries=5
        local EXIT=0

        container_proxies=`$DOCKER_RUN run --rm $REGISTRY_URL/$REPOSITORY/alpine sh -c 'env | grep -ic proxy'`
        EXIT=$?

        while [[ $EXIT -eq 125 && $retry_count -lt $max_retries ]]; do
            FailMsg "Failed to validate Docker, Retrying Docker check (attempt $retry_count)..."

            retry_count=$((retry_count + 1))
            sleep 2  # Wait for 2 second before retrying

            container_proxies=`$DOCKER_RUN run --rm $REGISTRY_URL/$REPOSITORY/alpine sh -c 'env | grep -ic proxy'`
            EXIT=$?
        done

        if [ $retry_count -eq $max_retries ]; then
            FailMsg "Failed to validate Docker. Verify the Docker daemon is started and retry."
            exit 1
        fi

        GenMsg "Found ${container_proxies} of 6 proxy settings in env"
        AlpineCleanUp
        if [ $container_proxies -eq 6 ]; then
            PassMsg "Container has proxy configured"
        else
            FailMsg "Proxy environment variables not found in container"
            exit 1
        fi
    fi
}


# Check permissions for docker to mount the read-only Op Config
function PermChk {

    HeaderMsg "Checking Docker permissions to volume mount files from $PWD directory:"

    mkdir -p h3.txt &> /dev/null
    $DOCKER_RUN run --rm --name h3-test -v $PWD/h3.txt:/h3.txt $IMAGE &> /dev/null
    EXIT=$?
    rmdir h3.txt &> /dev/null

    case $EXIT in
        0*)
            PassMsg "Docker permissions are correct for the $PWD directory location."
            CleanUp
            ;;
        125*)
            FailMsg "Failed to validate Docker. The path $PWD is not shared and is not known to Docker. Retry from a directory path that allows Docker to mount files from $PWD. See $FS_DOC for more information on how to allow Docker access to a system directory."
            exit 1
            ;;
        *)
            FailMsg "Failed to validate Docker. Please ensure Docker is installed and this user has permission to execute."
            exit 1
            ;;
    esac

    CleanUp
}


# Check if sudo is required to run Docker commands
function SudoChk {

    WarnMsg "Unable to validate Docker due to insufficient privileges."
    AskMsg "Would you like to retry using sudo privileges?"

    echo
    if [[ $REPLY =~ ^[Yy]$ ]]; then

        HeaderMsg "Attempting to validate Docker using sudo privileges:"

        # Check that sudo is used
        if echo "$DOCKER_RUN" | grep -q 'sudo'; then
            SUDO_DOCKER_RUN=$DOCKER_RUN
        else
            SUDO_DOCKER_RUN="sudo $DOCKER_RUN"
        fi

        local retry_count=0
        local max_retries=5
        local EXIT=0

        $SUDO_DOCKER_RUN run --rm --name h3-test $IMAGE &> /dev/null
        EXIT=$?

        while [[ $EXIT -eq 125 && $retry_count -lt $max_retries ]]; do
            FailMsg "Failed to validate Docker. Retrying Docker check (attempt $retry_count)..."

            retry_count=$((retry_count + 1))
            sleep 2  # Wait for 2 second before retrying

            $SUDO_DOCKER_RUN run --rm --name h3-test $IMAGE &> /dev/null
            EXIT=$?
        done

        case $EXIT in
            0*)
                WarnMsg "Check completed successfully, sudo will be used to run all Docker commands with this user."
                SUDO_REQ="YES"
                PermChk
                ;;
            1*)
                FailMsg "Failed to validate Docker. Verify this account is in the sudoers file and retry."
                exit 1
                ;;
            126*)
                FailMsg "Failed to validate Docker. Verify this account has permissions to run Docker and retry."
                exit 1
                ;;
            *)
                FailMsg "Failed to validate Docker. Please ensure Docker is installed and this user has permission to execute."
                exit 1
                ;;
        esac

        CleanUp
    else
        CleanUp
        ExitMsg "Exiting without validating Docker, please allow this user Docker privileges and retry."
        exit 1
    fi
}


# Check if disk space has room; $FREE_SPACE_KB is set in OsChk().
function DiskChk {

    HeaderMsg "Checking HDD space requirements (minimum 30GB Recommended, 20GB Required):"

    # IMG_SIZE_KB is to account for NodeZero image size if provided.
    if [[ $IMG_SIZE_KB -gt 0 ]]; then
        FREE_SPACE_KB=$(( $FREE_SPACE_KB + $IMG_SIZE_KB ))
    fi

    FREE_SPACE_GB="$(( $FREE_SPACE_KB / 1024 / 1024 ))GB"
    if [[ $FREE_SPACE_KB -ge 31457280 ]]; then
        PassMsg "There is enough space for the NodeZero container: $FREE_SPACE_GB"
    elif [[ $FREE_SPACE_KB -le 20971520 ]]; then
        FailMsg "$FREE_SPACE_GB is not enough space to support NodeZero. Remove unused containers and volumes to reclaim space and retry. See $SPACE_DOC for more information on how to remove unused images or containers to reclaim space."  # Only exit with checkscript, for launch_script allow the script to continue
        exit 1
        
    else
        WarnMsg "$FREE_SPACE_GB is less than the recommended 30GB free space on this disk, please ensure to prune old images before running Node Zero again."
    fi
}


# Check if enough memory; $MEM_B variable should be in bytes
function MemChk {

    HeaderMsg "Checking 8GB RAM requirement:"

    MEM_GB="$(( $MEM_B / 1024 / 1024 / 1024 ))GB"
    # Leave a little buffer for kernel reservations
    if [[ $MEM_B -ge 8053063680 ]]; then
        PassMsg "This system meets the recommended minimum RAM to support NodeZero."
    else
        WarnMsg "It is recommended to have a minimum of 8GB RAM to run NodeZero. This system has $MEM_GB RAM and you may experience sluggish NodeZero performance."
    fi
}


# Check if enough compute
function CpuChk {

    HeaderMsg "Checking compute resource requirements:"

    if [[ $CPU -ge 2 ]]; then
        PassMsg "This system has $CPU CPUs which meets the minimum logical CPU requirements to run NodeZero."
    else
        WarnMsg "It is recommended to have a minimum of 2 logical CPUs to run NodeZero. This system has $CPU CPUs and you may experience sluggish NodeZero performance."
    fi
}


# Check if CPU architecture is supported (x86_64/amd64 only)
function ArchChk {

    HeaderMsg "Checking CPU architecture compatibility:"

    ARCH=$(uname -m)

    case $ARCH in
        x86_64|amd64)
            PassMsg "CPU architecture '$ARCH' is supported by NodeZero."
            ;;
        arm64|aarch64|armv*)
            FailMsg "CPU architecture '$ARCH' is not supported. NodeZero requires an x86_64 (amd64) processor. ARM-based systems (including Apple Silicon Macs and ARM servers) are not supported."
            exit 1
            ;;
        *)
            FailMsg "Unknown CPU architecture '$ARCH'. NodeZero requires an x86_64 (amd64) processor."
            exit 1
            ;;
    esac
}


function EdrDefinitions {
    # Get list of common EDR processes to check for
    # CrowdStrike Falcon (falcon)
    cs_falcon="csfalconservice|csagent|falcon"

    # Microsoft Defender for Endpoint (mdatp)
    ms_defender="msmpeng|mssense|nissrv|sensendr|mdatp"

    # SentinelOne Singularity (sentinel)
    sentinel="sentinelone|sentineld|sentinelctl"

    # Palo Alto Networks Cortex XDR (pmd|dypd|traps)
    pa_cortex="pmd|dypd|traps|cortex|cyveraservice|cyserver"

    # Sophos Intercept X (sophos)
    sophos="savservice|sophoshealth|sophossps|sophosfilescanner|sophosclean|sophososquery"

    # Elastic Security (elastic)
    elastic="elastic-endpoint|elastic-agent"

    # VMware Carbon Black (cbagent)
    vm_cb="cbdaemon|cbagent|cbdefense|repmgr|rtvscand"

    # Symantec/Broadcom
    sb="ccsvchst|smc|symcorpui"

    # Cisco Secure Endpoint (amp)
    cisco_se="ampcli|ampscansvc|ampdaemon"

    # Cybereason (cybereason)
    cr="cybereason|minionhost|crsensor"

    # Cylance/ArcticWolf (cylance)
    aw="arcticwolfagent|arcticwolfdesktop|cylancesvc|wazuh"

    # McAfee/Trellix/FireEye
    mcafee="mfetpd|mfetp|mfemactl|isectpd|mvision|mvedr|mvedrcontrol|xagt"

    # Trend Micro/Apex One
    tm="tmntsrv|tmlisten|ds_agent"

    # Bitdefender
    bitdefender="bdagent|vsserv|bdservice|bdsec|bitdefender-security-tools"

    # Fortinet
    fortinet="fortiedr|fortiesnac"

    # Malwarebytes
    malwarebytes="mbamservice|mbam"

    # Tanium
    tanium="taniumclient"

    # Rapid7
    rapid7="ir_agent"

    echo "$cs_falcon|$ms_defender|$sentinel|$pa_cortex|$sophos|$elastic|$vm_cb|$cisco_se|$cr|$aw|$sb|$mcafee|$tm|$bitdefender|$fortinet|$malwarebytes|$tanium|$rapid7"

}

# Checks if an EDR is running on the NodeZero host that may interfere with NodeZero operations.
function EdrChk {

    local EDR_DEFINITIONS=$(EdrDefinitions)

    # EDR_PROCESSES=$(ps aux | grep -i "$EDR_DEFINITIONS" | awk '{print $2, $11}' | grep -v 'grep')
    EDR_PROCESSES=$(pgrep -aif "$EDR_DEFINITIONS" || true)

    if command -v systemctl &> /dev/null; then
        EDR_SERVICES=$(systemctl list-units | grep -i "$EDR_DEFINITIONS")
    fi


    if [[ -n "$EDR_PROCESSES" ]] || [[ -n "$EDR_SERVICES" ]]; then
        WarnMsg "EDR processes or services detected!!!"
        WarnMsg "Please disable the EDR on this system that may interfere with NodeZero operations."

        if [[ -n "$EDR_PROCESSES" ]]; then
            WarnMsg "Detected EDR Processes:"
            echo "$EDR_PROCESSES"
        fi
        if [[ -n "$EDR_SERVICES" ]]; then
            WarnMsg "Detected EDR Services:"
            echo "$EDR_SERVICES"
        fi
        sleep 5 # Sleep to allow user to read the detected EDR processes and services before the script continues
    else
        PassMsg "No common EDR processes or services detected on this system."
    fi
}


# Check network connectivity and SSL certificate validity for required hosts
# Reference: https://docs.horizon3.ai/quickstart/network_requirements/#your-portal-region
function NetworkChk {

    HeaderMsg "Checking network connectivity and SSL certificates for required hosts:"

    NETWORK_WARNINGS=0
    NETWORK_DOC="https://docs.horizon3.ai/quickstart/network_requirements/"

    # Determine which hosts to check based on the gateway/registry URL
    
    GATEWAY_HOST="gateway.horizon3ai.com"
    USING_GATEWAY="false"
    

    # Define hosts based on region (URLs for SSL certificate checks)
    if [[ "$GATEWAY_HOST" == *"horizon3ai.eu"* && "$USING_GATEWAY" == "false" ]]; then
        # EU Region hosts - https://docs.horizon3.ai/quickstart/network_requirements/#eu-based-portal
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.eu"
            "https://interact.gateway.horizon3ai.eu"
            "https://api.gateway.horizon3ai.eu"
            "https://registry.gateway.horizon3ai.eu"
            "https://api.horizon3ai.eu"
            "https://cognito-identity.eu-central-1.amazonaws.com"
            "https://cognito-idp.eu-central-1.amazonaws.com"
            "https://downloads.horizon3ai.com"
            "https://sqs.eu-central-1.amazonaws.com"
            "https://ecr.eu-central-1.amazonaws.com"
            "https://queue.amazonaws.com"
            "https://s3.amazonaws.com"
            "https://s3.eu-central-1.amazonaws.com"
            "https://s3-w.eu-central-1.amazonaws.com"
            "https://s3-r-w.eu-central-1.amazonaws.com"
            "https://canonical.com"
             # "https://ineracth3t.eu" In public docs but host check fails. Need to follow up on this host.
            "https://ubuntu.com"
        )
    elif [[ "$GATEWAY_HOST" == *"horizon3ai.eu"* && "$USING_GATEWAY" == "true" ]]; then
        # EU Gateway URLs - https://docs.horizon3.ai/quickstart/network_requirements/#eu-based-nodezero-gateway
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.eu"
            "https://interact.gateway.horizon3ai.eu"
            "https://api.gateway.horizon3ai.eu"
            "https://registry.gateway.horizon3ai.eu"
        )
    elif [[ "$GATEWAY_HOST" == *"gov-horizon3ai.com"* ]]; then
        # FedRAMP/Federal hosts - https://docs.horizon3.ai/quickstart/network_requirements/#fedramp-high-nodezero-gateway
        REQUIRED_URLS=(
            "https://gateway.gov-horizon3ai.com"
            "https://interact.gateway.gov-horizon3ai.com"
            "https://api.gateway.gov-horizon3ai.com"
        )
    elif [[ "$USING_GATEWAY" == "false" ]]; then
        # US Region hosts - https://docs.horizon3.ai/quickstart/network_requirements/#us-based-portal
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.com"
            "https://interact.gateway.horizon3ai.com"
            "https://api.gateway.horizon3ai.com"
            "https://registry.gateway.horizon3ai.com"
            "https://api.horizon3ai.com"
            "https://cognito-identity.us-east-2.amazonaws.com"
            "https://cognito-idp.us-east-2.amazonaws.com"
            "https://downloads.horizon3ai.com"
            "https://sqs.us-east-2.amazonaws.com"
            "https://ecr.us-east-2.amazonaws.com"
            "https://queue.amazonaws.com"
            "https://s3.amazonaws.com"
            "https://s3.us-east-1.amazonaws.com"
            "https://s3.us-east-2.amazonaws.com"
            "https://s3-w.us-east-2.amazonaws.com"
            "https://ubuntu.com"
            "https://canonical.com"
            # "https://interacth3.io" In public docs but host check fails. Need to follow up on this host.
        )
    else
        # US Region gateway hosts - https://docs.horizon3.ai/quickstart/network_requirements/#us-based-nodezero-gateway
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.com"
            "https://interact.gateway.horizon3ai.com"
            "https://api.gateway.horizon3ai.com"
            "https://registry.gateway.horizon3ai.com"
            "https://api.horizon3ai.com"
        )
    fi

    GenMsg "Checking connectivity and SSL certificates for region: $GATEWAY_HOST"

    for url in "${REQUIRED_URLS[@]}"; do
        # Extract host and port from URL
        host=$(echo "$url" | sed -E 's#https?://([^:/]+).*#\1#')
        port=$(echo "$url" | grep -oE ':[0-9]+' | tr -d ':')
        [[ -z "$port" ]] && port=443

        echo -ne "  Checking: $url ... "

        # First check SSL certificate using openssl
        # Use timeout on Linux, gtimeout on macOS (if available), or no timeout as fallback
        if [[ "$UNAMES" == "Darwin" ]]; then
            if command -v gtimeout &>/dev/null; then
                cert_output=$(echo | gtimeout 10 openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null)
            else
                cert_output=$(echo | openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null)
            fi
        else
            cert_output=$(echo | timeout 10 openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null)
        fi
        OPENSSL_EXIT=$?

        if [[ $OPENSSL_EXIT -ne 0 ]] || [[ -z "$cert_output" ]]; then
            # Connection failed - determine why using curl for better error messages
            HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 -m 10 "$url" 2>/dev/null)
            CURL_EXIT=$?

            if [[ $CURL_EXIT -eq 6 ]]; then
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "Cannot resolve hostname $host - DNS resolution failed"
                WarnMsg "  Please ensure your DNS can resolve $host or add it to your hosts file"
                WarnMsg "  See $NETWORK_DOC for network requirements"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            elif [[ $CURL_EXIT -eq 7 ]] || [[ $CURL_EXIT -eq 28 ]]; then
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "Cannot connect to $host - connection refused or timed out"
                WarnMsg "  This host may be blocked by a firewall. Please ensure outbound HTTPS (443/TCP) is allowed"
                WarnMsg "  See $NETWORK_DOC for network requirements"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            elif [[ $CURL_EXIT -eq 60 ]] || [[ $CURL_EXIT -eq 51 ]]; then
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "SSL certificate issue connecting to $host - your firewall may be intercepting HTTPS traffic"
                WarnMsg "  This can happen when a corporate firewall or proxy performs SSL inspection"
                WarnMsg "  Please ensure $host is allowlisted in your firewall. See $NETWORK_DOC"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            else
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "Failed to connect to $host (connection error)"
                WarnMsg "  Please verify network connectivity. See $NETWORK_DOC for requirements"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            fi
            continue
        fi

        # Check SSL certificate expiration
        not_after=$(echo "$cert_output" | openssl x509 -noout -enddate 2>/dev/null | sed 's/notAfter=//')
        if [[ -z "$not_after" ]]; then
            echo -e "${RED}FAIL${NORMAL}"
            WarnMsg "Could not parse SSL certificate for $host"
            NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            continue
        fi

        # Convert certificate expiration to epoch (handle both Linux and macOS date formats)
        if [[ "$UNAMES" == "Darwin" ]]; then
            end_epoch=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$not_after" +%s 2>/dev/null)
        else
            end_epoch=$(date -d "$not_after" +%s 2>/dev/null)
        fi
        now_epoch=$(date +%s)

        if [[ -z "$end_epoch" ]]; then
            echo -e "${YELLOW}WARN${NORMAL}"
            WarnMsg "Could not determine certificate expiration date for $host"
            NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
        elif [[ $now_epoch -gt $end_epoch ]]; then
            echo -e "${RED}FAIL${NORMAL}"
            WarnMsg "SSL certificate for $host has EXPIRED (expired: $not_after)"
            WarnMsg "  This may indicate a firewall/proxy intercepting HTTPS traffic with an expired certificate"
            WarnMsg "  Please ensure $host is allowlisted in your firewall. See $NETWORK_DOC"
            NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
        else
            echo -e "${GREEN}PASS${NORMAL}"
        fi
    done

    if [[ $NETWORK_WARNINGS -eq 0 ]]; then
        PassMsg "All required hosts are reachable with valid SSL certificates."
    else
        WarnMsg "$NETWORK_WARNINGS host(s) had connectivity or SSL certificate issues. NodeZero may not function correctly."
        WarnMsg "Please review the warnings above and ensure all required hosts are accessible."
        WarnMsg "For full network requirements, see: $NETWORK_DOC"
    fi
}


# Cleanup hello-world containers created by checks
function CleanUp {
    $DOCKER rmi $IMAGE -f &> /dev/null
}


function AlpineCleanUp {
    $DOCKER rmi $REGISTRY_URL/$REPOSITORY/alpine:latest -f &> /dev/null
}


# If nodezero_version is defined, remove all nodezero images except the version defined to reclaim space
function KeepCurrentNodeZeroImage {
    # Find all NodeZero images except the current version
    # Match h3/n0: anywhere in the repository name (handles both h3/n0: and registry/h3/n0:)
    OLD_IMAGES=$($DOCKER images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | grep "h3/n0:" | grep -v ":${NODEZERO_VERSION}$" || true)

    if [[ -z "$OLD_IMAGES" ]]; then
        GenMsg "No old NodeZero images found to clean up."
        return 0
    fi

    for image in $OLD_IMAGES; do
        # Get image size in bytes, then convert to KB
        IMG_SIZE_BYTES=$($DOCKER images --format "{{.Size}}" "$image" 2>/dev/null | head -1)

        # Try to remove the image
        if $DOCKER rmi "$image" -f &> /dev/null; then
            GenMsg "Removed old NodeZero image: $image (${IMG_SIZE_BYTES})"
        else
            WarnMsg "Could not remove image: $image (may be in use)"
        fi
    done
}


# Check if previous NodeZero images exist and remove to reclaim space
# if nodezero_version is defined, remove all docker images except the version defined
# if nodezero_version is not defined, remove all docker images except the most recent image based on creation date
function RemoveNodeZeroArtifacts {

    HeaderMsg "Checking for previous NodeZero configuration file artifacts:"

    # Check in .nodezero folder
    if ls ~/.nodezero/n0*.conf &>/dev/null; then
        # legacy: deletes configs from PWD
        rm $PWD/n0*.conf &>/dev/null
        rm ~/.nodezero/n0*.conf
        PassMsg "Identified and deleted all previous NodeZero configuration files."
    else
        GenMsg "No previous NodeZero configuration files identified."
    fi

    HeaderMsg "Checking for previous NodeZero container artifacts to remove and reclaim space:"
    # Cleanup unused containers
    GenMsg "Remove exited node-zero containers"
    $DOCKER rm $($DOCKER ps -a -q -f status=exited -f "name=n0-[0-9A-Fa-f]{4}") 2>/dev/null || true

    HeaderMsg "Checking for previous NodeZero image artifacts to remove and reclaim space:"
    # Capture current state of images
    ALL_IMG=$($DOCKER images --format='table {{.ID}}\t{{.Repository}}\t{{.Tag}}')
    GenMsg "Current state of images is as follows:"
    echo "$ALL_IMG"



    # Remove all n0 images except the most recent
    IMG=$($DOCKER images --format='table {{.ID}}\t{{.Repository}}\t{{.Tag}}' | grep h3\/n0 | awk '{ print $1 }')
    IMG_CNT=$(echo $IMG | wc -w)

    if [[ $IMG_CNT -ge 1 ]]; then
        GenMsg "Identified at least one NodeZero image, checking if latest and attempting to remove all others not being used:"

        # Get most recent image date among the NodeZero images to identify the latest image to keep
        NEWEST_IMG=0 # Initialize to a very old date
        for X in $IMG; do
            # Get the most recent image date
            IMG_DATE=$($DOCKER inspect $X --format='{{ .Created }}')
            GenMsg "Image $X was created on: $IMG_DATE"
            IMG_DATE=$(date -d "$IMG_DATE" +%s) # Convert to seconds since epoch

            if [[ $IMG_DATE -eq 0 ]]; then
                FailMsg "Unable to parse image date for $X, skipping."
                continue
            elif [[ $NEWEST_IMG -lt $IMG_DATE ]]; then
                NEWEST_IMG=$IMG_DATE
            fi
        done

        # Delete all but the most recent image
        for X in $IMG; do
            IMG_DATE=$($DOCKER inspect $X --format='{{ .Created }}')
            IMG_DATE=$(date -d "$IMG_DATE" +%s) # Convert to seconds since epoch

            if [[ $IMG_DATE -ne $NEWEST_IMG ]]; then
                GenMsg "Removing image $X..."
                $DOCKER rmi $X -f &>/dev/null
                EXIT=$?
                if [[ $EXIT == 0 ]]; then
                    PassMsg "Successfully removed $X image."
                else
                    FailMsg "Unable to remove $X image. It may be used by a container or referenced in multiple repositories. Please stop the dependent container if not needed and/or attempt to remove manually."
                fi
            else
                GenMsg "The $X image is the latest and will NOT be deleted."
                image_size=$($DOCKER inspect $X --format='{{ .Size }}')
                IMG_SIZE_KB=$(( image_size / 1024 ))
            fi
        done
    else
        GenMsg "No previous NodeZero images identified."
    fi



    # Remove untagged images
    ALL_DANGLING=$($DOCKER images -f dangling=true -q)
    if [[ -n "$ALL_DANGLING" ]]; then
        GenMsg "Dangling images found, Checking if any are NodeZero images..."
        for X in $ALL_DANGLING; do
            # Looks at the Cmd value to get the start NodeZero command
            if docker image inspect $X --format='{{.Config.Cmd}}' | grep 'app/nodezero.py' > /dev/null; then
                GenMsg "Found dangling NodeZero image. Attempting to remove dangling image $X..."
                $DOCKER rmi $X
            fi
        done
    else
        GenMsg "No dangling images identified."
    fi

    PassMsg "NodeZero artifact cleanup completed."

}





# Allow clean exit by trapping ctrl+c and calling ctrl_c()
trap ctrl_c INT


function ctrl_c() {
    echo
    ExitMsg "Ctrl+C detected, exiting gracefully..."
    exit 1
}


function main {

    HeaderMsg "Conducting pre-checks to validate the environment is NodeZero ready:"

    NetworkChk
    ProxyChk
    DockerChk
    DOCKERCHKRC=$?
    if [ $DOCKERCHKRC -ne 0 ]; then
        RetryDockerChk
    fi
    DockerProxyCheck
    RemoveNodeZeroArtifacts

    OsChk
    DiskChk
    MemChk
    CpuChk
    EdrChk

    HeaderMsg ${BOLD}${MAGENTA}"Pre-check validation completed successfully."${NORMAL}

    

    rm -R $DOCKER_CONFIG_PATH &> /dev/null
    rc=$?
    if [[ $rc != 0 ]]; then
        WarnMsg "Failed to remove temporary Docker config directory $DOCKER_CONFIG_PATH, please remove manually."
    fi
}


# Beginning of script execution
main
