#!/usr/bin/env bash
# Ensure bash path is found

# shellcheck source=/etc/os-release
# use shellcheck to declare which file to source

# Using set to make the script safer
set -e          # Exit on error
set -u          # Exit on undefined variable
# set -x          # Print commands for debugging
set -a          # Export all variables
set -C          # Disable overwriting of files
set -o pipefail # Exit on pipe error
# set -euxaCo pipefail # All options

# Use the built-in trap command to catch error lines and signal numbers
trap 'printf "Error on line %d with signal %s" "$LINENO" "$?"' ERR # Exit on error
# Also use trap to catch interrupt signals and exit cleanly with a message to the user and a return code
trap 'printf "Interrupted on line %d with signal %s" "$LINENO" "$?"' INT SIGHUP SIGINT SIGTERM

# Get script name using parameter expansion to not spawn a new subprocess
SCRIPT_NAME="${0##*/}"

# We will be using printf instead of echo because it is more standardised.
# Also we will be using the test command's functionality as
# [[ because like this it constitutes a keyword and not a command.
# Functions will be defined as function "name" {body}" to make them
# more clear and () will not be used since using the keyword function renders them redundant

# We are taking for granted that the os-release file is in /etc as
# it has become a standard in most GNU/Linux distributions using systemd.
# We'll be using the -e flag just in case it is actually a symlink to another location.
# The get_distro function will use short if statements to check for the os-release file existence and readability.
# Then it will source it and output the distribution's name or exit in case of failure of either case.
function get_distro {
    if [[ -e /etc/os-release ]] && [[ -r /etc/os-release ]]; then # Check if file exists and is readable
        . /etc/os-release                                            # Source the file
        printf "%s" "$NAME"                                          # Output the distribution's name
    else                                                          # If the file does not exist or is not readable
        printf "%s" "File os-release not found or not readable"      # Output error message
        exit 1                                                       # Exit with error code 1
    fi
}

# The get_package_manager function will take the output of the get_distro function and determine
# which is the package manager used for the most popular server distros and exit if it is not found.
function get_package_manager {
    local distro                    # Declare distro as a local variable
    distro="$(get_distro)"          # Get the distribution name
    case "$distro" in               # Use case to check for the distribution name
        "Ubuntu" | "Debian GNU/Linux") # If the distribution is Ubuntu or Debian
            printf "%s" "apt"             # Output apt
            ;;
        "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL
            printf "%s" "dnf"                                             # Output dnf
            ;;
        "openSUSE Leap")      # If the distribution is OpenSUSE
            printf "%s" "zypper" # Output zypper
            ;;
        *)
            # If the distribution is none of the above, output unsupported distribution
            # and exit with error code 1
            printf "%s" "Unsupported distribution"
            exit 1 # Exit with error code 1
            ;;
    esac
}

# The install_packages function will take the output of the get_package_manager function and install any
# package passed as an argument to it. It will also check if the package manager is known and exit if it is not.
function install_packages {
    local package_manager                    # Declare package_manager as a local variable
    package_manager="$(get_package_manager)" # Get the package manager
    case "$package_manager" in               # Use case to check for the package manager
        "apt")                                  # If the package manager is apt
            printf "%s" 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections
            export DEBIAN_FRONTEND=noninteractive
            export NEEDRESTART_MODE=a
            export DEBIAN_PRIORITY=critical
            # Running sudo with -E will preserve the environment variables set in the script
            sudo -E apt update -y && sudo apt upgrade -y > /dev/null 2>&1 # Update the package list and upgrade the packages
            sudo -E apt install -y "$@" > /dev/null 2>&1               # Install the packages passed as arguments
            ;;
        "dnf")                    # If the package manager is dnf
            sudo dnf upgrade -y > /dev/null 2>&1     # Update the package list
            sudo dnf install -y "$@" > /dev/null 2>&1 # Install the packages passed as arguments
            ;;
        "zypper")                    # If the package manager is zypper
            sudo zypper update -y > /dev/null 2>&1      # Update the package list
            sudo zypper install -y "$@" > /dev/null 2>&1 # Install the packages passed as arguments
            ;;
        *)
            # If the package manager is not one of the above, output unsupported package manager
            # and exit with error code 1
            printf "%s" "Unsupported package manager"
            exit 1 # Exit with error code 1
            ;;
    esac
}

