#!/bin/bash
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
# [LINUX] Rackspace RackConnect Image Validation Script
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

# Make sure path is enumerated properly
PATH="${PATH}:${HOME}/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/sbin:/usr/sbin:"
HISTFILE=/dev/null

# Set variables
ver="Version 2.1.112"
WGET_TEST_URL="https://scripts.rackconnect.rackspace.com/download_test"
TEST_ACCOUNTNAME="rctestdeleteme"

if [[ "$1" == "-m" ]]; then
  HUMAN_READABLE=0
else
  HUMAN_READABLE=1
fi

# Set exit statuses
EXIT_SUCCESS=0
EXIT_TESTFAILED=1
EXIT_INVALIDOS=2
EXIT_NOTROOT=3
EXIT_UNDEFINED_STATUS=99

LOG_BUFFER=""
NUM_PASSES=0
NUM_FAILS=0
INDENT_LVL=0

# RBA depends on status output being in a certain format
function rba_exit(){
  local RBA_START="RBA START STATUS:"
  local RBA_END="RBA END STATUS:"
  local RBA_EXIT_STATUS="${1}"

  if [ ${HUMAN_READABLE} -ne 1 ]; then
    "${ECHO}" "${RBA_START}"
    case "${1}" in
      "${EXIT_SUCCESS}")
        if [ ${NUM_FAILS} -eq 0 ]; then
          "${ECHO}" "SUCCESS"
        else
          "${ECHO}" "FAILED: ${NUM_FAILS} tests failed."
        fi ;;
      "${EXIT_NOTROOT}")   "${ECHO}" "FAILED: You must be root" ;;
      "${EXIT_INVALIDOS}") "${ECHO}" "FAILED: OS is not supported" ;;
      *)                   "${ECHO}" "FAILED: Undefined exit status."
                           RBA_EXIT_STATUS="${EXIT_UNDEFINED_STATUS}" ;;
    esac

    "${ECHO}" "${RBA_END}"
    "${ECHO}" "RBA START DATA:"
    "${ECHO}" "${LOG_BUFFER}"
    "${ECHO}" "RBA END DATA:"
  fi

  if [[ "${1}" == "${EXIT_SUCCESS}" ]] && [ ${NUM_FAILS} -gt 0 ]; then
    exit ${EXIT_TESTFAILED}
  fi

  exit ${RBA_EXIT_STATUS}
}

function print_indented() {
  # First argument is the prefix string ('[PASS]', etc.).  Rest is log message.
  # We always want to indent by INDENT_LVL # of spaces, but only want to add a
  # space after the prefix string if the prefix string isn't ''
  PREFIX_STR="$(printf "%${INDENT_LVL}s%s" '' "$1")"
  [[ -z "$1" ]] || PREFIX_STR="${PREFIX_STR} "
  shift

  # If the log message has multiple lines, make them line up by prefixing with
  # a number of spaces equal to the length of PREFIX_STR.
  MULTILINE_INDENT_STR="$(echo "${PREFIX_STR}" | sed 's/./ /g')"

  echo "${PREFIX_STR}${*}" | sed '1!s/^/'"${MULTILINE_INDENT_STR}/"
}

