Untitled

mail@pastecode.io avatar
unknown
sh
a month ago
22 kB
11
No Index
Never
#!/bin/bash
#[redacted]
# a library was loaded here that likely covers any mysterious gaps you find below

(( ${EUID:-$(id -u)} == 0 )) || {
  printf -- '%s\n' "This must be run as root or with sudo" >&2
  exit 1
}

# Ensure that we're on RHEL 7
#if ! grep -q "Red Hat.*release 7" /etc/redhat-release; then
# Cent added in for testing:
#if ! grep -Eq "Red Hat.*release 7|Cent.*7" /etc/redhat-release; then
#  printf -- '%s\n' "This check is specific to RHEL-7" >&2
#  exit 1
#fi

# Undocumented feature: Allow debugging if we need it
[[ "$*" = "*-x*" ]] && set -x

###############################################################################
# Variables

# Figure out the lowest boundary for the available UID range
uid_min=$(awk '/^UID_MIN/{print $2}' /etc/login.defs)
# Older releases of various Linux distros tended to use '500' as the minimum
# So if we can't find it in login.defs, we'll default to '500'
uid_min="${uid_min:-500}"

HOSTNAME="${HOSTNAME:-$(hostname -s)}"

conf_dir=/opt/redacted/etc/

###############################################################################
# Functions

# A functionalised version of "if command -v prog >/dev/null 2>&1; then..."
# Returns '0' if true, and '1' if not
exists-cmd() {
  [[ $(command -v "${1:?null parameter}") ]]
}

# Function to check that a filesystem exists.
# Returns '0' if true, and '1' if not
exists-fs() {
  mount | grep -q "on ${1:?fs unset} "
  #grep -q " ${1:?fs unset} " /proc/mounts #alternative
}

exists-fs-logvol() {
  # First we need to establish that the fs exists
  if get-fs-device "${1:?fs unset}" >/dev/null 2>&1; then
    # Next, we test whether it has its own vg/lv
    lvs "$(get-fs-device "${1}")" >/dev/null 2>&1
  # If the fs doesn't exist, we return 1
  else
    return 1
  fi
}

exists-iptables-rule() {
  local iptbl_policy="${1:?No iptables policy set}"
  local iptbl_protocol="${2:?No iptables protocol set}"
  local iptbl_port="${3:?No iptables port set}"
  local iptbl_action="${4:?No iptables action set}"
  iptables-save \
  | grep -q -- "${iptbl_policy}.*-p ${iptbl_protocol}.*--dport ${iptbl_port}.*${iptbl_action}"
}

exists-swap() {
  free | grep -q "^Swap:.*[1-9]"
}

# Test if a variable is set in a more idiomatic way than [[ -n/-z ]]
exists-var() {
  [[ "${1}" ]]
}

get-fs-device() {
  if exists-fs "${1:?fs unset}"; then
    df -hP "${1}" | tail -n 1 | awk '{print $1}'
  else
    return 1
  fi
}

# This function will print out the size of the given filesystem
get-fs-size() {
  df --output=size -m "${1:?fs unset}" | tail -n 1 | awk '{$1=$1};1'
}

# This function will print out the given filesystem type
get-fs-type() {
  df -PT "${1:?fs unset}" | tail -n 1 | awk '{print $2}'
}

# List home directories for all local users
get-home-dirs() {
  awk -F ':' -v min="${uid_min}" '{ if ( $3 >= min ) print $6 }' /etc/passwd
}

# List active network interface names
get-network-interfaces() {
  ip a | awk -F':' '/^[0-9]/{print $2}' | tr -d " " | grep -v "^lo$"
}

