#!/bin/bash

# DigitalOcean Marketplace Image Validation Tool
# © 2021-2022 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)

VERSION="v. 1.8"
RUNDATE=$( date )

# Script should be run with SUDO
if [ "$EUID" -ne 0 ]
  then echo "[Error] - This script must be run with sudo or as the root user."
  exit 1
fi

STATUS=0
PASS=0
WARN=0
FAIL=0

# $1 == command to check for
# returns: 0 == true, 1 == false
cmdExists() {
    if command -v "$1" > /dev/null 2>&1; then
        return 0
    else
        return 1
    fi
}

function getDistro {
    if [ -f /etc/os-release ]; then
    # freedesktop.org and systemd
    # shellcheck disable=SC1091
    . /etc/os-release
    OS=$NAME
    VER=$VERSION_ID
elif type lsb_release >/dev/null 2>&1; then
    # linuxbase.org
    OS=$(lsb_release -si)
    VER=$(lsb_release -sr)
elif [ -f /etc/lsb-release ]; then
    # For some versions of Debian/Ubuntu without lsb_release command
    # shellcheck disable=SC1091
    . /etc/lsb-release
    OS=$DISTRIB_ID
    VER=$DISTRIB_RELEASE
elif [ -f /etc/debian_version ]; then
    # Older Debian/Ubuntu/etc.
    OS=Debian
    VER=$(cat /etc/debian_version)
elif [ -f /etc/SuSe-release ]; then
    # Older SuSE/etc.
    :
elif [ -f /etc/redhat-release ]; then
    # Older Red Hat, CentOS, etc.
    VER=$(cut -d" " -f3 < /etc/redhat-release | cut -d "." -f1)
    d=$(cut -d" " -f1 < /etc/redhat-release | cut -d "." -f1)
    if [[ $d == "CentOS" ]]; then
      OS="CentOS Linux"
    fi
else
    # Fall back to uname, e.g. "Linux <version>", also works for BSD, etc.
    OS=$(uname -s)
    VER=$(uname -r)
fi
}
function loadPasswords {
SHADOW=$(cat /etc/shadow)
}

function checkAgent {
  # Check for the presence of the DO directory in the filesystem
  if [ -d /opt/digitalocean ];then
     echo -en "\e[41m[FAIL]\e[0m DigitalOcean directory detected.\n"
            ((FAIL++))
            STATUS=2
      if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
        echo "To uninstall the agent: 'sudo yum remove droplet-agent'"
        echo "To remove the DO directory: 'find /opt/digitalocean/ -type d -empty -delete'"
      elif [[ $OS == "Ubuntu" ]] || [[ $OS == "Debian" ]]; then
        echo "To uninstall the agent and remove the DO directory: 'sudo apt-get purge droplet-agent'"
      fi
  else
    echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n"
    ((PASS++))
  fi
}