# TODO - add ANSI color sequences
function log() {
  local LOG_TYPE="$1"
  shift
  local LOG_MSG="$*"

  case "${LOG_TYPE}" in
    "FAIL" | \
    "FATAL") NUM_FAILS=$((NUM_FAILS + 1)) ;;
    "PASS")  NUM_PASSES=$((NUM_PASSES + 1)) ;;
  esac

  if [ ${HUMAN_READABLE} -eq 1 ]; then
    case "${LOG_TYPE}" in
      "SECTION_END") printf '\n'
                     INDENT_LVL=$((INDENT_LVL-2))
                     ;;
      "SECTION")     print_indented '' "### ${LOG_MSG} ###"
                     INDENT_LVL=$((INDENT_LVL+2))
                     ;;
      "INFO")        print_indented ''              "${LOG_MSG}" ;;
      "PASS")        print_indented '[PASS]'        "${LOG_MSG}" ;;
      "FAIL")        print_indented '[FAIL]'        "${LOG_MSG}" >&2 ;;
      "WARN")        print_indented '[WARNING]'     "${LOG_MSG}" >&2 ;;
      "FATAL")       print_indented '[ERROR]'       "${LOG_MSG}" >&2 ;;
      *)             print_indented "[${LOG_TYPE}]" "${LOG_MSG}" ;;
    esac
  else
    case "${LOG_TYPE}" in
      "SECTION_END") LOG_LINE="SECTION END: ${LOG_MSG}" ;;
      "SECTION")     LOG_LINE="SECTION BEGIN: ${LOG_MSG}" ;;
      "INFO")        LOG_LINE="${LOG_MSG}" ;;
      "PASS")        LOG_LINE="PASS: ${LOG_MSG}" ;;
      "FAIL")        LOG_LINE="FAIL: ${LOG_MSG}" ;;
      "WARN")        LOG_LINE="WARNING: ${LOG_MSG}" ;;
      "FATAL")       LOG_LINE="ERROR: ${LOG_MSG}" ;;
      *)             LOG_LINE="${LOG_TYPE}: ${LOG_MSG}" ;;
    esac

    if [[ -z "${LOG_BUFFER}" ]]; then
      LOG_BUFFER="${LOG_LINE}"
    else
      LOG_BUFFER="${LOG_BUFFER}; ${LOG_LINE}"
    fi
  fi
}


# Determine OS type
function os_type() {
  if [[ `cat /etc/issue | grep -i 'ubuntu' | wc -l` -ge 1 ]]; then
    os=ubuntu
    OS_DISPLAY="Ubuntu"
  elif [[ -f /etc/debian_version ]]; then
    os=debian
    OS_DISPLAY="Debian"
  elif [[ -f /etc/arch-release ]]; then
    os=arch
    OS_DISPLAY="Arch"
  elif [[ -f /etc/gentoo-release ]]; then
    os=gentoo
    OS_DISPLAY="Gentoo"
  elif [[ `cat /etc/issue | grep -i 'opensuse' | wc -l` -ge 1 ]]; then
    os=opensuse
    OS_DISPLAY="SuSE"
  elif [[ -f /etc/sysconfig/network-scripts/ifcfg-eth0 || -f /etc/sysconfig/network-scripts/ifcfg-eth1 ]]; then
    os=redhat
    OS_DISPLAY="RHEL/CentOS"
  else
    rba_exit "${EXIT_INVALIDOS}"
  fi

  log INFO "Detected OS type: ${OS_DISPLAY}"
}

# Check the location and existence of a command
function which_cmd() {
  local QUIET=0
  if [ "$1" == "-q" ]; then
    local QUIET=1
    shift
  fi

  # Would use `which ${2}` here, but which doesn't exist in base CentOS 5.8
  local cmd="`builtin type -P ${2} 2>/dev/null`"
  if [ ${?} -gt 0 -o ! -x "${cmd}" ]; then
    [ ${QUIET} -eq 0 ] && log FAIL "${2}: FAIL (Command not found in path)"
  else
    if [ -x "${cmd}" ]; then
      [ ${QUIET} -eq 0 ] && log PASS "${2}: OK (${cmd})"
      eval ${1}='"'"${cmd}"'"'
      return 0
    else
      [ ${QUIET} -eq 0 ] && log FAIL "${2}: FAIL (${cmd} exists but not executible)"
    fi
  fi

  eval ${1}=""
  return 1
}

