Untitled
unknown
sh
2 years ago
22 kB
89
No Index
#!/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[@]}"
fiEditor is loading...