# Commentary for this function is available in [redacted]
get-memory-size() {
  local mem_total
  mem_total=$(awk '/MemTotal:/{ s+=$2 } END { printf("%0.f\n", s/1024) }' /proc/meminfo)

  (( ${#mem_total} == 0 )) && mem_total=$(free -m | awk '/Mem:/{print $2}')

  printf -- '%s\n' "${mem_total:-null}"
}

get-passwd-state() {
  case $(awk -F ':' -v user="${1}" '{if ($1 == user) print $2}' /etc/shadow) in
    (\$[1256]*)
      printf '%s\n' "P" # Password found
    ;;
    (\!|\*|\!\!|\!\*|\*LK\*)
      printf '%s\n' "LK" # Password or account locked
    ;;
    (NP|"")
      printf '%s\n' "NP" # No password set
    ;;
  esac
}

# Return 0 if a package is installed, 1 if it isn't
get-package-state() {
  rpmquery --quiet "${1:?No pkg supplied}" && return "$?"
}

# Check if a service is enabled
get-service-enabled() {
  systemctl list-unit-files | grep -q "${1:?svc unset}.*enabled"
  return "$?"
}

# Check if a service is active
get-service-active() {
  systemctl | grep -q "${1:?svc unset}.service.*running"
  return "$?"
}

get-ssh-keys() {
  while read -r home_dir; do
    for file in "${home_dir}"/.ssh/*; do
      ssh-keygen -lf "${file}" 2>/dev/null
    done
  done < <(get-home-dirs)
}

get-swap-size() {
  local swap_total
  swap_total=$(awk '/SwapTotal:/{ s+=$2 } END { printf("%0.f\n", s/1024) }' /proc/meminfo)

  (( ${#swap_total} == 0 )) && swap_total=$(free -m | awk '/Swap:/{print $2}')

  printf -- '%s\n' "${swap_total:-null}"  
}

get-system-accounts() {
  awk -F ':' -v min="${uid_min}" '{ if ( $3 < min ) print $1 }' /etc/passwd
}

get-system-uids() {
  awk -F ':' -v min="${uid_min}" '{ if ( $3 < min ) print $3 }' /etc/passwd
}

get-user-uids() {
  awk -F ':' -v min="${uid_min}" '{ if ( $3 >= min ) print $3 }' /etc/passwd
}

get-virtual-status() {
  if grep -q hypervisor /proc/cpuinfo; then
    printf '%s\n' "virtual"
  elif grep -qE '^flags.*svm|^flags.*vmx' /proc/cpuinfo; then
    printf '%s\n' "physical"
  else
    printf '%s\n' "unknown"
  fi
}

is-azure() {
  # All Azure hosts should have waagent.log
  grep -q -m 1 Azure /var/log/waagent.log 2>/dev/null && return "$?"
  # This may be a suitable alternative:
  #dmidecode | grep "String 1: \[MS_VM_CERT"
  # Does not work reliably:
  #blkid | grep -qE 'BEK|KEK' && return "$?"
  # The below might is not reliable, and technically it identifies HyperV:
  #dmesg | grep -q "Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine"
}

# From https://serverfault.com/a/903599
# See also: 
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html
is-aws() {
  local doc_url
  doc_url="http://169.254.169.254/latest/dynamic/instance-identity/document"
  if grep -q "^ec2" /sys/hypervisor/uuid 2>/dev/null; then
    return 0
  elif grep -q "^EC2" /sys/devices/virtual/dmi/id/product_uuid 2>/dev/null; then
    return 0
  elif curl -s -m 5 "${doc_url}" | grep -q availabilityZone; then
    return 0
  else
    return 1
  fi
}

# This function tries to determine if a host started life as a core install
# This one is going to be a bit tricky and will likely need further work
is-coreinstall() {
  # The approach I'm taking here is as follows:
  # grep for package groups (lines starting with @ e.g. @Base, @Core etc)
  # Quietly exclude '@^minimal' and '@core'.
  # If there's anything left, grep returns 0.
  # This indicates something like '@Base', so we flip the return code
  grep "^@" /root/anaconda-ks.cfg | grep -Eiqv '^@\^minimal|^@Core' && return 1
  # Otherwise, we return ok.
  return 0
}

# Function to test if a filesystem is encrypted
is-fs-encrypted() {
  blkid | grep -q "${1:?No fs supplied}.*crytpo" && return "$?"
}

task_success() {
  out_array+=( "$(printf '[-] %s\n' "${@}")" )
}

task_fail() {
  fail_array+=( "$(printf '[x] %s\n' "${@}")" )
}

# Test if a given filesystem meets a minimum desired size
# Usage: test-fs-size [fs] [type]
test-fs-size() {
  case "$#" in
    (2)
      if (( $(get-fs-size "${1}") >= ${2} )); then
        task_fail "${1} does not appear to be correctly sized"
      else
        task_success "${1} appears to be correctly sized"
      fi
    ;;
    (*)
      task_fail "Misuse of test-fs-size function detected"
      return 1
    ;;
  esac
}

# Test if a given filesystem is using the desired fs type
# Usage: test-fs-type [fs] [type]
test-fs-type() {
  case "$#" in
    (2)
      if [[ $(get-fs-type "${1}") = "${2}" ]]; then
        task_success "${1} appears to be a filesystem of type: ${2}"
      else
        task_fail "${1} does not appear to be a filesystem of type: ${2}"
      fi
    ;;
    (*)
      task_fail "Misuse of test-fs-type function detected"
      return 1
    ;;
  esac
}

test-swap-size() {
  # If swap is enabled, ensure it is correctly sized relative to system memory
  if (( $(get-memory-size) <= 12288 )); then
    # In this scenario, swap should be half the memory size (or more)
    swap_target=$(( $(get-memory-size) / 2 ))
    if (( $(get-swap-size) >= swap_target )); then
      task_success "Swap appears to be correctly sized"
    else
      task_fail "Swap does not appear to be correctly sized (minimum: $(get-memory-size)M)"
    fi
  else
    # Otherwise, swap should max out at 8G
    if (( $(get-swap-size) != 8096 )); then
      task_fail "Swap does not appear to be correctly sized (maximum: 8096M)"
    elif (( $(get-swap-size) == 8096 )); then
      task_success "Swap appears to be correctly sized"
    fi
  fi
}

main () {
  ###############################################################################
  # Disks and VolGroups, Swap and Encryption

  # /boot needs to be xfs 
  test-fs-type /boot xfs

  # and bigger than 1GB
  test-fs-size /boot 1024

  # If we're on AWS, we expect to be under a single / with no swap
  if is-aws; then
    task_success "${HOSTNAME} appears to be AWS based"
    if exists-swap; then
      task_success "Swap appears to be present on this system"
      # Ensure that it is correctly sized
      test-swap-size

      ###### TO DO : Figure out whether swap is on encrypted EBS/ephemeral
      # Something like the below may be of use
      #bdmUrl="http://169.254.169.254/2012-01-12/meta-data/block-device-mapping/"
      #for bd in $(curl -s ${bdmUrl}); do
      #  curl -s "${bdmUrl}${bd}" | \
      #    perl -pe 'BEGIN { $d = shift } s/^(\/dev\/)?(sd)?(.*)$/\/dev\/xvd$3 is $d\n/' "${bd}"
      #done | sort
      # See also https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
    else
      task_fail "Swap is either not present or enabled on this system"
    fi

  # If we're on Azure, we expect a single / and 
  # swapfile(s) in ephemeral disk under /mnt
  elif is-azure; then
    task_success "${HOSTNAME} appears to be Azure based"
    if exists-swap; then
      task_success "Swap appears to be present on this system"
      # Ensure that it is correctly sized
      test-swap-size

      # This was tricky to get right.  If there's anything left in the output of
      # swapon *after* excluding our expected output, then things are wrong
      if swapon --noheadings | grep -v "^/mnt" | grep -q .; then
        task_fail "Swap file or device found outside Azure ephemeral storage"
      else
        task_success "Swap is correctly placed on Azure ephemeral storage"
      fi
    else
      task_fail "Swap is either not present or enabled on this system"
    fi

  # Otherwise, we expect our own filesystem layout
  else
    task_success "${HOSTNAME} appears to be traditional/on-premise"
    for fs in / /home /tmp /var /var/tmp /var/crash /var/log /var/log/audit; do
      if exists-fs "${fs}"; then
        task_success "${fs} appears to be its own filesystem"
        if exists-fs-logvol "${fs}"; then
          task_success "${fs} appears to have its own Logical Volume"
        else
          task_fail "${fs} does not appear to have its own Logical Volume"
        fi
        # All of these filesystem targets should be xfs
        test-fs-type "${fs}" xfs
        # And we test the following disk sizes
        [[ "${fs}" = "/" ]] && test-fs-size / 4096
        [[ "${fs}" = "/home" ]] && test-fs-size /home 4096
        [[ "${fs}" = "/tmp" ]] && test-fs-size /tmp 2048
        [[ "${fs}" = "/var" ]] && test-fs-size /var 8192
        [[ "${fs}" = "/var/tmp" ]] && test-fs-size /var/tmp 2048
        [[ "${fs}" = "/var/log" ]] && test-fs-size /var/log 4096
        [[ "${fs}" = "/var/log/audit" ]] && test-fs-size /var/log/audit 4096
      else
        task_fail "${fs} does not appear to be its own filesystem"
      fi
    done

    # Ensure /tmp is encrypted
    if exists-fs /tmp; then
      if is-fs-encrypted /tmp; then
        task_success "/tmp appears to be encrypted"
      else
        task_fail "/tmp does not appear to be encrypted"
      fi
    fi

    # /var/crash needs to be present and 1.1x the size of memory
    if exists-fs /var/crash; then
      crash_target=$(bc <<< "$(get-memory-size) * 1.1")
      crash_size=$(get-fs-size /var/crash)
      if (( crash_size != crash_target )); then
        task_fail "/var/crash does not appear to be correctly sized (${crash_size} != ${crash_target})"
      else
        task_success "/var/crash appears to be correctly sized"
      fi
    # If it doesn't exist, we send a failure message
    else
      task_fail "/var/crash does not appear to be its own filesystem"
    fi

    # Ensure that swap is present and enabled
    if exists-swap; then
      task_success "Swap appears to be present on this system"
      # Ensure that it is correctly sized
      test-swap-size
      # Ensure that swap is encrypted
      if is-fs-encrypted swap; then
        task_success "Swap appears to be encrypted"
      else
        task_fail "Swap does not appear to be encrypted"
      fi
    else
      task_fail "Swap is either not present or enabled on this system"
    fi
  fi

  ###############################################################################
  # Software

  # This test uses task_success for both outputs as it's informational
  if is-coreinstall; then
    task_success "This host appears to have started life as a Core install"
  else
    task_success "This host does not appear to have started life as a Core install"
  fi

  # Optional packages as defined by ${conf_dir}/check_soe_pkgopt.conf
  # We want these packages to be installed
  if [[ -r "${conf_dir}/check_soe_pkgopt.conf" ]]; then
    while read -r pkg; do
      # If get-package-state returns 1, add the package to a list
      if ! get-package-state "${pkg}"; then
        pkg_fail="${pkg},${pkg_fail}"
      fi
    done < "${conf_dir}/check_soe_pkgopt.conf"
    
    # Remove any errant trailing commas
    pkg_fail="${pkg_fail%,}"

    # If the pkg_fail variable has any content, then we fail
    if exists-var "${pkg_fail}"; then
      task_fail "Optional packages appear to be missing: ${pkg_fail}"
    else
      task_success "All optional packages appear to be present"
    fi
  else
    task_fail "Optional package listing not provided or not readable"
  fi

  # Clear these two vars, ready for the next round of testing
  unset pkg pkg_fail

  # Excluded packages as defined by ${conf_dir}/check_soe_pkgexcl.conf
  # We want these packages to *not* be installed, so we take an opposite approach
  if [[ -r "${conf_dir}/check_soe_pkgexcl.conf" ]]; then
    while read -r pkg; do
      # If get-package-state returns 0, add the package to a list
      if get-package-state "${pkg}"; then
        pkg_fail="${pkg},${pkg_fail}"
      fi
    done < "${conf_dir}/check_soe_pkgexcl.conf"
    
    # Remove any errant trailing commas
    pkg_fail="${pkg_fail%,}"

    # If the pkg_fail variable has any content, then we fail
    if exists-var "${pkg_fail}"; then
      task_fail "Unwanted packages appear to be installed: ${pkg_fail}"
    else
      task_success "All unwanted packages appear to be absent"
    fi
  else
    task_fail "Package exclusion listing not provided or not readable"
  fi

  ###############################################################################
  # Firewall

  # Start by checking if we're running iptables
  if get-service-enabled iptables; then
    task_success "This host appears to be configured for 'iptables'"

    # Check that the service is active
    if get-service-active iptables; then
      task_success "'iptables' appears to be running"
    else
      task_fail "'iptables' does not appear to be running"
    fi

    # Now check our default policies
    if iptables -L -n | grep -q "INPUT (policy DROP)"; then
      task_success "'iptables' INPUT policy appears to be correct"
    else
      task_fail "'iptables' INPUT policy appears to be incorrect (expected: policy DROP)"
    fi

    if iptables -L -n | grep -q "FORWARD (policy DROP)"; then
      task_success "'iptables' FORWARD policy appears to be correct"
    else
      task_fail "'iptables' FORWARD policy appears to be incorrect (expected: policy DROP)"
    fi

    if iptables -L -n | grep -q "OUTPUT (policy ACCEPT)"; then
      task_success "'iptables' OUTPUT policy appears to be correct"
    else
      task_fail "'iptables' OUTPUT policy appears to be incorrect (expected: policy ACCEPT)"
    fi

    # Now test for the default rules
    # ssh, nrpe and check_mk:
    for port in 22 5556 6556; do
      if exists-iptables-rule INPUT tcp "${port}" ACCEPT; then
        task_success "Default rule for port ${port} exists"
      else
        task_fail "Default rule for port ${port} does not exist"
      fi
    done
    # snmp:
    if exists-iptables-rule INPUT udp 161 ACCEPT; then
      task_success "Default rule for port 161 exists"
    else
      task_fail "Default rule for port 161 does not exist"
    fi

  # Is firewalld enabled and running
  elif get-service-enabled firewalld; then
    task_success "This host appears to be configured for 'firewalld'"

    # Check that the service is active
    if get-service-active firewalld; then
      task_success "'firewalld' appears to be running"
    else
      task_fail "'firewalld' does not appear to be running"
    fi

    # Supposedly the same check method from iptables can be used
    # If not, I'll have to re-invent this...
    # ssh, nrpe and check_mk:
    for port in 22 5556 6556; do
      if exists-iptables-rule INPUT tcp "${port}" ACCEPT; then
        task_success "Default rule for port ${port} exists"
      else
        task_fail "Default rule for port ${port} does not exist"
      fi
    done
    # snmp:
    if exists-iptables-rule INPUT udp 161 ACCEPT; then
      task_success "Default rule for port 161 exists"
    else
      task_fail "Default rule for port 161 does not exist"
    fi

  # No firewall found
  else
    task_fail "This host does not appear to have a firewall configured"
  fi

  ###############################################################################
  # Networking

  case $(get-virtual-status) in
    (virtual)
      #if host is a vm, network interfaces should use ethx standard
      task_success "Host appears to be a virtual machine"
      if get-network-interfaces | grep -q eth; then
        task_success "Host appears to be correctly using ethx network naming standard"
      else
        task_fail "Host does not appear to be using ethx network naming standard"
      fi
    ;;
    (physical)
      #if host is physical, network interfaces should use "consistent naming" standard
      task_success "Host appears to be a physical machine"
      if get-network-interfaces | grep -q eth; then
        task_fail "Host does not appear to be using consistent naming standard"
      else
        # Careful wording - we don't assume that consistent naming is being used
        # All we have actually proved here is that ethx is not being used...
        task_success "Host does not appear to be using ethx network naming standard"
      fi
    ;;
    (unknown|*)
      task_fail "Could not determine if host is virtual or physical"
    ;;
  esac

  ###############################################################################
  # Security

  # Ensure SELinux is set to Enforcing mode
  if [[ "$(getenforce)" = "Enforcing" ]]; then
    task_success "SELinux appears to be in Enforcing mode"
  else
    task_fail "SELinux appears to not be in Enforcing mode"
  fi

  # Ensure TCP Wrappers is installed and has an ssh allow rule
  if rpm -qa | grep -q tcp_wrappers; then
    task_success "TCP Wrappers appears to be installed"
    if grep -qi "ssh.*all" /etc/hosts.allow; then
      task_success "TCP Wrappers ssh rule appears to exist"
    else
      task_fail "TCP Wrappers ssh rule does not appear to exist"
    fi
  else
    task_fail "TCP Wrappers does not appear to be installed"
  fi

  ###############################################################################
  # SSH

  # Check for certain key types
  if get-ssh-keys | grep -q ECDSA; then
    task_fail "ECDSA ssh keys were found on this host"
  else
    task_success "ECDSA ssh keys were not found on this host"
  fi

  if get-ssh-keys | grep RSA | grep -qv ^4096; then
    task_fail "RSA ssh keys with less than 4096 bits were found on this host"
  else
    task_success "RSA ssh keys on this host appear to be 4096 bits"
  fi

  ###############################################################################
  # Accounts
  
  # No system accounts with passwords
  for acct in $(get-system-accounts | grep -v root); do
    if [[ "$(get-passwd-state "${acct}")" == "P" ]]; then
      sys_acct_pwd=found
      task_fail "A system account was found with a configured password: ${acct}"
    fi
  done

  [[ ! "${sys_acct_pwd}" = "found" ]] && task_success "No system accounts with configured passwords"

  # No duplicate UID's
  if [[ $(get-system-uids | uniq -D &>/dev/null) ]]; then
    task_fail "Duplicate system UID's were found"
  else
    task_success "Duplicate system UID's were not found"
  fi

  if [[ $(get-user-uids | uniq -D &>/dev/null) ]]; then
    task_fail "Duplicate user UID's were found"
  else
    task_success "Duplicate user UID's were not found"
  fi

  # No duplicate home directories in /etc/passwd
  if [[ $(get-home-dirs | uniq -D &>/dev/null) ]]; then
    task_fail "Duplicate home directories exist on this host"
  else
    task_success "Duplicate home directories were not found on this host"
  fi
}

# Call the main function
main

if (( ${#fail_array[@]} > 0 )); then
  printf -- '%s\n' "Issues=${#fail_array[@]} This host is not compliant with the [redacted] SOE" \
    "${fail_array[@]}" \
    "${out_array[@]}"
else
  printf -- '%s\n' "Issues=0 This host appears to be compliant with the [redacted] SOE" \
    "${out_array[@]}"
fi