# The MOUNTPOINTS global variable is populated with a new line every time check_file() is called.
# See test_disk_space() for more details.
MOUNTPOINTS=""
function check_file() {
  local CHECK_DIR=0
  local CHECK_WRITE=1
  local CHECK_EXEC=0
  local MISSING_OK=0
  while [[ "${1}" == '-d' || "${1}" == '-r' || "${1}" == '-x' || "${1}" == "--missing-ok" ]]; do
    [[ "${1}" == "-d" ]] && CHECK_DIR=1
    [[ "${1}" == "-r" ]] && CHECK_WRITE=0
    [[ "${1}" == "-x" ]] && CHECK_WRITE=0 && CHECK_EXEC=1
    [[ "${1}" == "--missing-ok" ]] && MISSING_OK=1
    shift
  done

  local FILE="$*"
  local REASON=""

  if [ -e "${FILE}" ]; then
    local IS_IMMUTABLE="$("${LSATTR}" -d "${FILE}" 2>/dev/null | "${SED}" -ne 's/\s.*$//;/i/p')"
    local IS_APPEND_ONLY="$("${LSATTR}" -d "${FILE}" 2>/dev/null | "${SED}" -ne 's/\s.*$//;/a/p')"

    if [ ${CHECK_DIR} -eq 1 ]; then
      ! [ -d "${FILE}" ] && REASON="${REASON} (Should be a directory, isn't)"
      ! [ -x "${FILE}" ] && REASON="${REASON} (Can't traverse - set execute bit)"
    else
      ! [ -f "${FILE}" ] && REASON="${REASON} (Should be a regular file, isn't)"
    fi

    [ ! -r "${FILE}" ] && REASON="${REASON} (Not readable)"

    if [ ${CHECK_WRITE} -eq 1 ]; then
      [ ! -w "${FILE}"           ] && REASON="${REASON} (Not writable)"
      [ ! -z "${IS_IMMUTABLE}"   ] && REASON="${REASON} (Immutable chattr bit set)"
      [ ! -z "${IS_APPEND_ONLY}" ] && REASON="${REASON} (Append-Only chattr bit set)"

      MOUNTPOINTS="$("${ECHO}" "${MOUNTPOINTS}" ; \
                      ( "${DF}" -P  "${FILE}" | "${SED}" -ne '2s/^.*\s\([0-9]\+\)%\s\+\(.*\)$/\1/p' ; \
                        "${DF}" -Pi "${FILE}" | "${SED}" -ne '2s/^.*\s\([0-9]\+\)%\s\+\(.*\)$/\1 \2/p' ) \
                      | "${SED}" 'N;s/\n/ /')"
    fi

    [ ${CHECK_EXEC} -eq 1 -a ! -x "${FILE}" ] && REASON="${REASON} (Not executable)"
  else
    if [ ${MISSING_OK} -eq 0 ]; then
      REASON="${REASON} (Does Not Exist)"
    else
      FILE_DIR="$(dirname "${FILE}")"
      if [ ${CHECK_WRITE} -eq 1 ]; then
      ! [ -d "${FILE_DIR}" ] && REASON="${REASON} (Doesn't exist, owning dir doesn't exist)"
      ! [ -x "${FILE_DIR}" ] && REASON="${REASON} (Doesn't exist, can't traverse owning dir - set execute bit)"
      ! [ -w "${FILE_DIR}" ] && REASON="${REASON} (Doesn't exist, owning dir not writable)"

        MOUNTPOINTS="$("${ECHO}" "${MOUNTPOINTS}" ; \
                        ( "${DF}" -P  "${FILE_DIR}" | "${SED}" -ne '2s/^.*\s\([0-9]\+\)%\s\+\(.*\)$/\1/p' ; \
                          "${DF}" -Pi "${FILE_DIR}" | "${SED}" -ne '2s/^.*\s\([0-9]\+\)%\s\+\(.*\)$/\1 \2/p' ) \
                        | "${SED}" 'N;s/\n/ /')"
      fi
    fi
  fi

  local FILE_DISPLAY="${FILE}"
  [ ${CHECK_WRITE} -eq 0 ] && FILE_DISPLAY="${FILE_DISPLAY} (Read)"
  [ ${CHECK_EXEC}  -eq 1 ] && FILE_DISPLAY="${FILE_DISPLAY} (Exec)"
  if [[ -z "${REASON}" ]]; then
    log PASS "${FILE_DISPLAY}: OK"
  else
    log FAIL "${FILE_DISPLAY}: FAIL${REASON}"
  fi
}

function test_is_root(){
  # root user check
  if [ "$(id -u)" == "0" ]; then
    log PASS  "User is root: OK"
  else
    log FATAL "User is root: FAILED\n\nYou MUST be root to use this script!  Quitting..."
    rba_exit ${EXIT_NOTROOT}
  fi
}