# The check_dependencies function will check if the dependencies defined in a local array are not installed
# and store the ones that are indeed absent in another local array.
# Then it will install the packages that are missing by invoking the install_packages function.
function check_dependencies { # systemd-container is for machinectl
    local dependencies=(fuse-overlayfs dbus-user-session uidmap slirp4netns systemd-container at cron htop curl git sudo vim ssh wget fail2ban) # Declare dependencies as a local array
    #> in the future we should see what to do with name differences between distros if any <#
    local missing_dependencies=()              # Declare missing_dependencies as a local array
    for dependency in "${dependencies[@]}"; do # Loop through the dependencies array
        # If the dependency is not installed, add it to the missing_dependencies array
        ! command -v "$dependency" &> /dev/null && missing_dependencies+=("$dependency")
    done
    # If the missing_dependencies array is not empty, install the packages
    [[ ${#missing_dependencies[@]} -ne 0 ]] && install_packages "${missing_dependencies[@]}"
}

# Global array of the service names to be restarted
services=()

# The hardenSSH function will use sed to modify the sshd_config file to have the following settings:
# - Allow ssh access to users in the sudo group only
# - Change the port to 22100 if it is available
# - Configure idle timeout to 5 minutes
# - Limit the number of authentication attempts to 3
# - Disable root login
# - Disable empty passwords
# - Disable ssh protocol 1
# - Disable password authentication and only allow public key authentication
# - Disable X11 forwarding for security reasons (X11 forwarding is not needed for ssh)
# - Disable agent forwarding to prevent ssh-agent hijacking
# Then it will store the sshd service name in the services array.
function hardenSSH {
    # Check if the sshd_config file exists and is readable
    # If it is, then modify it using sed and restart the sshd service
    # If it is not, then output an error message and exit with error code 1
    # The -i flag is used to modify the file in place
    # We split the sed command into multiple lines for readability purposes
    # and to avoid calling it multiple times
    if [[ -e /etc/ssh/sshd_config ]] && [[ -r /etc/ssh/sshd_config ]]; then
        sudo sed -i \
            -e 's/^#AllowGroups.*/AllowGroups sudo/' \
            -e 's/^#Port.*/Port 22100/' \
            -e 's/^#ClientAliveInterval.*/ClientAliveInterval 300/' \
            -e 's/^#ClientAliveCountMax.*/ClientAliveCountMax 3/' \
            -e 's/^#PermitRootLogin.*/PermitRootLogin no/' \
            -e 's/^#PermitEmptyPasswords.*/PermitEmptyPasswords no/' \
            -e 's/^#Protocol.*/Protocol 2/' \
            -e 's/^#PasswordAuthentication.*/PasswordAuthentication no/' \
            -e 's/^#X11Forwarding.*/X11Forwarding no/' \
            -e 's/^X11Forwarding.*/X11Forwarding no/' \
            -e 's/^#AllowAgentForwarding.*/AllowAgentForwarding no/' \
            /etc/ssh/sshd_config
        services+=("sshd") # Add sshd to the services array
    else
        printf "%s" "File sshd_config not found or not readable"
        exit 1
    fi
}

function getCorrectFirewall {
    local distro                    # Declare distro as a local variable
    distro="$(get_distro)"          # Get the distribution name
    case "$distro" in               # Use case to check for the distribution name
        "Ubuntu" | "Debian GNU/Linux") # If the distribution is Ubuntu or Debian
            printf "%s" 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections
            export DEBIAN_FRONTEND=noninteractive
            export NEEDRESTART_MODE=a
            export DEBIAN_PRIORITY=critical
            sudo -E apt install ufw -y > /dev/null 2>&1 # Install ufw
            printf "%s" "ufw" # Output ufw
            ;;
        "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL
            sudo dnf install firewalld -y > /dev/null 2>&1 # Install firewalld
            printf "%s" "firewalld" # Output firewalld
            ;;

        "openSUSE Leap")      # If the distribution is OpenSUSE
            sudo zypper install firewalld -y > /dev/null 2>&1 # Install firewalld
            printf "%s" "firewalld" # Output firewalld
            ;;
        *)
            # If the distribution is none of the above, output unsupported distribution
            # and exit with error code 1
            printf "%s" "Unsupported distribution"
            exit 1 # Exit with error code 1
            ;;
    esac
}