function checkLogs {
    cp_ignore="/var/log/cpanel-install.log"
    echo -en "\nChecking for log files in /var/log\n\n"
    # Check if there are log archives or log files that have not been recently cleared.
    for f in /var/log/*-????????; do
      [[ -e $f ]] || break
      if [ "${f}" != "${cp_ignore}" ]; then
        echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n"
        cat "${f}"
        ((WARN++))
        if [[ $STATUS != 2 ]]; then
            STATUS=1
        fi
      fi
    done
    for f in  /var/log/*.[0-9];do
      [[ -e $f ]] || break
        echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n"
        cat "${f}"
        ((WARN++))
        if [[ $STATUS != 2 ]]; then
            STATUS=1
        fi
    done
    for f in /var/log/*.log; do
      [[ -e $f ]] || break
      if [[ "${f}" = '/var/log/lfd.log' && "$(grep -E -v '/var/log/messages has been reset| Watching /var/log/messages' "${f}" | wc -c)" -gt 50 ]]; then
      if [ "${f}" != "${cp_ignore}" ]; then
        echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n"
        cat "${f}"
        ((WARN++))
        if [[ $STATUS != 2 ]]; then
            STATUS=1
        fi
      fi
      elif [[ "${f}" != '/var/log/lfd.log' && "$(wc -c < "${f}")" -gt 50 ]]; then
      if [ "${f}" != "${cp_ignore}" ]; then
        echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n"
        cat "${f}"
        ((WARN++))
        if [[ $STATUS != 2 ]]; then
            STATUS=1
        fi
      fi
    fi
    done
}
function checkTMP {
  # Check the /tmp directory to ensure it is empty.  Warn on any files found.
  return 1
}
function checkRoot {
    user="root"
    uhome="/root"
    for usr in $SHADOW
    do
      IFS=':' read -r -a u <<< "$usr"
      if [[ "${u[0]}" == "${user}" ]]; then
        if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
            echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
            ((PASS++))
        else
            echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n"
            ((FAIL++))
            STATUS=2
        fi
      fi
    done
    if [ -d ${uhome}/ ]; then
            if [ -d ${uhome}/.ssh/ ]; then
                if  ls ${uhome}/.ssh/*> /dev/null 2>&1; then
                    for key in "${uhome}"/.ssh/*
                        do
                             if  [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then

                                if [ "$(wc -c < "${key}")" -gt 50 ]; then
                                    echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
                                    akey=$(cat "${key}")
                                    echo "File Contents:"
                                    echo "$akey"
                                    echo "--------------"
                                    ((FAIL++))
                                    STATUS=2
                                fi
                            elif  [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
                                if [ "$(wc -c < "${key}")" -gt 0 ]; then
                                  echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
                                      akey=$(cat "${key}")
                                      echo "File Contents:"
                                      echo "$akey"
                                      echo "--------------"
                                      ((FAIL++))
                                      STATUS=2
                                else
                                  echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
                                  ((WARN++))
                                  if [[ $STATUS != 2 ]]; then
                                    STATUS=1
                                  fi
                                fi
                            elif  [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
                                 echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n"
                                    ((WARN++))
                                    if [[ $STATUS != 2 ]]; then
                                        STATUS=1
                                    fi
                            else
                                if [ "$(wc -c < "${key}")" -gt 50 ]; then
                                    echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n"
                                    ((WARN++))
                                    if [[ $STATUS != 2 ]]; then
                                        STATUS=1
                                    fi
                                fi
                            fi
                        done
                else
                    echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
                fi
            else
                echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
            fi
             if [ -f /root/.bash_history ];then

                      BH_S=$(wc -c < /root/.bash_history)

                      if [[ $BH_S -lt 200 ]]; then
                          echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
                          ((PASS++))
                      else
                          echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
                          ((FAIL++))
                              STATUS=2
                      fi

                      return 1;
                  else
                      echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n"
                      ((PASS++))
                  fi
        else
            echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
        fi
        echo -en "\n\n"
    return 1
}

function checkUsers {
    # Check each user-created account
    awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' < /etc/passwd | while IFS= read -r user;
    do
      # Skip some other non-user system accounts
      if [[ $user == "centos" ]]; then
        :
      elif [[ $user == "nfsnobody" ]]; then
        :
    else
      echo -en "\nChecking user: ${user}...\n"
      for usr in $SHADOW
        do
          IFS=':' read -r -a u <<< "$usr"
          if [[ "${u[0]}" == "${user}" ]]; then
              if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
                  echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
                  # shellcheck disable=SC2030
                  ((PASS++))
              else
                  echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n"
                  # shellcheck disable=SC2030
                  ((FAIL++))
                  STATUS=2
              fi
          fi
        done
        #echo "User Found: ${user}"
        uhome="/home/${user}"
        if [ -d "${uhome}/" ]; then
            if [ -d "${uhome}/.ssh/" ]; then
                if  ls "${uhome}/.ssh/*"> /dev/null 2>&1; then
                    for key in "${uhome}"/.ssh/*
                        do
                            if  [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
                                if [ "$(wc -c < "${key}")" -gt 50 ]; then
                                    echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
                                    akey=$(cat "${key}")
                                    echo "File Contents:"
                                    echo "$akey"
                                    echo "--------------"
                                    ((FAIL++))
                                    STATUS=2
                                fi
                              elif  [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
                                if [ "$(wc -c < "${key}")" -gt 0 ]; then
                                  echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
                                      akey=$(cat "${key}")
                                      echo "File Contents:"
                                      echo "$akey"
                                      echo "--------------"
                                      ((FAIL++))
                                      STATUS=2
                                else
                                  echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
                                  # shellcheck disable=SC2030
                                  ((WARN++))
                                  if [[ $STATUS != 2 ]]; then
                                    STATUS=1
                                  fi
                                fi
                            elif  [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then

                                 echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n"
                                 ((WARN++))
                                 if [[ $STATUS != 2 ]]; then
                                        STATUS=1
                                    fi

                            else
                                if [ "$(wc -c < "${key}")" -gt 50 ]; then
                                    echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n"
                                    ((WARN++))
                                    if [[ $STATUS != 2 ]]; then
                                        STATUS=1
                                    fi
                                fi
                            fi


                        done
                else
                    echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
                fi
            else
                echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
            fi
        else
            echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
        fi

         # Check for an uncleared .bash_history for this user
              if [ -f "${uhome}/.bash_history" ]; then
                            BH_S=$(wc -c < "${uhome}/.bash_history")

                            if [[ $BH_S -lt 200 ]]; then
                                echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
                                ((PASS++))
                            else
                                echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
                                ((FAIL++))
                                    STATUS=2

                            fi
                           echo -en "\n\n"
                         fi
        fi
    done
}
function checkFirewall {

    if [[ $OS == "Ubuntu" ]]; then
      fw="ufw"
      ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
      if [[ $ufwa == "active" ]]; then
        FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
        # shellcheck disable=SC2031
        ((PASS++))
      else
        FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
        # shellcheck disable=SC2031
        ((WARN++))
      fi
    elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
      if [ -f /usr/lib/systemd/system/csf.service ]; then
        fw="csf"
        if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then

        FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
        ((PASS++))
        elif cmdExists "firewall-cmd"; then
          if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
           FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
          ((PASS++))
          else
            FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
          ((WARN++))
          fi
        else
          FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
        ((WARN++))
        fi
      else
        fw="firewalld"
        if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
          FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
        ((PASS++))
        else
          FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
        ((WARN++))
        fi
      fi
    elif [[ "$OS" =~ Debian.* ]]; then
      # user could be using a number of different services for managing their firewall
      # we will check some of the most common
      if cmdExists 'ufw'; then
        fw="ufw"
        ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
        if [[ $ufwa == "active" ]]; then
        FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
        ((PASS++))
      else
        FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
        ((WARN++))
      fi
      elif cmdExists "firewall-cmd"; then
        fw="firewalld"
        if [[ $(systemctl is-active --quiet $fw) ]]; then
          FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
        ((PASS++))
        else
          FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
        ((WARN++))
        fi
      else
        # user could be using vanilla iptables, check if kernel module is loaded
        fw="iptables"
        if lsmod | grep -q '^ip_tables' 2>/dev/null; then
          FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
        ((PASS++))
        else
          FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
        ((WARN++))
        fi
      fi
    fi

}
function checkUpdates {
    if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
        # Ensure /tmp exists and has the proper permissions before
        # checking for security updates
        # https://github.com/digitalocean/marketplace-partners/issues/94
        if [[ ! -d /tmp ]]; then
          mkdir /tmp
        fi
        chmod 1777 /tmp

        echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n"
        apt-get -y update > /dev/null

        uc=$(apt-get --just-print upgrade | grep -i "security" -c)
        if [[ $uc -gt 0 ]]; then
          update_count=$(( uc / 2 ))
        else
          update_count=0
        fi

        if [[ $update_count -gt 0 ]]; then
            echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
            echo -en
            echo -en "Here is a list of the security updates that are not installed:\n"
            sleep 2
            apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++'
            echo -en
            # shellcheck disable=SC2031
            ((FAIL++))
            STATUS=2
        else
            echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n"
            ((PASS++))
        fi
    elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
        echo -en "\nChecking for available security updates, this may take a minute...\n\n"

        update_count=$(yum check-update --security --quiet | wc -l)
         if [[ $update_count -gt 0 ]]; then
            echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
            ((FAIL++))
            STATUS=2
        else
            echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n"
            ((PASS++))
        fi
    else
        echo "Error encountered"
        exit 1
    fi

    return 1;
}
function checkCloudInit {

    if hash cloud-init 2>/dev/null; then
        CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n"
        ((PASS++))
    else
        CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n"
        ((FAIL++))
        STATUS=2
    fi
    return 1
}

function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }


clear
echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}"
echo "Executed on: ${RUNDATE}"
echo "Checking local system for Marketplace compatibility..."

getDistro

echo -en "\n\e[1mDistribution:\e[0m ${OS}\n"
echo -en "\e[1mVersion:\e[0m ${VER}\n\n"

ost=0
osv=0

if [[ $OS == "Ubuntu" ]]; then
        ost=1
    if [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then
        osv=1
    fi

elif [[ "$OS" =~ Debian.* ]]; then
    ost=1
    case "$VER" in
        9)
            osv=1
            ;;
        10)
            osv=1
            ;;
        11)
            osv=1
            ;;            
        *)
            osv=2
            ;;
    esac

elif [[ $OS == "CentOS Linux" ]]; then
        ost=1
    if [[ $VER == "8" ]]; then
        osv=1
    elif [[ $VER == "7" ]]; then
        osv=1
    elif [[ $VER == "6" ]]; then
        osv=1
    else
        osv=2
    fi
elif [[ $OS == "CentOS Stream" ]]; then
        ost=1
    if [[ $VER == "8" ]]; then
        osv=1
    else
        osv=2
    fi
elif [[ $OS == "Rocky Linux" ]]; then
        ost=1
    if [[ $VER =~ 8\. ]]; then
        osv=1
    else
        osv=2
    fi
else
    ost=0
fi

if [[ $ost == 1 ]]; then
    echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n"
    ((PASS++))
else
    echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n"
    ((FAIL++))
    STATUS=2
fi

if [[ $osv == 1 ]]; then
    echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n"
    ((PASS++))
elif [[ $ost == 1 ]]; then
    echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n"
    ((FAIL++))
    STATUS=2
else
    echo "Exiting..."
    exit 1
fi

checkCloudInit

echo -en "${CI}"

checkFirewall

echo -en "${FW_VER}"

checkUpdates

loadPasswords

checkLogs

echo -en "\n\nChecking all user-created accounts...\n"
checkUsers

echo -en "\n\nChecking the root account...\n"
checkRoot

checkAgent


# Summary
echo -en "\n\n---------------------------------------------------------------------------------------------------\n"

if [[ $STATUS == 0 ]]; then
    echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n"
elif [[ $STATUS == 1 ]]; then
    echo -en "Scan Complete. \n\e[93mSome non-critical tests failed.  Please review these items.\e[0m\e[0m\n"
else
    echo -en "Scan Complete. \n\e[41mOne or more tests failed.  Please review these items and re-test.\e[0m\n"
fi
echo "---------------------------------------------------------------------------------------------------"
echo -en "\e[1m${PASS} Tests PASSED\e[0m\n"
echo -en "\e[1m${WARN} WARNINGS\e[0m\n"
echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n"
echo -en "---------------------------------------------------------------------------------------------------\n"

if [[ $STATUS == 0 ]]; then
    echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n"
    exit 0
elif [[ $STATUS == 1 ]]; then
    echo -en "Please review all [WARN] items above and ensure they are intended or resolved.  If you do not have a specific requirement, we recommend resolving these items before image submission\n\n"
    exit 0
else
    echo -en "Some critical tests failed.  These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n"
    exit 1
fi