function test_programs(){
  which_cmd AWK awk
  which_cmd CAT cat
  which_cmd CHMOD chmod
  which_cmd CHOWN chown
  which_cmd CP cp
  which_cmd CUT cut
  which_cmd DATE date
  which_cmd DF df
  which_cmd ECHO echo
  which_cmd GREP grep
  which_cmd HEAD head
  which_cmd IFCONFIG ifconfig
  which_cmd IP ip
  which_cmd IPTABLES iptables
  which_cmd IPTABLESRESTORE iptables-restore
  which_cmd IPTABLESSAVE iptables-save
  which_cmd LSATTR lsattr
  which_cmd MV mv
  which_cmd PGREP pgrep
  which_cmd RM rm
  which_cmd ROUTE route
  which_cmd SED sed
  which_cmd SLEEP sleep
  which_cmd SORT sort
  which_cmd TOUCH touch
  which_cmd TR tr
  which_cmd USERADD useradd
  which_cmd -q USERDEL userdel
  which_cmd WC wc
  which_cmd WGET wget

  if [[ "${os}" == "ubuntu" || "${os}" == "debian" ]]; then
    which_cmd SERVICE service
    which_cmd UPDATERCD update-rc.d
  elif [[ "${os}" == "arch" ]]; then
    which_cmd NETCFG netcfg
  elif [[ "${os}" == "gentoo" ]]; then
    which_cmd RCUPDATE rc-update
  elif [[ "${os}" == "opensuse" ]]; then
    which_cmd RCSUSEFIREWALL rcSuSEfirewall2
    which_cmd SERVICE service
  else # [[ "${os}" == "redhat" ]]; then
    which_cmd CHKCONFIG chkconfig
    which_cmd CHPASSWD chpasswd
    which_cmd SERVICE service
  fi

  # Commands not present on all systems.  Check for them quietly and don't error if missing.
  which_cmd -q SYSTEMCTL systemctl
  which_cmd -q SESTATUS sestatus
  which_cmd -q SETENFORCE setenforce
  which_cmd -q GETENFORCE getenforce
}

function test_selinux() {
  if [[ "${SESTATUS}"   == "" &&
        "${GETENFORCE}" == "" ]]; then
    log PASS "SELinux not detected."
    return 0
  fi

  log INFO "SELinux Detected."

  if [[ "${GETENFORCE}" == "" ]]; then
    SELINUX_MODE="$("${SESTATUS}" | "${SED}" -ne '/^Current mode:\s*/s///p')"
  else
    SELINUX_MODE="$("${GETENFORCE}")"
  fi

  case "${SELINUX_MODE}" in
    "" | "Disabled" | "permissive" | "Permissive")
      log PASS "SELinux disabled or in permissive mode: OK" ;;
    *)
      log FAIL "SELinux in '${SELINUX_MODE}' mode: FAIL (Must be in Disabled or Permissive mode)"
  esac
}

function test_files() {
  MOUNTPOINTS=""

  check_file -d /tmp/
  [ -e /tmp/rules.txt ] && check_file --missing-ok /tmp/rules.txt

  check_file /etc/hosts
  check_file /etc/sudoers
  check_file -r /etc/resolv.conf
  check_file -r /etc/passwd

  if [[ "${os}" == "ubuntu" || "${os}" == "debian" ]]; then
    check_file /etc/network/interfaces
    check_file /etc/network/iptables
    check_file /etc/network/if-up.d/iptables
    check_file -x /etc/init.d/networking
  elif [[ "${os}" == "arch" ]]; then
    check_file /etc/rc.conf
    check_file /etc/conf.d/netcfg
    check_file --missing-ok /etc/network.d/eth0
    check_file --missing-ok /etc/network.d/eth1
    check_file --missing-ok /etc/iptables/iptables.rules
    check_file -x /etc/rc.d/iptables
    check_file -x /etc/rc.d/syslog-ng
  elif [[ "${os}" == "gentoo" ]]; then
    check_file /etc/conf.d/net
    check_file -x /etc/init.d/iptables
    check_file -x /etc/init.d/net.eth1
  elif [[ "${os}" == "opensuse" ]]; then
    check_file /etc/sysconfig/network/ifcfg-eth0
    check_file --missing-ok /etc/sysconfig/network/ifroute-eth1
    check_file -x /etc/init.d/network
  else # [[ "${os}" == "redhat" ]]; then
    check_file /etc/sysconfig/network
    check_file /etc/sysconfig/network-scripts/ifcfg-eth0
    check_file /etc/sysconfig/network-scripts/ifcfg-eth1
    check_file -x /etc/init.d/network
    [[ -z "$("${GREP}" -i Fedora /etc/redhat-release)" ]] && check_file -x /etc/init.d/iptables
  fi
}