function getCorrectKernelSecurityModule {
    local distro                    # Declare distro as a local variable
    distro="$(get_distro)"          # Get the distribution name
    case "$distro" in               # Use case to check for the distribution name
        "Ubuntu") # If the distribution is Ubuntu
            sudo apt install apparmor-profiles apparmor-utils apparmor-profiles-extra -y > /dev/null 2>&1 # Install apparmor
            printf "%s" "apparmor" # Output apparmor
            ;;
        "Debian GNU/Linux") # If the distribution is Debian
            printf "%s" 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections
            export DEBIAN_FRONTEND=noninteractive
            export NEEDRESTART_MODE=a
            export DEBIAN_PRIORITY=critical
            sudo -E apt install apparmor apparmor-profiles apparmor-profiles-extra apparmor-utils auditd python3-apparmor -y > /dev/null 2>&1 # Install apparmor
            printf "%s" "apparmor" # Output apparmor
            ;;
        "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL
            sudo dnf install selinux container-selinux -y > /dev/null 2>&1 # Install selinux
            printf "%s" "selinux" # Output selinux
            ;;
        "openSUSE Leap")      # If the distribution is OpenSUSE
            sudo zypper install -t pattern apparmor -y > /dev/null 2>&1 # Install apparmor
            printf "%s" "apparmor" # Output apparmor
            ;;
        *)
            # If the distribution is none of the above, output unsupported distribution
            # and exit with error code 1
            printf "%s" "Unsupported distribution"
            exit 1 # Exit with error code 1
            ;;
    esac
}

function firewallInit {
    getCorrectFirewall # Get the correct firewall installed
    # Determine if ufw or firewalld is installed
    # We are not using command -v because we may be checking from a user that doesn't have the command in his path
    whereis ufw | grep -q /ufw && currentFirewall="ufw" || currentFirewall="firewalld"
    case "$currentFirewall" in
        ufw)
            sudo ufw default allow outgoing # Allow outgoing connections
            sudo ufw default deny incoming  # Deny incoming connections
            sudo ufw allow 22100/tcp        # Allow ssh connections on port 22100
            sudo ufw allow 8000/tcp         # Allow portainer connections on port 8000
            sudo ufw allow 9443/tcp         # Allow portainer connections on port 9443
            sudo ufw allow 8080/tcp         # Allow watchtower connections on port 8080
        ;;
        firewalld)
            sudo systemctl enable firewalld # Enable the firewall on boot
            sudo firewall-cmd --permanent --add-port=22100/tcp # Allow ssh connections on port 22100
            sudo firewall-cmd --permanent --add-port=8000/tcp # Allow portainer connections on port 8000
            sudo firewall-cmd --permanent --add-port=9443/tcp # Allow portainer connections on port 9443
            sudo firewall-cmd --permanent --add-port=8080/tcp # Allow watchtower connections on port 8080
        ;;
        *)
            printf "%s" "Unsupported firewall"
            exit 1
        ;;
    esac
}

