| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 | #!/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 SUDOif [ "$EUID" -ne 0 ]  then echo "[Error] - This script must be run with sudo or as the root user."  exit 1fiSTATUS=0PASS=0WARN=0FAIL=0# $1 == command to check for# returns: 0 == true, 1 == falsecmdExists() {    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_IDelif 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_RELEASEelif [ -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"    fielse    # 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"; }clearecho "DigitalOcean Marketplace Image Validation Tool ${VERSION}"echo "Executed on: ${RUNDATE}"echo "Checking local system for Marketplace compatibility..."getDistroecho -en "\n\e[1mDistribution:\e[0m ${OS}\n"echo -en "\e[1mVersion:\e[0m ${VER}\n\n"ost=0osv=0if [[ $OS == "Ubuntu" ]]; then        ost=1    if [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then        osv=1    fielif [[ "$OS" =~ Debian.* ]]; then    ost=1    case "$VER" in        9)            osv=1            ;;        10)            osv=1            ;;        11)            osv=1            ;;                    *)            osv=2            ;;    esacelif [[ $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    fielif [[ $OS == "CentOS Stream" ]]; then        ost=1    if [[ $VER == "8" ]]; then        osv=1    else        osv=2    fielif [[ $OS == "Rocky Linux" ]]; then        ost=1    if [[ $VER =~ 8\. ]]; then        osv=1    else        osv=2    fielse    ost=0fiif [[ $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=2fiif [[ $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=2else    echo "Exiting..."    exit 1ficheckCloudInitecho -en "${CI}"checkFirewallecho -en "${FW_VER}"checkUpdatesloadPasswordscheckLogsecho -en "\n\nChecking all user-created accounts...\n"checkUsersecho -en "\n\nChecking the root account...\n"checkRootcheckAgent# Summaryecho -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"fiecho "---------------------------------------------------------------------------------------------------"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 0elif [[ $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 0else    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 1fi
 |