function test_disk_space() {
  # The MOUNTPOINTS var is populated with the mountpoint of each file that check_file() is called on.
  # Specifically, the %used for both space and inode usage is preserved in the format:
  # 12 34 /mount/point
  # (Space %used) (Inode %used) (Mountpoint)

  # Some nasty gymnastics are needed here because the bash tokenizer breaks if the mountpoint
  # has a space in it.  Otherwise, we could just use a for loop to make this much prettier.

  # Remove duplicates in the mountpoint list, only keeping the one with highest percentages for each column.
  MP_DISK_USAGE="$("${ECHO}" "${MOUNTPOINTS}" | "${SORT}" -rn | "${SORT}" -u -k3 | \
                   "${SED}" -ne 's/^\([0-9]\+\) \([0-9]\+\) \(.*\)$/\1 \3/p')"
  MP_INODE_USAGE="$("${ECHO}" "${MOUNTPOINTS}" | "${SORT}" -rn -k2 | "${SORT}" -u -k3 | \
                    "${SED}" -ne 's/^\([0-9]\+\) \([0-9]\+\) \(.*\)$/\2 \3/p')"

  DISK_FREE_99="$("${ECHO}" "${MP_DISK_USAGE}" | "${AWK}" '{if($1>=99){print $0}}' | \
                  "${SED}" 's/^\([0-9]\+\) \(.*\)$/\2: ERROR - Disk usage (\1%) is >=99%./')"
  DISK_FREE_90="$("${ECHO}" "${MP_DISK_USAGE}" | "${AWK}" '{if($1>=90 && $1<99){print $0}}' | \
                  "${SED}" 's/^\([0-9]\+\) \(.*\)$/\2: WARNING - Disk usage (\1%) is >=90%./')"

  INODE_FREE_99="$("${ECHO}" "${MP_INODE_USAGE}" | "${AWK}" '{if($1>=99){print $0}}' | \
                   "${SED}" 's/^\([0-9]\+\) \(.*\)$/\2: ERROR - Disk inode (df -i) usage (\1%) is >=99%./')"
  INODE_FREE_90="$("${ECHO}" "${MP_INODE_USAGE}" | "${AWK}" '{if($1>=90 && $1<99){print $0}}' | \
                   "${SED}" 's/^\([0-9]\+\) \(.*\)$/\2: WARNING - Disk inode (df -i) usage (\1%) is >=90%./')"

  # Combine both lists, if the highest # of either value is <90%, mark as OK.
  MOUNTPOINT_OK="$( ( "${ECHO}" "${MP_DISK_USAGE}" ; "${ECHO}" "${MP_INODE_USAGE}" ) | \
                    "${SORT}" -rn | "${SORT}" -u -k2 | "${AWK}" '{if($1<90){print $0}}' | \
                    "${SED}" 's/^\([0-9]\+\) \(.*\)$/\2: OK/')"

  [[ ! -z "${MOUNTPOINT_OK}" ]] && log PASS "${MOUNTPOINT_OK}"
  [[ ! -z "${DISK_FREE_90}"  ]] && log WARN "${DISK_FREE_90}"
  [[ ! -z "${DISK_FREE_99}"  ]] && log FAIL "${DISK_FREE_99}"
  [[ ! -z "${INODE_FREE_90}" ]] && log WARN "${INODE_FREE_90}"
  [[ ! -z "${INODE_FREE_99}" ]] && log FAIL "${INODE_FREE_99}"
}

function test_iptables() {
  "${IPTABLES}" -A INPUT -m comment --comment "RCAuto-TEST" -j ACCEPT
  if [[ "$?" == "0" ]]; then
    log PASS "IPTables Rule Creation: OK"
  else
    log FAIL "IPTables Rule Creation: FAIL"
  fi

  "${IPTABLESSAVE}" | "${SED}" '/RCAuto-TEST/d' | "${IPTABLESRESTORE}"
  if [[ "$?" == "0" ]]; then
    log PASS "IPTables Atomic Rule Deletion: OK"
  else
    log FAIL "IPTables Atomic Rule Deletion: FAIL"
  fi
}