function kernelSecurityModuleInit {
    getCorrectKernelSecurityModule # Get the correct kernel security module installed
    # Determine if apparmor or selinux is installed
    whereis apparmor | grep -q /apparmor && kernelSecurityModule="apparmor" || kernelSecurityModule="selinux"
    case "$kernelSecurityModule" in
        apparmor)
            sudo systemctl enable --now apparmor # Enable the kernel security module on boot and start it
            sudo aa-enforce /etc/apparmor.d/* # Enforce all apparmor profiles
        ;;
        selinux)
            sudo systemctl enable --now selinux # Enable the kernel security module on boot and start it
            sudo setenforce 1 # Enforce selinux
            sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config # Set selinux to enforcing
        ;;
        *)
            printf "%s" "Unsupported kernel security module"
            exit 1
        ;;
    esac
}

function dockerInit {
    # Set up rootless docker
    # Enable linger for the secdep user
    sudo loginctl enable-linger secdep
    # Enable dbus for the secdep user
    sudo machinectl shell secdep@ /bin/bash -c "systemctl --user enable --now dbus"
    # Install rootless docker
    sudo machinectl shell secdep@ /bin/bash -c "curl -fsSL https://get.docker.com/rootless | sh"
    # Add important environment variables to the secdep user's .bashrc
    sudo su secdep << 'EOF'
printf "%s\n" "export PATH=/home/$USER/bin:$PATH" >> "$HOME/.bashrc"
printf "%s\n" "export DOCKER_HOST=unix:///run/user/$UID/docker.sock" >> "$HOME/.bashrc"
EOF
    # Enable the user to bind to ports below 1024
    sudo setcap cap_net_bind_service=ep /home/secdep/bin/rootlesskit
    # Restart docker
    sudo machinectl shell secdep@ /bin/bash -c "systemctl --user restart docker"

    # Get all arguments passed to the function and store them in the dockerImages array
    local dockerImages=("$@")
    # Using -f instead of -e to check if the file exists AND that it is a regular file
    [[ -f /root/docker-compose.yml ]] && sudo mv /root/docker-compose.yml /home/secdep/docker-compose.yml
    [[ -f /home/admin/docker-compose.yml ]] && sudo mv /home/admin/docker-compose.yml /home/secdep/docker-compose.yml
    [[ -f /home/ec2-user/docker-compose.yml ]] && sudo mv /home/ec2-user/docker-compose.yml /home/secdep/docker-compose.yml
    [[ -f /home/centos/docker-compose.yml ]] && sudo mv /home/centos/docker-compose.yml /home/secdep/docker-compose.yml
    [[ -f /home/fedora/docker-compose.yml ]] && sudo mv /home/fedora/docker-compose.yml /home/secdep/docker-compose.yml
    [[ -f /home/ubuntu/docker-compose.yml ]] && sudo mv /home/ubuntu/docker-compose.yml /home/secdep/docker-compose.yml
    # Since FileDeployment does not work and we used ScriptFileDeployment which might make the file owned by another user
    # we need to make sure the file is owned by the secdep user.
    # When using [[ -f /home/secdep/docker-compose.yml ]] && sudo chown secdep:secdep /home/secdep/docker-compose.yml
    # it doesn't get executed somehow so we'll send the "no such file or directory" error to /dev/null
    sudo chown secdep:secdep /home/secdep/docker-compose.yml > /dev/null 2>&1
    # Since FileDeployment does not work and we used ScriptFileDeployment which automatically makes the file executable
    # we need to make sure the file is not executable.
    # when using [[ -f /home/secdep/docker-compose.yml ]] && sudo chmod -x /home/secdep/docker-compose.yml
    # it doesn't get executed somehow so we'll send the "no such file or directory" error to /dev/null
    sudo chmod -x /home/secdep/docker-compose.yml > /dev/null 2>&1
    sudo machinectl shell secdep@ /bin/bash -c 'curl -SL https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-linux-x86_64 -o /home/secdep/bin/docker-compose'
    sudo machinectl shell secdep@ /bin/bash -c 'chmod +x /home/secdep/bin/docker-compose'

    # Install gVisor (runsc) and containerd-shim-runsc-v1
    (
    set -e
    ARCH=$(uname -m)
    URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}
    wget -q "${URL}"/runsc "${URL}"/runsc.sha512 \
    "${URL}"/containerd-shim-runsc-v1 "${URL}"/containerd-shim-runsc-v1.sha512
    sha512sum -c runsc.sha512 \
    -c containerd-shim-runsc-v1.sha512
    rm -f -- *.sha512
    chmod a+rx runsc containerd-shim-runsc-v1
    sudo mv runsc containerd-shim-runsc-v1 /home/secdep/bin
    sudo chown secdep:secdep /home/secdep/bin/runsc
    sudo chown secdep:secdep /home/secdep/bin/containerd-shim-runsc-v1
    )
    # Enable gVisor for docker by default while hardening the docker daemon.json file
    # The no-new-privileges option will prevent the docker daemon from gaining new privileges
    # The runsc option --network=host and --ignore-cgroups=true will help with running runsc
    # with rootless docker without much hassle
    sudo runuser - secdep -c 'mkdir -p /home/secdep/.config/docker'
    # "icc": false to disable inter-container communication, does not work without br_netfilter module loaded
    # and it is sometimes prohibited by the hosting provider
    # so we'll leave it out of the daemon config
    DOCKERD_CONFIG=$(cat <<'EOF'
{
"no-new-privileges": true,
"selinux-enabled": false,
"default-runtime": "runsc",
"runtimes": {
"runsc": {
"path": "/home/secdep/bin/runsc",
"runtimeArgs": [
"--network=host",
"--ignore-cgroups=true"
]
}
}
}
EOF
)
    sudo runuser - secdep -c "printf '%s\n' '$DOCKERD_CONFIG' > /home/secdep/.config/docker/daemon.json"
    # Restart docker to apply the changes
    sudo machinectl shell secdep@ /bin/bash -c "systemctl --user restart docker"

    whereis apparmor | grep -q /apparmor && kernelSecurityModule="apparmor" || kernelSecurityModule="selinux"
    if [[ "$kernelSecurityModule" == "selinux" ]]; then
        sudo chcon -Rt svirt_sandbox_file_t /home/secdep/.config/docker
        sudo chcon -Rt svirt_sandbox_file_t /home/secdep/bin/runsc
        setsebool -P container_manage_cgroup true
        sudo runuser - secdep -c 'sed -i "s/\"selinux-enabled\": false/\"selinux-enabled\": true/" /home/secdep/.config/docker/daemon.json'
    fi

    sudo machinectl shell secdep@ "$(which bash)" -c '[[ -f "$HOME/docker-compose.yml" ]] && DOCKER_HOST=unix:///run/user/$UID/docker.sock /home/secdep/bin/docker-compose -f /home/secdep/docker-compose.yml up -d'
    # Read the docker-compose.yml file for port mappings to add to the firewall
    CMD_PORTS="cat /home/secdep/docker-compose.yml | sed '/^[[:space:]]*$/d' | grep -A1 ports | grep '[0-9]:[0-9]' | rev | cut -d':' -f1 | rev | grep -Eow '[[:digit:]]+' | tr '\n' ' '"
    sudo -E runuser - secdep -c "$CMD_PORTS" > /dev/null 2>&1 && PORTS="$(sudo -E runuser - secdep -c "$CMD_PORTS")" || PORTS=""
    # Loop through the ports in the PORTS variable
    if [[ -n "$PORTS" ]]; then
        for port in "${PORTS[@]}"; do # Using "${PORTS[@]}" instead of "$PORTS" as the latter will only return the first port
            # Allow the port in the firewall
            case "$currentFirewall" in
                ufw)
                    sudo ufw allow "$port"/tcp
                ;;
                firewalld)
                    sudo firewall-cmd --permanent --add-port="$port"/tcp
                ;;
                *)
                    printf "%s" "Unsupported firewall"
                    exit 1
                ;;
            esac
        done
    fi
    # Portainer is a docker image that provides a web interface for docker
    # which will be installed and run on port 9443 by default to make it easier to manage docker
    # url to follow after the installation is complete: https://vps_ip:9443
    # the https:// part is important as portainer will not work without it
    # For portainer (and watchtower), we will be using the --runtime=runc option to run it with runc because
    # it doesn't work with runsc as it is not exposing the docker socket to the container
    # but containers downloaded from it will still use runsc
    # Note: If a new install of Portainer is not configured within 5 minutes, 
    # it shuts down internally for security reasons but that does not matter since
    # we reboot after the script is done.
    sudo -E runuser - secdep -c 'docker run --runtime=runc -d -p 8000:8000 -p 9443:9443 --name=portainer --restart=always -v /run/user/$UID/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce'
    # Watchtower is a docker image that will automatically update all the other docker images
    # that are installed and running so we don't have to do it manually
    sudo -E runuser - secdep -c 'docker run --runtime=runc -d --name watchtower --restart=always -v /run/user/$UID/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --interval 3600'
    # Check if the dockerImages array is empty and return 0 if it is
    [[ "${#dockerImages[@]}" -eq 0 ]] && return 0
    # Loop through the dockerImages array
    # The dockerImages array contains all the docker images to install and run
    for dockerImage in "${dockerImages[@]}"; do
        # If dockerImage contains a ":" character (if it is a docker image with a tag)
        # then store the docker image name in the dockerImageName variable
        [[ "$dockerImage" == *":"* ]] && dockerImageName="${dockerImage%:*}" || dockerImageName="$dockerImage"
        # Same goes for "/"
        [[ "$dockerImageName" == *"/"* ]] && dockerImageName="${dockerImageName%/*}"
        CMD="docker pull $dockerImage"
        printf "%s\n" "Downloaded $dockerImageName docker image"
        sudo -E runuser - secdep -c "$CMD"
    done
}

# The configureFail2ban function will configure fail2ban to ban ip addresses that try to brute force the ssh port (22100)
function configureFail2ban {
FAIL2BAN_LOCAL=$(cat <<'EOF'
[Definition]
allowipv6 = auto
EOF
)
printf "%s\n" "$FAIL2BAN_LOCAL" | sudo tee /etc/fail2ban/fail2ban.local > /dev/null 2>&1
FAIL2BAN_SSH_JAIL_LOCAL=$(cat <<'EOF'
[sshd]
enabled = true
filter = sshd
banaction = iptables
backend = systemd
maxretry = 3
# 3 failed attempts in 600 seconds = 10 minutes
findtime = 1d
bantime = 1d
ignoreip = 127.0.0.1/8
EOF
)
FAIL2BAN_JAIL_LOCAL=$(cat <<'EOF'
[DEFAULT]
bantime = 1d
EOF
)
printf "%s\n" "$FAIL2BAN_JAIL_LOCAL" | sudo tee /etc/fail2ban/jail.local > /dev/null 2>&1
sudo rm -f /etc/fail2ban/jail.d/*
printf "%s\n" "$FAIL2BAN_SSH_JAIL_LOCAL" | sudo tee /etc/fail2ban/jail.d/sshd.local > /dev/null 2>&1
FAIL2BAN_FILTER=$(cat <<'EOF'
[Definition]
failregex = ^.*DROP_.*SRC=<ADDR> DST=.*$
journalmatch = _TRANSPORT=kernel
EOF
)
printf "%s\n" "$FAIL2BAN_FILTER" | sudo tee /etc/fail2ban/filter.d/fwdrop.local > /dev/null 2>&1
sudo systemctl enable --now fail2ban
printf "%s\n" "LogLevel VERBOSE" | sudo tee -a /etc/ssh/sshd_config > /dev/null 2>&1
}

function enableServices {
    # Loop through the services array which in the future could contain more services
    for service in "${services[@]}"; do
        sudo systemctl restart "$service"
    done
    whereis ufw | grep -q /ufw && currentFirewall="ufw" || currentFirewall="firewalld"

    # With the if block it doesn't error out at firewalld check so we do it this way
    # For ufw
    if [[ "$currentFirewall" == "ufw" ]]; then
        # Enable the firewall
        sudo ufw --force enable
        # Enable and start the firewall on boot
        sudo systemctl enable --now ufw
    # For firewalld
    elif [[ "$currentFirewall" == "firewalld" ]]; then
        # Reload the firewall
        sudo firewall-cmd --reload
    else
        printf "%s" "Somehow there is no firewall installed"
        exit 1
    fi
}

# If we are using aws to make the vps there is a default user different from the one we want to use.
# Also for the same reason, this script could be run by one of those users so we are using at to delete them
# after 2 minutes. If it was 1 minute it could fail if its xx:59 because at doesn't actually wait for 60 seconds
# but it waits for the next minute to come. Also the user would not be deleted without at because the script would
# be running as the user that we want to delete.
function deleteRemainingUsers {
    # In case atd wasn't running
    sudo systemctl enable --now atd
    # Set the correct timezone for Greece before using at
    sudo timedatectl set-timezone Europe/Athens
    # Delete possible remaining users
    cat << EOF | sudo tee /root/delete_users.sh > /dev/null 2>&1
[[ -d /home/admin ]] && sudo userdel -r admin && sudo groupdel admin
[[ -d /home/ec2-user ]] && sudo userdel -r ec2-user && sudo groupdel ec2-user
[[ -d /home/centos ]] && sudo userdel -r centos && sudo groupdel centos
[[ -d /home/fedora ]] && sudo userdel -r fedora && sudo groupdel fedora
[[ -d /home/ubuntu ]] && sudo userdel -r ubuntu && sudo groupdel ubuntu
sudo rm -f /root/delete_users.sh
EOF
    # Restart atd after setting the timezone
    sudo systemctl restart atd
    # Use at as root because if it is run as one of the users above it will fail
    sudo at now + 2 minute <<< "bash /root/delete_users.sh"
}

# It is not entirely possible to dynamically add and remove firewall rules for docker ports,
# especially when using portainer so we will use a cronjob to check every 30 minutes if there are
# any changes to be made to the firewall rules.
function dynamicDockerPortsCronjob {
# Part of the code responsible for the comparison
# of the arrays was taken from:
# https://stackoverflow.com/questions/2312762/compare-difference-of-two-arrays-in-bash
    sudo mkdir -p /root/bin
    cat << 'TOHERE' | sudo tee /root/bin/dynamic_docker_ports_cronjob.sh > /dev/null 2>&1
#!/usr/bin/env bash
# Get the current ports used by docker
CURRENT_DOCKER_PORTS="$(DOCKER_HOST=unix:///run/user/"$(id -u secdep)"/docker.sock /home/secdep/bin/docker ps --format '{{.Ports}}' | tr ' ' '\n' | rev | cut -d'/' -f2 | sed 's@^[^0-9]*\([0-9]\+\).*@\1@' | rev | sort -u | tr '\n' ' ')"
# Get the current ports allowed by the firewall
CURRENT_FIREWALL_PORTS_FIREWALLD="$(sudo firewall-cmd --list-ports | tr '\n' ' ')"
CURRENT_FIREWALL_PORTS_UFW="$(sudo ufw status numbered | grep -i allow | awk '{print $3}' | sed '/^[[:space:]]*$/d' | \grep -Eow '[[:digit:]]+' | sort -u | tr '\n' ' ')"
# Determine if ufw or firewalld is currently used
whereis ufw | grep -q /ufw && currentFirewall="ufw" || currentFirewall="firewalld"
# Find which ports are not allowed by the firewall but are used by docker
case "$currentFirewall" in
    ufw)
        l2=" ${CURRENT_FIREWALL_PORTS_UFW[*]} " # add framing blanks
        for item in ${CURRENT_DOCKER_PORTS[@]}; do
            if ! [[ $l2 =~  $item  ]] ; then    # use $item as regexp
                NEW_PORTS+=("$item")
            fi
        done
    ;;
    firewalld)
        l2=" ${CURRENT_FIREWALL_PORTS_FIREWALLD[*]} " # add framing blanks
        for item in ${CURRENT_DOCKER_PORTS[@]}; do
            if ! [[ $l2 =~  $item  ]] ; then    # use $item as regexp
                NEW_PORTS+=("$item")
            fi
        done
    ;;
    *)
        printf "%s" "Unsupported firewall"
        exit 1
    ;;
esac
# Loop through the ports in the NEW_PORTS variable if it is not empty
if [[ -n "$NEW_PORTS" ]]; then
    for port in "${NEW_PORTS[@]}"; do
        # Allow the port in the firewall
        case "$currentFirewall" in
            ufw)
                sudo ufw allow "$port"/tcp
            ;;
            firewalld)
                sudo firewall-cmd --permanent --add-port="$port"/tcp
            ;;
            *)
                printf "%s" "Unsupported firewall"
                exit 1
            ;;
        esac
    done
fi
# Find which ports are not used by docker but are allowed by the firewall
case "$currentFirewall" in
    ufw)
        l2=" ${CURRENT_DOCKER_PORTS[*]} " # add framing blanks
        for item in ${CURRENT_FIREWALL_PORTS_UFW[@]}; do
            if ! [[ $l2 =~  $item  ]] ; then    # use $item as regexp
                OLD_PORTS+=("$item")
            fi
        done
    ;;
    firewalld)
        l2=" ${CURRENT_DOCKER_PORTS[*]} " # add framing blanks
        for item in ${CURRENT_FIREWALL_PORTS_FIREWALLD[@]}; do
            if ! [[ $l2 =~  $item  ]] ; then    # use $item as regexp
                OLD_PORTS+=("$item")
            fi
        done
    ;;
    *)
        printf "%s" "Unsupported firewall"
        exit 1
    ;;
esac
# Loop through the ports in the OLD_PORTS variable if it is not empty
if [[ -n "$OLD_PORTS" ]]; then
    for port in "${OLD_PORTS[@]}"; do
        # Deny the port in the firewall
        case "$currentFirewall" in
            ufw) # There are two rules by default for ipv4 and ipv6
                yes | sudo ufw delete allow "$port"/tcp
            ;;
            firewalld)
                sudo firewall-cmd --permanent --remove-port="$port"/tcp
            ;;
            *)
                printf "%s" "Unsupported firewall"
                exit 1
            ;;
        esac
    done
fi
sudo ufw allow 22100/tcp
if [[ "$currentFirewall" == "firewalld" ]]; then
    sudo firewall-cmd --reload
else
    sudo ufw reload
fi
TOHERE
# Every 20 minutes check if there are any new ports used by docker and allow them in the firewall
cat << TOHERE | sudo tee -a /var/spool/cron/crontabs/root > /dev/null 2>&1
*/20 * * * * /root/bin/dynamic_docker_ports_cronjob.sh
TOHERE
sudo chmod +x /root/bin/dynamic_docker_ports_cronjob.sh
sudo systemctl restart cron
}