function test_sshd_config() {
  ### Listening on 0.0.0.0:22?
  # Fetch any Port entries before the first non-ported ListenAddress entry (if any).
  SSH_PORTS="$("${SED}" -ne '/^\s*\Port\s\+\(.*\)$/I{s//\1/p};/^\s*ListenAddress\s\+[^:]*$/q' /etc/ssh/sshd_config)"

  # Fetch any ListenAddress entries.
  SSH_LISTEN_ADDRS="$("${SED}" -ne '/^\s*ListenAddress\s\+\(.*\)\s*$/I{s//\1/p}' /etc/ssh/sshd_config)"

  # Special case - port specified in ListenAddress entry
  if [[ "$("${ECHO}" "${SSH_LISTEN_ADDRS}" | "${SED}" -ne '/^0.0.0.0:22$/p;/^:22$/p')" != "" ]]; then
    log PASS "SSH listening on all IPv4 IPs: OK"
    log PASS "SSH listening on port 22: OK"
  else
    # If no ListenAddress config or config contains 0.0.0.0 line, pass.
    if [[ -z "${SSH_LISTEN_ADDRS}" || "$("${ECHO}" "${SSH_LISTEN_ADDRS}" | "${SED}" -ne '/^0.0.0.0$/p')" != "" ]]; then
      log PASS "SSH listening on all IPv4 IPs: OK"
    else
      log FAIL "SSH listening on all IPv4 IPs: FAIL (Add ListenAddress 0.0.0.0 to /etc/ssh/sshd_config)"
    fi

    # If no Port config or config contains port 22, pass.
    if [[ -z "${SSH_PORTS}" || "$("${ECHO}" "${SSH_PORTS}" | "${SED}" -ne '/^22$/p')" != "" ]]; then
      log PASS "SSH listening on port 22: OK"
    else
      log FAIL "SSH listening on port 22: FAIL"
    fi
  fi


  ### Protocol 2 only - no-one would really use v1, would they?
  SSH_PROTOCOL="$("${SED}" -ne '/^\s*Protocol\s\+\(.*\)\s*$/I{s//\1/p}' /etc/ssh/sshd_config)"

  if [[ -z "${SSH_PROTOCOL}" || "$("${ECHO}" "${SSH_PROTOCOL}" | "${SED}" -ne '/^2$/!p')" == "" ]]; then
    log PASS "Protocol set to version 2: OK"
  else
    log FAIL "Protocol set to version 2: FAIL (SSH1 is as insecure as telnet.  Please switch to version 2 only.)"
  fi


  ### PasswordAuthentication or ChallengeResponseAuthentication enabled?
  SSH_PW_AUTH="$("${SED}" -ne '/^\s*PasswordAuthentication\s\+\(.*\)\s*$/I{s//\1/p}' /etc/ssh/sshd_config)"
  SSH_CR_AUTH="$("${SED}" -ne '/^\s*ChallengeResponseAuthentication\s\+\(.*\)\s*$/I{s//\1/p}' /etc/ssh/sshd_config)"

  # If both of these have a non-empty value other than "yes" (case-insensitive), fail.
  if [[ "$("${ECHO}" "${SSH_CR_AUTH}" | "${SED}" -ne '/^$/!{/^yes$/I!p}')" != "" && \
        "$("${ECHO}" "${SSH_PW_AUTH}" | "${SED}" -ne '/^$/!{/^yes$/I!p}')" != "" ]]; then
    log FAIL "Password or ChallengeResponse Authentication enabled: FAIL (One of these must be enabled)"
  else
    log PASS "Password or ChallengeResponse Authentication enabled: OK"
  fi


  ### PermitRootLogin yes?
  SSH_PERMIT_ROOT="$("${SED}" -ne '/^\s*PermitRootLogin\s\+\(.*\)\s*$/I{s//\1/p}' /etc/ssh/sshd_config)"

  # If non-empty and not "yes" (case-insensitive), fail.
  if [[ "$("${ECHO}" "${SSH_PERMIT_ROOT}" | "${SED}" -ne '/^$/!{/^yes$/I!p}')" != "" ]]; then
    log FAIL "PermitRootLogin enabled: FAIL (Must be 'yes' initially, may be changed after RS post-build processes)"
  else
    log PASS "PermitRootLogin enabled: OK"
  fi


  ### DenyUsers
  SSH_DENYUSERS="$("${SED}" -ne '/^\s*DenyUsers\s/Ip' /etc/ssh/sshd_config)"
  SSH_DENYUSERS_RC="$("${SED}" -ne '/^\s*DenyUsers\s/I{/\srackconnect\(\s.*\|$\)/Ip}' /etc/ssh/sshd_config)"
  if [[ -z "${SSH_DENYUSERS}" ]]; then
    log PASS "No DenyUsers configuration directive found."
  else
    if [[ -z "${SSH_DENYUSERS_RC}" ]]; then
      log PASS "User 'rackconnect' not in DenyUsers: OK"
    else
      log FAIL "User 'rackconnect' not in DenyUsers: FAIL (Please remove rackconnect user from DenyUsers directive)"
    fi
  fi


  ### AllowUsers
  SSH_ALLOWUSERS="$("${SED}" -ne '/^\s*AllowUsers\s/Ip' /etc/ssh/sshd_config)"
  SSH_ALLOWUSERS_NO_RC="$("${SED}" -ne '/^\s*AllowUsers\s/I{/\srackconnect\(\s\|$\)/I!p}' /etc/ssh/sshd_config)"
  if [[ -z "${SSH_ALLOWUSERS}" ]]; then
    log PASS "No AllowUsers configuration directive found."
  else
    if [[ -z "${SSH_ALLOWUSERS_NO_RC}" ]]; then
      log PASS "User 'rackconnect' in AllowUsers: OK"
    else
      log FAIL "User 'rackconnect' in AllowUsers: FAIL (Please add rackconnect user to AllowUsers directive)"
    fi
  fi


  ### DenyGroups
  SSH_DENYGROUPS="$("${SED}" -ne '/^\s*DenyGroups\s/Ip' /etc/ssh/sshd_config)"
  SSH_DENYGROUPS_RC="$("${SED}" -ne '/^\s*DenyGroups\s/I{/\srackconnect\(\s.*\|$\)/Ip}' /etc/ssh/sshd_config)"
  if [[ -z "${SSH_DENYGROUPS}" ]]; then
    log PASS "No DenyGroups configuration directive found."
  else
    if [[ "${OS}" == "opensuse" ]]; then
      SSH_DENYGROUPS_RC="$("${SED}" -ne '/^\s*DenyGroups\s/I{/\s\(users\|video\)\(\s.*\|$\)/Ip}' /etc/ssh/sshd_config)"
      if [[ -z "${SSH_DENYGROUPS_RC}" ]]; then
        log PASS "Default SuSE user groups 'users'/'video' not in DenyGroups: OK"
      else
        log FAIL "Default SuSE user groups 'users'/'video' not in DenyGroups: FAIL (Please either remove these groups from DenyGroups directive or ensure rackconnect user is not in any denied groups)"
      fi
    else
      if [[ -z "${SSH_DENYGROUPS_RC}" ]]; then
        log PASS "Group 'rackconnect' not in DenyGroups: OK"
      else
        log FAIL "Group 'rackconnect' not in DenyGroups: FAIL (Please remove rackconnect group from DenyGroups directive)"
      fi
    fi
  fi


  ### AllowGroups
  SSH_ALLOWGROUPS="$("${SED}" -ne '/^\s*AllowGroups\s/Ip' /etc/ssh/sshd_config)"
  SSH_ALLOWGROUPS_NO_RC="$("${SED}" -ne '/^\s*AllowGroups\s/I{/\srackconnect\(\s\|$\)/I!p}' /etc/ssh/sshd_config)"
  if [[ -z "${SSH_ALLOWGROUPS}" ]]; then
    log PASS "No AllowGroups configuration directive found."
  else
    if [[ "${OS}" == "opensuse" ]]; then
      SSH_ALLOWGROUPS_NO_RC="$("${SED}" -ne '/^\s*AllowGroups\s/I{/\s\(users\|video\)\(\s\|$\)/I!p}' /etc/ssh/sshd_config)"
      if [[ -z "${SSH_ALLOWGROUPS_RC}" ]]; then
        log PASS "Default SuSE user groups 'users'/'video' in AllowGroups: OK"
      else
        log FAIL "Default SuSE user groups 'users'/'video' in AllowGroups: FAIL (Please either add one of these groups to AllowGroups directive or ensure rackconnect user is added to a group which is allowed)"
      fi
    else
      if [[ -z "${SSH_ALLOWGROUPS_RC}" ]]; then
        log PASS "Group 'rackconnect' in AllowGroups: OK"
      else
        log FAIL "Group 'rackconnect' in AllowGroups: FAIL (Please add rackconnect group to AllowGroups directive)"
      fi
    fi
  fi


  ### Config newer than process?
  if [ /etc/ssh/sshd_config -nt "/proc/$("${PGREP}" -o sshd)" ]; then
    log WARN "WARNING: sshd_config file is newer than sshd process.  We recommend restarting sshd to ensure running config matches disk file."
  fi
}

function test_wget() {
  WGET_RESULTS="$("${WGET}" --tries=5 --waitretry=10 --timeout=30 --no-check-certificate -O - \
                  "${WGET_TEST_URL}" --retry-connrefused 2>/dev/null)"

  if [[ "${WGET_RESULTS}" == "OK" ]]; then
    log PASS "Fetch test URL: OK"
  else
    log FAIL "Fetch test URL: FAIL (Please verify connectivity to public HTTPS URLs via 'wget' command.)"
  fi
}

function test_new_user() {
  "${USERADD}" -m "${TEST_ACCOUNTNAME}" &>/dev/null
  if [[ "$?" == "0" ]]; then
    log PASS "Test Account Creation: OK"
  else
    log FAIL "Test Account Creation: FAIL (Ensure root has permissions to add users via 'useradd')"
  fi


  # PW value isn't important, but use a good one just in case we fail to remove the account and
  # the customer forgets to remove it too.  Current nanosecond is added to make the PW unique.
  TEST_PASSWORD="5q=XB8g^t!2Z$("${DATE}" '+%N')"

  "${ECHO}" "${TEST_ACCOUNTNAME}:${TEST_PASSWORD}" | "${CHPASSWD}" &>/dev/null
  if [[ "$?" == "0" ]]; then
    log PASS "Test Account Password Change: OK"
  else
    log FAIL "Test Account Password Change: FAIL (Ensure root has permissions to change PWs via 'chpasswd')"
  fi


  "${USERDEL}" -r "${TEST_ACCOUNTNAME}" &>/dev/null
  if [[ "$?" == "0" ]]; then
    log PASS "Remove Test Account: OK"
  else
    log WARN "Remove Test Account: Failed (RC scripts do not need this permission, but please ensure that the '${TEST_ACCOUNTNAME}' account is removed.)"
  fi
}