# Even though there is a package called cron-apt, it is only available for debian and ubuntu
# while this script is supposed to work on all the major server distros
function automaticUpdatesCronjob {
    sudo mkdir -p /root/bin
    cat << 'TOHERE' | sudo tee /root/bin/automatic_updates_cronjob.sh > /dev/null 2>&1
#!/usr/bin/env bash
function get_distro {
    if [[ -e /etc/os-release ]] && [[ -r /etc/os-release ]]; then # Check if file exists and is readable
        . /etc/os-release                                            # Source the file
        printf "%s" "$NAME"                                          # Output the distribution's name
    else                                                          # If the file does not exist or is not readable
        printf "%s" "File os-release not found or not readable"      # Output error message
        exit 1                                                       # Exit with error code 1
    fi
}

# The get_package_manager function will take the output of the get_distro function and determine
# which is the package manager used for the most popular server distros and exit if it is not found.
function get_package_manager {
    local distro                    # Declare distro as a local variable
    distro="$(get_distro)"          # Get the distribution name
    case "$distro" in               # Use case to check for the distribution name
        "Ubuntu" | "Debian GNU/Linux") # If the distribution is Ubuntu or Debian
            printf "%s" "apt"             # Output apt
            ;;
        "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL
            printf "%s" "dnf"                                             # Output dnf
            ;;
        "openSUSE Leap")      # If the distribution is OpenSUSE
            printf "%s" "zypper" # Output zypper
            ;;
        *)
            # If the distribution is none of the above, output unsupported distribution
            # and exit with error code 1
            printf "%s" "Unsupported distribution"
            exit 1 # Exit with error code 1
            ;;
    esac
}