function maincalls() {
  log INFO "$(date -u +'%D %r') UTC: BEGIN [LINUX] Rackspace RackConnect Image Validation Script ${ver}"
  log INFO ''

  log SECTION "OS Detection"
  os_type
  log SECTION_END "OS Detection"

  log SECTION "Root Permissions"
  test_is_root
  log SECTION_END "Root Permissions"

  # Note that test_programs() does what sanity_check() does in other scripts.  Any code that uses
  # which_cmd variables ("${SED}" et al) must come after test_programs().
  log SECTION     "Required Programs"
  test_programs
  log SECTION_END "Required Programs"

  log SECTION     "SELinux"
  test_selinux
  log SECTION_END "SELinux"

  log SECTION     "Required File/Directory Permissions"
  test_files
  log SECTION_END "Required File/Directory Permissions"

  log SECTION     "Available Disk Space"
  test_disk_space
  log SECTION_END "Available Disk Space"

  log SECTION     "IPTables"
  test_iptables
  log SECTION_END "IPTables"

  log SECTION     "SSH Daemon Config"
  test_sshd_config
  log SECTION_END "SSH Daemon Config"

  log SECTION     "Public HTTPS Connectivity"
  test_wget
  log SECTION_END "Public HTTPS Connectivity"

  log INFO "$(date -u +'%D %r') UTC: END [LINUX] Rackspace RackConnect Image Validation Script ${ver}"
}

# Begin running the actual script
maincalls

# If all works, exit with success
rba_exit "${EXIT_SUCCESS}"