# The update_system function will take the output of the get_package_manager function and update the system
# It will also check if the package manager is known and exit if it is not.
function update_system {
    local package_manager                    # Declare package_manager as a local variable
    package_manager="$(get_package_manager)" # Get the package manager
    case "$package_manager" in               # Use case to check for the package manager
        "apt")                                  # If the package manager is apt
            printf "%s" 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections
            export DEBIAN_FRONTEND=noninteractive
            export NEEDRESTART_MODE=a
            export DEBIAN_PRIORITY=critical
            # Running sudo with -E will preserve the environment variables set in the script
            sudo -E apt update -y && sudo apt upgrade -y # Update the package list and upgrade the packages
            sudo -E apt install -y
            ;;
        "dnf")                    # If the package manager is dnf
            sudo dnf upgrade -y      # Update the package list
            sudo dnf install -y
            ;;
        "zypper")                    # If the package manager is zypper
            sudo zypper update -y       # Update the package list
            sudo zypper install -y
            ;;
        *)
            # If the package manager is not one of the above, output unsupported package manager
            # and exit with error code 1
            printf "%s" "Unsupported package manager"
            exit 1 # Exit with error code 1
            ;;
    esac
}
update_system
TOHERE
# Every day at 4:00 AM update the system
cat << TOHERE | sudo tee -a /var/spool/cron/crontabs/root > /dev/null 2>&1
0 4 * * * /root/bin/automatic_updates_cronjob.sh
TOHERE
sudo chmod +x /root/bin/automatic_updates_cronjob.sh
sudo systemctl restart cron
}

# The main function will call the declared functions in order and exit if any of them fails.
# It will also pass any arguments passed to the script to the dockerInit function.
# Then it will output a message to the user and reboot the system in 2 minutes.
function main {
    printf "\x1B[01;93m========== SecDep ==========\n\x1B[0m"
    printf "%s\n" "=> $SCRIPT_NAME script started <="
    check_dependencies || exit 1                    # Check dependencies and exit if it fails
    printf "%s\n" "=> Dependencies installed <="
    hardenSSH || exit 1                             # Harden ssh and exit if it fails
    printf "%s\n" "=> SSH hardened <="
    firewallInit || exit 1                          # Initialize the firewall and exit if it fails
    printf "%s\n" "=> Firewall initialized <="
    kernelSecurityModuleInit || exit 1              # Initialize the kernel security module and exit if it fails
    printf "%s\n" "=> Kernel security module initialized <="
    configureFail2ban || exit 1                     # Initialize fail2ban and exit if it fails
    printf "%s\n" "=> Fail2ban configured <="
    # Call the dockerInit function with the arguments passed to the script
    dockerInit "$@" || exit 1                       # Initialize docker and exit if it fails
    printf "%s\n" "=> Docker Rootless, docker-compose and gVisor installed and configured <="
    printf "%s\n" "=> Portainer and Watchtower along with any specified docker images from the command line or a docker-compose.yml file installed <="
    enableServices || exit 1                        # Enable the services that need to be restarted and the firewall
    printf "%s\n" "=> Services restarted and firewall enabled <="
    dynamicDockerPortsCronjob || exit 1                    # Allow the ports used by docker in the firewall
    printf "%s\n" "=> CronJob to adjust the ports used by docker and the firewall installed <="
    automaticUpdatesCronjob || exit 1               # Install a cronjob to update the system periodically
    printf "%s\n" "=> CronJob to update the system installed <="
    # If the username is not secdep, delete the remaining users
    [[ "$USER" != "secdep" ]] && deleteRemainingUsers || exit 1                  # Delete possible remaining users
    printf "%s\n" "=> Any unnecessary users deleted <="
    printf "%s\n" "=> $SCRIPT_NAME script finished <="
    printf "%s\n" "=> System will reboot momentarily <="
    # Reboot the system in 3 minutes with the shutdown command so that login before the reboot is not possible
    # If the username is not secdep, reboot the system in 1 minute
    # We reboot just in case there are any updates that need to be applied
    # It was not the original intention of the script to reboot the system but it is better to be safe than sorry
    # We also wait (for 1 or 3 minutes depending on the user running it) so that the script can finish as we 
    # want to see the exit code.
    if [[ "$USER" != "secdep" ]]; then
        sudo shutdown -r +3
    else
        sudo shutdown -r +1
    fi
}

# Check if the user is root
if [[ "$EUID" -eq 0 ]]; then
    printf "%s\n" "Which distro let you login as root?"
    exit 1
else
    # Call the main function
    main "$@"
fi

exit 0 # The right and proper way to exit a script
