From 900481b424a5b31b993766618f37899022143e65 Mon Sep 17 00:00:00 2001 From: Damien Cavagnini Date: Thu, 30 Jul 2026 16:01:19 +0200 Subject: [PATCH] add new debian 12 scripts bin/hardening/local_login_banner.sh -> 1.6.2 bin/hardening/nis_server_disabled.sh -> 2.1.10 bin/hardening/pam_unix_strong_hash_algorithm.sh -> 5.3.3.4.3 bin/hardening/password_last_change_past.sh -> 5.4.1.6 bin/hardening/graphical_warning_banners.sh -> 1.7.2 bin/hardening/find_unowned_ungrouped_files.sh -> 7.1.12 -> This is a mix of existing find_unowned_files.sh and find_ungrouped_files.sh, which are now in a single recommendation bin/hardening/find_world_writable_files_and_folders.sh -> 7.1.11 -> this is a mix of existing find_world_writable_files.sh and sticky_bit_world_writable_folder.sh --- AGENTS.md | 8 + bin/hardening/find_unowned_ungrouped_files.sh | 136 +++++++++++++ .../find_world_writable_files_and_folders.sh | 111 +++++++++++ bin/hardening/graphical_warning_banners.sh | 188 ++++++++++++++++++ bin/hardening/local_login_banner.sh | 101 ++++++++++ bin/hardening/nis_server_disabled.sh | 130 ++++++++++++ .../pam_unix_strong_hash_algorithm.sh | 120 +++++++++++ bin/hardening/password_last_change_past.sh | 54 ++++- .../hardening/find_unowned_ungrouped_files.sh | 65 ++++++ .../find_world_writable_files_and_folders.sh | 46 +++++ tests/hardening/graphical_warning_banners.sh | 96 +++++++++ tests/hardening/local_login_banner.sh | 38 ++++ tests/hardening/nis_server_disabled.sh | 37 ++++ .../pam_unix_strong_hash_algorithm.sh | 37 ++++ tests/hardening/password_last_change_past.sh | 50 ++++- 15 files changed, 1210 insertions(+), 7 deletions(-) create mode 100755 bin/hardening/find_unowned_ungrouped_files.sh create mode 100755 bin/hardening/find_world_writable_files_and_folders.sh create mode 100755 bin/hardening/graphical_warning_banners.sh create mode 100755 bin/hardening/local_login_banner.sh create mode 100755 bin/hardening/nis_server_disabled.sh create mode 100755 bin/hardening/pam_unix_strong_hash_algorithm.sh create mode 100644 tests/hardening/find_unowned_ungrouped_files.sh create mode 100644 tests/hardening/find_world_writable_files_and_folders.sh create mode 100644 tests/hardening/graphical_warning_banners.sh create mode 100644 tests/hardening/local_login_banner.sh create mode 100644 tests/hardening/nis_server_disabled.sh create mode 100644 tests/hardening/pam_unix_strong_hash_algorithm.sh diff --git a/AGENTS.md b/AGENTS.md index 42c4aa77..cfb307d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,6 +107,14 @@ Never define `FNRET` in scripts. Use explicit `if/then` — never `command && { Global variable names must be unique across all scripts. Use a script-specific prefix (e.g. `GDM_AR_`, `AUDIT_LOG_`). +### State variable hygiene + +- Keep a single source of truth for a given state in a script. Do not duplicate the same status in both a local variable and a global variable. +- Avoid redundant assignments to default values (for example, setting a variable to `0` in a branch when it is already initialized to `0`). +- Use global state variables in `apply()` only when they are set by `audit()`. +- Prefer declaring state globals once at file scope with their default value, instead of reinitializing them to the same value at the start of `audit()`. +- Reinitialize a state variable in `audit()` only when its value must be explicitly cleared or rebuilt for that run (for example strings, lists, or accumulators). + ### Package detection (multiple packages) ```bash diff --git a/bin/hardening/find_unowned_ungrouped_files.sh b/bin/hardening/find_unowned_ungrouped_files.sh new file mode 100755 index 00000000..63d46048 --- /dev/null +++ b/bin/hardening/find_unowned_ungrouped_files.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +# run-shellcheck +# +# CIS Debian Hardening +# + +# +# Ensure no unowned or ungrouped files or directories exist (Scored) +# + +set -e # One error, it's over +set -u # One variable unset, it's over + +# shellcheck disable=2034 +HARDENING_LEVEL=2 +# shellcheck disable=2034 +DESCRIPTION="Ensure no unowned or ungrouped files or directories exist." + +UNOWNED_UNGROUPED_USER='root' +UNOWNED_UNGROUPED_GROUP='root' +IGNORED_PATH='' + +# find emits following error if directory or file disappears during +# tree traversal: find: '/tmp/xxx': No such file or directory +FIND_IGNORE_NOSUCHFILE_ERR=false + +# Global audit state reused by apply() +UNOWNED_UNGROUPED_UNOWNED_RESULT='' +UNOWNED_UNGROUPED_UNGROUPED_RESULT='' +UNOWNED_UNGROUPED_IS_COMPLIANT=1 + +# This function will be called if the script status is on enabled / audit mode +audit() { + local fs_names + + info "Checking if there are unowned or ungrouped files" + + if [ -n "$IGNORED_PATH" ]; then + # maybe IGNORED_PATH allows us to filter out some FS + fs_names=$(df --local -P | awk '{if (NR!=1) print $6}' | grep -vE "$IGNORED_PATH") + + [ "$FIND_IGNORE_NOSUCHFILE_ERR" = true ] && set +e + # shellcheck disable=SC2086 + UNOWNED_UNGROUPED_UNOWNED_RESULT=$($SUDO_CMD find $fs_names -xdev -ignore_readdir_race -nouser -regextype 'egrep' ! -regex "$IGNORED_PATH" -print 2>/dev/null) + # shellcheck disable=SC2086 + UNOWNED_UNGROUPED_UNGROUPED_RESULT=$($SUDO_CMD find $fs_names -xdev -ignore_readdir_race -nogroup -regextype 'egrep' ! -regex "$IGNORED_PATH" -print 2>/dev/null) + [ "$FIND_IGNORE_NOSUCHFILE_ERR" = true ] && set -e + else + fs_names=$(df --local -P | awk '{if (NR!=1) print $6}') + + [ "$FIND_IGNORE_NOSUCHFILE_ERR" = true ] && set +e + # shellcheck disable=SC2086 + UNOWNED_UNGROUPED_UNOWNED_RESULT=$($SUDO_CMD find $fs_names -xdev -ignore_readdir_race -nouser -print 2>/dev/null) + # shellcheck disable=SC2086 + UNOWNED_UNGROUPED_UNGROUPED_RESULT=$($SUDO_CMD find $fs_names -xdev -ignore_readdir_race -nogroup -print 2>/dev/null) + [ "$FIND_IGNORE_NOSUCHFILE_ERR" = true ] && set -e + fi + + if [ -z "$UNOWNED_UNGROUPED_UNOWNED_RESULT" ] && [ -z "$UNOWNED_UNGROUPED_UNGROUPED_RESULT" ]; then + ok "No unowned or ungrouped files found" + UNOWNED_UNGROUPED_IS_COMPLIANT=0 + return + fi + + UNOWNED_UNGROUPED_IS_COMPLIANT=1 + crit "Some files are unowned and/or ungrouped are present" + + if [ -n "$UNOWNED_UNGROUPED_UNOWNED_RESULT" ]; then + crit "Some unowned files are present" + fi + + if [ -n "$UNOWNED_UNGROUPED_UNGROUPED_RESULT" ]; then + crit "Some ungrouped files are present" + fi + + # shellcheck disable=SC2001 + FORMATTED_RESULT=$(printf '%s\n%s\n' "$UNOWNED_UNGROUPED_UNOWNED_RESULT" "$UNOWNED_UNGROUPED_UNGROUPED_RESULT" | sed '/^$/d' | sort | uniq | tr '\n' ' ') + crit "$FORMATTED_RESULT" +} + +# This function will be called if the script status is on enabled mode +apply() { + if [ "$UNOWNED_UNGROUPED_IS_COMPLIANT" -eq 0 ]; then + ok "No unowned or ungrouped files found, nothing to apply" + return + fi + + if [ -n "$UNOWNED_UNGROUPED_UNOWNED_RESULT" ]; then + warn "Applying chown on all unowned files in the system" + printf '%s\n' "$UNOWNED_UNGROUPED_UNOWNED_RESULT" | sed '/^$/d' | xargs chown "$UNOWNED_UNGROUPED_USER" + fi + + if [ -n "$UNOWNED_UNGROUPED_UNGROUPED_RESULT" ]; then + warn "Applying chgrp on all ungrouped files in the system" + printf '%s\n' "$UNOWNED_UNGROUPED_UNGROUPED_RESULT" | sed '/^$/d' | xargs chgrp "$UNOWNED_UNGROUPED_GROUP" + fi + + ok "Ownership remediation commands have been applied" +} + +# This function will check config parameters required +check_config() { + : +} + +create_config() { + cat </dev/null) + # shellcheck disable=SC2086 + WORLD_WRITABLE_DIRS_RESULT=$($SUDO_CMD find $FS_NAMES -xdev -ignore_readdir_race -type d \( -perm -0002 -a ! -perm -1000 \) -regextype 'egrep' ! -regex $EXCLUDED -print 2>/dev/null) + [ "${FIND_IGNORE_NOSUCHFILE_ERR}" = true ] && set -e + else + FS_NAMES=$(df --local -P | awk '{if (NR!=1) print $6}') + + [ "${FIND_IGNORE_NOSUCHFILE_ERR}" = true ] && set +e + # shellcheck disable=SC2086 + WORLD_WRITABLE_FILES_RESULT=$($SUDO_CMD find $FS_NAMES -xdev -ignore_readdir_race -type f -perm -0002 -print 2>/dev/null) + # shellcheck disable=SC2086 + WORLD_WRITABLE_DIRS_RESULT=$($SUDO_CMD find $FS_NAMES -xdev -ignore_readdir_race -type d \( -perm -0002 -a ! -perm -1000 \) -print 2>/dev/null) + [ "${FIND_IGNORE_NOSUCHFILE_ERR}" = true ] && set -e + fi + + if [ -n "$WORLD_WRITABLE_FILES_RESULT" ] || [ -n "$WORLD_WRITABLE_DIRS_RESULT" ]; then + crit "Some world writable files or directories are present" + if [ -n "$WORLD_WRITABLE_FILES_RESULT" ]; then + crit "Some world writable files are present" + fi + if [ -n "$WORLD_WRITABLE_DIRS_RESULT" ]; then + crit "Some world writable directories are not on sticky bit mode" + fi + # shellcheck disable=SC2001 + FORMATTED_RESULT=$(printf '%s\n%s\n' "$WORLD_WRITABLE_FILES_RESULT" "$WORLD_WRITABLE_DIRS_RESULT" | sed '/^$/d' | sed "s/ /\n/g" | sort | uniq | tr '\n' ' ') + crit "$FORMATTED_RESULT" + else + ok "No world writable files or directories requiring remediation found" + fi +} + +# This function will be called if the script status is on enabled mode +apply() { + if [ -n "$WORLD_WRITABLE_FILES_RESULT" ]; then + warn "chmoding o-w all world writable files in the system" + printf '%s\n' "$WORLD_WRITABLE_FILES_RESULT" | sed '/^$/d' | xargs chmod o-w + fi + + if [ -n "$WORLD_WRITABLE_DIRS_RESULT" ]; then + warn "Setting sticky bit on world writable directories" + printf '%s\n' "$WORLD_WRITABLE_DIRS_RESULT" | sed '/^$/d' | xargs chmod a+t + fi + + if [ -n "$WORLD_WRITABLE_FILES_RESULT" ] || [ -n "$WORLD_WRITABLE_DIRS_RESULT" ]; then + ok "World writable files and directories remediation applied" + else + ok "No world writable files or directories requiring remediation found, nothing to apply" + fi +} + +# This function will check config parameters required +check_config() { + : +} + +# Source Root Dir Parameter +if [ -r /etc/default/cis-hardening ]; then + # shellcheck source=../../debian/default + . /etc/default/cis-hardening +fi +if [ -z "$CIS_LIB_DIR" ]; then + echo "There is no /etc/default/cis-hardening file nor cis-hardening directory in current environment." + echo "Cannot source CIS_LIB_DIR variable, aborting." + exit 128 +fi + +# Main function, will call the proper functions given the configuration (audit, enabled, disabled) +if [ -r "${CIS_LIB_DIR}"/main.sh ]; then + # shellcheck source=../../lib/main.sh + . "${CIS_LIB_DIR}"/main.sh +else + echo "Cannot find main.sh, have you correctly defined your root directory? Current value is $CIS_LIB_DIR in /etc/default/cis-hardening" + exit 128 +fi diff --git a/bin/hardening/graphical_warning_banners.sh b/bin/hardening/graphical_warning_banners.sh new file mode 100755 index 00000000..75a65d05 --- /dev/null +++ b/bin/hardening/graphical_warning_banners.sh @@ -0,0 +1,188 @@ +#!/bin/bash + +# run-shellcheck +# +# CIS Debian Hardening +# + +# +# Ensure GDM login banner is configured (Scored) +# + +set -e # One error, it's over +set -u # One variable unset, it's over + +# shellcheck disable=2034 +HARDENING_LEVEL=3 +# shellcheck disable=2034 +DESCRIPTION="Set graphical warning banner." + +PACKAGES='gdm gdm3' +DCONF_PROFILE_DIR='/etc/dconf/profile' +DCONF_DB_BASE_DIR='/etc/dconf/db' +BANNER_MESSAGE_TEXT="'Authorized uses only. All activity may be monitored and reported'" + +GDM_LB_INSTALLED=1 +GDM_LB_ERROR_COUNT=0 +GDM_LB_PROFILE_NAME="" +GDM_LB_PROFILE_FILE="" +GDM_LB_KEYFILE="" +GDM_LB_DBFILE="" + +# This function will be called if the script status is on enabled / audit mode +audit() { + local l_package + local l_tmp_keyfile + + GDM_LB_INSTALLED=1 + GDM_LB_ERROR_COUNT=0 + GDM_LB_PROFILE_NAME="" + GDM_LB_PROFILE_FILE="" + GDM_LB_KEYFILE="" + GDM_LB_DBFILE="" + + for l_package in $PACKAGES; do + is_pkg_installed "$l_package" + if [ "$FNRET" = 0 ]; then + ok "Package $l_package is installed" + GDM_LB_INSTALLED=0 + break + fi + done + + if [ "$GDM_LB_INSTALLED" -ne 0 ]; then + ok "GNOME Desktop Manager is not installed on the system - Recommendation is not applicable" + return + fi + + l_tmp_keyfile=$(grep -Prils -- '^\h*banner-message-enable\h*=|^\h*banner-message-text\h*=' "$DCONF_DB_BASE_DIR"/*.d 2>/dev/null | head -n 1 || true) + GDM_LB_KEYFILE="$l_tmp_keyfile" + + if [ -z "$GDM_LB_KEYFILE" ]; then + GDM_LB_ERROR_COUNT=$((GDM_LB_ERROR_COUNT + 1)) + crit "banner-message options are not configured in $DCONF_DB_BASE_DIR/*.d" + else + GDM_LB_PROFILE_NAME=$(awk -F/ '{split($(NF-1),a,".");print a[1]}' <<<"$GDM_LB_KEYFILE") + GDM_LB_PROFILE_FILE="$DCONF_PROFILE_DIR/$GDM_LB_PROFILE_NAME" + GDM_LB_DBFILE="$DCONF_DB_BASE_DIR/$GDM_LB_PROFILE_NAME" + + if grep -Pq -- '^\h*banner-message-enable\h*=\h*true\b' "$GDM_LB_KEYFILE"; then + ok "banner-message-enable=true is set in $GDM_LB_KEYFILE" + else + GDM_LB_ERROR_COUNT=$((GDM_LB_ERROR_COUNT + 1)) + crit "banner-message-enable=true is not set in $GDM_LB_KEYFILE" + fi + + if grep -Pq -- '^\h*banner-message-text\h*=\h*.+$' "$GDM_LB_KEYFILE"; then + ok "banner-message-text is set in $GDM_LB_KEYFILE" + else + GDM_LB_ERROR_COUNT=$((GDM_LB_ERROR_COUNT + 1)) + crit "banner-message-text is not set in $GDM_LB_KEYFILE" + fi + + if [ -r "$GDM_LB_PROFILE_FILE" ] && grep -Pq -- "^\h*system-db:$GDM_LB_PROFILE_NAME\b" "$GDM_LB_PROFILE_FILE"; then + ok "The profile $GDM_LB_PROFILE_FILE exists and references system-db:$GDM_LB_PROFILE_NAME" + else + GDM_LB_ERROR_COUNT=$((GDM_LB_ERROR_COUNT + 1)) + crit "The profile $GDM_LB_PROFILE_FILE is missing or does not reference system-db:$GDM_LB_PROFILE_NAME" + fi + + if [ -f "$GDM_LB_DBFILE" ]; then + ok "The dconf database file $GDM_LB_DBFILE exists" + else + GDM_LB_ERROR_COUNT=$((GDM_LB_ERROR_COUNT + 1)) + crit "The dconf database file $GDM_LB_DBFILE does not exist" + fi + fi + + if [ "$GDM_LB_ERROR_COUNT" -eq 0 ]; then + ok "GDM login banner is configured" + fi +} + +# This function will be called if the script status is on enabled mode +apply() { + local l_gpname + local l_kfile + local l_profile_file + local l_db_dir + local l_db_file + + if [ "$GDM_LB_INSTALLED" -ne 0 ]; then + ok "GNOME Desktop Manager is not installed on the system - no remediation required" + return + fi + + l_gpname="${GDM_LB_PROFILE_NAME:-gdm}" + l_kfile="${GDM_LB_KEYFILE:-$DCONF_DB_BASE_DIR/$l_gpname.d/01-banner-message}" + l_profile_file="$DCONF_PROFILE_DIR/$l_gpname" + l_db_dir="$DCONF_DB_BASE_DIR/$l_gpname.d" + l_db_file="$DCONF_DB_BASE_DIR/$l_gpname" + + mkdir -p "$DCONF_PROFILE_DIR" + if [ ! -f "$l_profile_file" ]; then + info "Creating profile $l_profile_file" + { + echo "user-db:user" + echo "system-db:$l_gpname" + echo "file-db:/usr/share/$l_gpname/greeter-dconf-defaults" + } >"$l_profile_file" + elif ! grep -Pq -- "^\h*system-db:$l_gpname\b" "$l_profile_file"; then + info "Adding system-db:$l_gpname to $l_profile_file" + echo "system-db:$l_gpname" >>"$l_profile_file" + fi + + mkdir -p "$l_db_dir" + if [ -f "$l_kfile" ]; then + sed -i '/^\s*banner-message-enable\s*=/d' "$l_kfile" + sed -i '/^\s*banner-message-text\s*=/d' "$l_kfile" + else + echo "[org/gnome/login-screen]" >"$l_kfile" + fi + + if ! grep -q '^\[org/gnome/login-screen\]' "$l_kfile"; then + echo "[org/gnome/login-screen]" >>"$l_kfile" + fi + + sed -i '/^\[org\/gnome\/login-screen\]/a banner-message-enable=true' "$l_kfile" + sed -i "/^\[org\/gnome\/login-screen\]/a banner-message-text=$BANNER_MESSAGE_TEXT" "$l_kfile" + + if command -v dconf >/dev/null 2>&1; then + dconf update + else + warn "dconf command not found, cannot update dconf database" + fi + + if [ -f "$l_db_file" ]; then + ok "GDM login banner has been configured" + else + warn "Configuration files were updated, but dconf database file $l_db_file is still missing" + fi +} + +# This function will check config parameters required +check_config() { + if [ -z "$BANNER_MESSAGE_TEXT" ]; then + BANNER_MESSAGE_TEXT="'Authorized uses only. All activity may be monitored and reported'" + fi +} + +# Source Root Dir Parameter +if [ -r /etc/default/cis-hardening ]; then + # shellcheck source=../../debian/default + . /etc/default/cis-hardening +fi +if [ -z "$CIS_LIB_DIR" ]; then + echo "There is no /etc/default/cis-hardening file nor cis-hardening directory in current environment." + echo "Cannot source CIS_LIB_DIR variable, aborting." + exit 128 +fi + +# Main function, will call the proper functions given the configuration (audit, enabled, disabled) +if [ -r "${CIS_LIB_DIR}"/main.sh ]; then + # shellcheck source=../../lib/main.sh + . "${CIS_LIB_DIR}"/main.sh +else + echo "Cannot find main.sh, have you correctly defined your root directory? Current value is $CIS_LIB_DIR in /etc/default/cis-hardening" + exit 128 +fi diff --git a/bin/hardening/local_login_banner.sh b/bin/hardening/local_login_banner.sh new file mode 100755 index 00000000..3a37be52 --- /dev/null +++ b/bin/hardening/local_login_banner.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# run-shellcheck +# +# CIS Debian Hardening +# + +# +# Ensure local login warning banner is configured properly (Automated) +# + +set -e +set -u + +# shellcheck disable=2034 +HARDENING_LEVEL=1 +# shellcheck disable=2034 +DESCRIPTION="Ensure local login warning banner is configured properly" + +BLB_FILE='/etc/issue' +BLB_DISTRO_NAME='' +BLB_PATTERN_OK=1 + +audit() { + if [ ! -f "$BLB_FILE" ]; then + crit "$BLB_FILE does not exist" + return + fi + + # Get the distro name from /etc/os-release + if [ -f /etc/os-release ]; then + BLB_DISTRO_NAME=$(grep '^ID=' /etc/os-release | cut -d= -f2 | sed -e 's/"//g') + fi + + # Check for OS information patterns: \v, \r, \m, \s and distro name + # Build the pattern: (\v|\r|\m|\s|distro_name) + local pattern="(\\\\v|\\\\r|\\\\m|\\\\s" + if [ -n "$BLB_DISTRO_NAME" ]; then + pattern="${pattern}|${BLB_DISTRO_NAME}" + fi + pattern="${pattern})" + + if grep -E -i "$pattern" "$BLB_FILE" >/dev/null 2>&1; then + crit "OS information patterns found in $BLB_FILE" + BLB_PATTERN_OK=1 + else + ok "No OS information patterns found in $BLB_FILE" + BLB_PATTERN_OK=0 + fi +} + +apply() { + if [ ! -f "$BLB_FILE" ]; then + warn "$BLB_FILE does not exist, creating with default banner" + cat >"$BLB_FILE" <<'EOF' +Authorized access to this system is restricted to authorized users only. +All activity is monitored and logged. +By accessing this system, you agree that your actions may be monitored and recorded. +Unauthorized access attempts will be logged and may result in legal action. +EOF + return + fi + + if [ "$BLB_PATTERN_OK" -ne 0 ]; then + backup_file "$BLB_FILE" + + # Remove lines containing backslash and v/r/m/s (mingetty escape sequences) + sed -i '/\\v/d; /\\r/d; /\\m/d; /\\s/d' "$BLB_FILE" || true + + # Remove lines containing the distro name (case-insensitive) + if [ -n "$BLB_DISTRO_NAME" ]; then + sed -i "/${BLB_DISTRO_NAME}/I d" "$BLB_FILE" || true + fi + + info "Removed OS information from $BLB_FILE" + fi +} + +check_config() { + : +} + +# Source Root Dir Parameter +if [ -r /etc/default/cis-hardening ]; then + # shellcheck source=../../debian/default + . /etc/default/cis-hardening +fi +if [ -z "${CIS_LIB_DIR}" ]; then + echo "There is no /etc/default/cis-hardening file nor cis-hardening directory in current environment." + echo "Cannot source CIS_LIB_DIR variable, aborting." + exit 128 +fi + +# Main function, will call the proper functions given the configuration (audit, enabled, disabled) +if [ -r "${CIS_LIB_DIR}"/main.sh ]; then + # shellcheck source=../../lib/main.sh + . "${CIS_LIB_DIR}"/main.sh +else + echo "Cannot find main.sh, have you correctly defined your root directory? Current value is ${CIS_LIB_DIR} in /etc/default/cis-hardening" + exit 128 +fi diff --git a/bin/hardening/nis_server_disabled.sh b/bin/hardening/nis_server_disabled.sh new file mode 100755 index 00000000..c7835241 --- /dev/null +++ b/bin/hardening/nis_server_disabled.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +# run-shellcheck +# +# CIS Debian Hardening +# + +# +# Ensure nis server services are not in use (Automated) +# + +set -e # One error, it's over +set -u # One variable unset, it's over + +# shellcheck disable=2034 +HARDENING_LEVEL=1 +# shellcheck disable=2034 +DESCRIPTION="Ensure nis server services are not in use." +PACKAGE='ypserv' +SERVICE="ypserv.service" + +NIS_PACKAGE_INSTALLED=1 +NIS_PACKAGE_IS_DEPENDENCY=1 +NIS_SERVICE_ENABLED=1 +NIS_SERVICE_ACTIVE=1 + +# 2 scenarios here: +# - ypserv is a dependency for another package -> disable the service +# - ypserv is not a dependency for another package -> remove the package + +# This function will be called if the script status is on enabled / audit mode +audit() { + is_pkg_installed "$PACKAGE" + if [ "$FNRET" -eq 0 ]; then + NIS_PACKAGE_INSTALLED=0 # 0 means package is installed + fi + + # If package not installed, we're compliant + if [ "$NIS_PACKAGE_INSTALLED" -ne 0 ]; then + ok "$PACKAGE is not installed" + return + fi + + # Package is installed, check if it's a dependency for other packages + is_pkg_a_dependency "$PACKAGE" + if [ "$FNRET" -eq 0 ]; then + NIS_PACKAGE_IS_DEPENDENCY=0 # 0 means it IS a dependency + fi + + # Check if service is enabled + is_service_enabled "$SERVICE" + if [ "$FNRET" = 0 ]; then + NIS_SERVICE_ENABLED=0 + fi + + # Check if service is active + is_service_active "$SERVICE" + if [ "$FNRET" = 0 ]; then + NIS_SERVICE_ACTIVE=0 + fi + + if [ "$NIS_PACKAGE_IS_DEPENDENCY" -eq 1 ]; then + # Package is NOT a dependency, should be removed + crit "$PACKAGE is installed and should be removed" + elif [ "$NIS_PACKAGE_IS_DEPENDENCY" -eq 0 ]; then + # Package IS a dependency, service should be stopped and masked + if [ "$NIS_SERVICE_ENABLED" -eq 0 ]; then + crit "$SERVICE is enabled" + fi + + if [ "$NIS_SERVICE_ACTIVE" -eq 0 ]; then + crit "$SERVICE is active" + fi + + if [ "$NIS_SERVICE_ENABLED" -ne 0 ] && [ "$NIS_SERVICE_ACTIVE" -ne 0 ]; then + ok "$SERVICE is stopped and disabled" + fi + fi +} + +# This function will be called if the script status is on enabled mode +apply() { + if [ "$NIS_PACKAGE_INSTALLED" -ne 0 ]; then + info "$PACKAGE is not installed, nothing to do" + return + fi + + if [ "$NIS_PACKAGE_IS_DEPENDENCY" -eq 1 ]; then + info "$PACKAGE is installed and not a dependency, removing it" + DEBIAN_FRONTEND=noninteractive apt-get remove -y "$PACKAGE" + apt-get autoremove -y || true + elif [ "$NIS_PACKAGE_IS_DEPENDENCY" -eq 0 ]; then + # Package is a dependency, stop and mask the service + if [ "$NIS_SERVICE_ENABLED" -eq 0 ] || [ "$NIS_SERVICE_ACTIVE" -eq 0 ]; then + is_systemctl_running + if [ "$FNRET" -ne 0 ]; then + warn "systemd not running, cannot stop/mask $SERVICE automatically" + return + fi + info "Stopping and masking $SERVICE" + systemctl stop "$SERVICE" || true + systemctl mask "$SERVICE" + fi + fi +} + +# This function will check config parameters required +check_config() { + : +} + +# Source Root Dir Parameter +if [ -r /etc/default/cis-hardening ]; then + # shellcheck source=../../debian/default + . /etc/default/cis-hardening +fi +if [ -z "$CIS_LIB_DIR" ]; then + echo "There is no /etc/default/cis-hardening file nor cis-hardening directory in current environment." + echo "Cannot source CIS_LIB_DIR variable, aborting." + exit 128 +fi + +# Main function, will call the proper functions given the configuration (audit, enabled, disabled) +if [ -r "${CIS_LIB_DIR}"/main.sh ]; then + # shellcheck source=../../lib/main.sh + . "${CIS_LIB_DIR}"/main.sh +else + echo "Cannot find main.sh, have you correctly defined your root directory? Current value is $CIS_LIB_DIR in /etc/default/cis-hardening" + exit 128 +fi diff --git a/bin/hardening/pam_unix_strong_hash_algorithm.sh b/bin/hardening/pam_unix_strong_hash_algorithm.sh new file mode 100755 index 00000000..c38ff124 --- /dev/null +++ b/bin/hardening/pam_unix_strong_hash_algorithm.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +# run-shellcheck +# +# CIS Debian Hardening +# + +# +# Ensure pam_unix includes a strong password hashing algorithm (Automated) +# + +set -e # One error, it's over +set -u # One variable unset, it's over + +# shellcheck disable=2034 +HARDENING_LEVEL=1 +# shellcheck disable=2034 +DESCRIPTION="Ensure pam_unix includes a strong password hashing algorithm." + +# Global state +PUH_HASH_CONFIGURED=1 # 1 = not configured, 0 = configured +PUH_CONFIG_FILE="/etc/pam.d/common-password" + +# This function will be called if the script status is on enabled / audit mode +audit() { + if [ ! -f "$PUH_CONFIG_FILE" ]; then + info "$PUH_CONFIG_FILE does not exist - PAM not configured or using alternative setup" + return + fi + + # Search for password line with pam_unix.so containing sha512 or yescrypt + # Pattern: "password" followed by spaces, "pam_unix.so", and either "sha512" or "yescrypt" + if grep -P '^[[:space:]]*password\h+([^#\r\n]+)\h+pam_unix\.so(\h+[^#\r\n]+)?\h+(sha512|yescrypt)\b' "$PUH_CONFIG_FILE" >/dev/null 2>&1; then + ok "pam_unix.so configured with sha512 or yescrypt in $PUH_CONFIG_FILE" + PUH_HASH_CONFIGURED=0 + else + crit "pam_unix.so not configured with sha512 or yescrypt in $PUH_CONFIG_FILE" + fi +} + +# This function will be called if the script status is on enabled mode +apply() { + if [ "$PUH_HASH_CONFIGURED" -eq 0 ]; then + info "pam_unix already configured with strong hashing algorithm" + return + fi + + if [ ! -f "$PUH_CONFIG_FILE" ]; then + warn "$PUH_CONFIG_FILE does not exist" + return + fi + + # Find and modify the PAM source configuration + local unix_profile="/usr/share/pam-configs/unix" + + if [ ! -f "$unix_profile" ]; then + warn "Could not find $unix_profile for modification" + return + fi + + # Check if sha512 or yescrypt is already present in the profile + if grep -qE 'sha512|yescrypt' "$unix_profile"; then + info "$unix_profile already contains sha512 or yescrypt" + else + info "Adding sha512 to pam_unix.so in Password section of $unix_profile" + + # Use sed to add sha512 to pam_unix.so line in Password-Type section + # This handles multiline password section by finding Password-Type line + # and modifying pam_unix.so until the next -Type: line + sed -i '/^Password-Type:/,/^[A-Z].*-Type:/ { + /pam_unix\.so/ { + /sha512\|yescrypt/ ! s/$/\n sha512/ + } + }' "$unix_profile" + fi + + # Apply profile changes first when pam-auth-update is available. + if command -v pam-auth-update >/dev/null 2>&1; then + info "Running pam-auth-update to apply PAM configuration changes" + DEBIAN_FRONTEND='noninteractive' pam-auth-update --force --enable unix >/dev/null 2>&1 || true + fi + + # Enforce strong hash in the effective file audited by this script. + if grep -E 'password\s+.*pam_unix\.so' "$PUH_CONFIG_FILE" >/dev/null 2>&1; then + info "Enforcing yescrypt on pam_unix.so line(s) in $PUH_CONFIG_FILE" + sed -Ei '/^[[:space:]]*password[[:space:]]+.*pam_unix\.so/ { + s/([[:space:]])(md5|bigcrypt|sha256|sha512|blowfish|gost_yescrypt|yescrypt)([[:space:]]|$)/\1/g + s/[[:space:]]+$/ / + /(^|[[:space:]])yescrypt([[:space:]]|$)/! s/$/ yescrypt/ + }' "$PUH_CONFIG_FILE" + else + info "No pam_unix.so password line found in $PUH_CONFIG_FILE, adding one" + add_line_file_before_pattern "$PUH_CONFIG_FILE" "password [success=1 default=ignore] pam_unix.so yescrypt" "# pam-auth-update(8) for details." + fi +} + +# This function will check config parameters required +check_config() { + : +} + +# Source Root Dir Parameter +if [ -r /etc/default/cis-hardening ]; then + # shellcheck source=../../debian/default + . /etc/default/cis-hardening +fi +if [ -z "$CIS_LIB_DIR" ]; then + echo "There is no /etc/default/cis-hardening file nor cis-hardening directory in current environment." + echo "Cannot source CIS_LIB_DIR variable, aborting." + exit 128 +fi + +# Main function, will call the proper functions given the configuration (audit, enabled, disabled) +if [ -r "${CIS_LIB_DIR}"/main.sh ]; then + # shellcheck source=../../lib/main.sh + . "${CIS_LIB_DIR}"/main.sh +else + echo "Cannot find main.sh, have you correctly defined your root directory? Current value is $CIS_LIB_DIR in /etc/default/cis-hardening" + exit 128 +fi diff --git a/bin/hardening/password_last_change_past.sh b/bin/hardening/password_last_change_past.sh index 084bda72..1282fc8a 100755 --- a/bin/hardening/password_last_change_past.sh +++ b/bin/hardening/password_last_change_past.sh @@ -13,18 +13,66 @@ set -e # One error, it's over set -u # One variable unset, it's over # shellcheck disable=2034 -HARDENING_LEVEL=3 +HARDENING_LEVEL=2 # shellcheck disable=2034 DESCRIPTION="Check that user last password change date is in the past." +PASSWD_LAST_CHANGE_PAST_ERROR_COUNT=0 +PASSWD_LAST_CHANGE_PAST_FUTURE_USERS="" + # This function will be called if the script status is on enabled / audit mode audit() { - : + local l_user + local l_lastchg + local l_now_days + + PASSWD_LAST_CHANGE_PAST_ERROR_COUNT=0 + PASSWD_LAST_CHANGE_PAST_FUTURE_USERS="" + + if ! $SUDO_CMD cat /etc/shadow >/dev/null 2>&1; then + crit "Cannot read /etc/shadow to verify users password change dates" + return + fi + + l_now_days=$(($(date +%s) / 86400)) + + while IFS= read -r l_user; do + l_lastchg=$($SUDO_CMD grep -E "^${l_user}:" /etc/shadow 2>/dev/null | cut -d: -f3 | head -n 1 || true) + + # Empty lastchg means no usable date. + if [ -z "$l_lastchg" ]; then + continue + fi + + if ! [[ "$l_lastchg" =~ ^[0-9]+$ ]]; then + PASSWD_LAST_CHANGE_PAST_ERROR_COUNT=$((PASSWD_LAST_CHANGE_PAST_ERROR_COUNT + 1)) + crit "User: $l_user has an invalid last password change value in /etc/shadow: $l_lastchg" + continue + fi + + if [ "$l_lastchg" -gt "$l_now_days" ]; then + PASSWD_LAST_CHANGE_PAST_ERROR_COUNT=$((PASSWD_LAST_CHANGE_PAST_ERROR_COUNT + 1)) + PASSWD_LAST_CHANGE_PAST_FUTURE_USERS="$PASSWD_LAST_CHANGE_PAST_FUTURE_USERS $l_user" + crit "User: $l_user last password change is in the future (day index $l_lastchg > $l_now_days)" + fi + done < <($SUDO_CMD cat /etc/shadow | awk -F: '$2~/^\$.+$/ {print $1}') + + if [ "$PASSWD_LAST_CHANGE_PAST_ERROR_COUNT" -eq 0 ]; then + ok "All users last password change dates are in the past" + fi } # This function will be called if the script status is on enabled mode apply() { - : + if [ "$PASSWD_LAST_CHANGE_PAST_ERROR_COUNT" -eq 0 ]; then + ok "All users last password change dates are in the past" + return + fi + + info "Manual remediation required: review affected users and set a valid last password change date" + if [ -n "$PASSWD_LAST_CHANGE_PAST_FUTURE_USERS" ]; then + warn "Users with future last password change date:$PASSWD_LAST_CHANGE_PAST_FUTURE_USERS" + fi } # This function will check config parameters required diff --git a/tests/hardening/find_unowned_ungrouped_files.sh b/tests/hardening/find_unowned_ungrouped_files.sh new file mode 100644 index 00000000..d94852c5 --- /dev/null +++ b/tests/hardening/find_unowned_ungrouped_files.sh @@ -0,0 +1,65 @@ +# shellcheck shell=bash +# run-shellcheck +test_audit() { + describe Running void to generate the conf file that will later be edited + # shellcheck disable=2154 + "${CIS_CHECKS_DIR}/${script}.sh" || true + + sed -i '/^IGNORED_PATH/d' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + # shellcheck disable=2016 + echo 'IGNORED_PATH="^/proc|^/home/secaudit/6.1.12/.*"' >>"${CIS_CONF_DIR}/conf.d/${script}.cfg" + + mkdir -p /home/secaudit/6.1.12/ + touch /home/secaudit/6.1.12/test + chown 1200:1200 /home/secaudit/6.1.12/test + + describe Running on blank host + register_test retvalshouldbe 0 + register_test contain "No unowned or ungrouped files found" + # shellcheck disable=2154 + run blank "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe Tests purposely failing with both unowned and ungrouped files + local unowned_target="/home/secaudit/unowned_file" + local ungrouped_target="/home/secaudit/ungrouped_file" + touch "$unowned_target" "$ungrouped_target" + chown 1200 "$unowned_target" + chown 1200:1200 "$ungrouped_target" + + register_test retvalshouldbe 1 + register_test contain "Some files are unowned and/or ungrouped are present" + run noncompliant "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe Validate IGNORE_PATH excludes targeted files + rm -f "$unowned_target" "$ungrouped_target" + local ignored_target="/home/secaudit/6.1.12/ignored_unowned" + touch "$ignored_target" + chown 1200 "$ignored_target" + + register_test retvalshouldbe 0 + register_test contain "No unowned or ungrouped files found" + run ignored_path_applied "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe Tests failing with find ignore flag + sed -i '/^FIND_IGNORE_NOSUCHFILE_ERR/d' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + echo 'FIND_IGNORE_NOSUCHFILE_ERR=true' >>"${CIS_CONF_DIR}/conf.d/${script}.cfg" + + local unowned_target_2="/home/secaudit/unowned_file_2" + touch "$unowned_target_2" + chown 1200 "$unowned_target_2" + + register_test retvalshouldbe 1 + register_test contain "Some files are unowned and/or ungrouped are present" + run noncompliant_ignore_err "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe correcting situation + sed -i '/^status/s/audit/enabled/' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + "${CIS_CHECKS_DIR}/${script}.sh" --apply || true + + describe Checking resolved state + register_test retvalshouldbe 0 + register_test contain "No unowned or ungrouped files found" + run resolved "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + rm -rf /home/secaudit/6.1.12/ /home/secaudit/unowned_file /home/secaudit/ungrouped_file /home/secaudit/unowned_file_2 +} diff --git a/tests/hardening/find_world_writable_files_and_folders.sh b/tests/hardening/find_world_writable_files_and_folders.sh new file mode 100644 index 00000000..a1277f1f --- /dev/null +++ b/tests/hardening/find_world_writable_files_and_folders.sh @@ -0,0 +1,46 @@ +# shellcheck shell=bash +# run-shellcheck +test_audit() { + describe Running void to generate the conf file that will later be edited + # shellcheck disable=2154 + "${CIS_CHECKS_DIR}/${script}.sh" || true + # shellcheck disable=2016 + echo 'EXCLUDED="$EXCLUDED ^/home/secaudit/thispathisignored.*|^/dev/.*"' >>"${CIS_CONF_DIR}/conf.d/${script}.cfg" + touch /home/secaudit/thispathisignored + chmod 777 /home/secaudit/thispathisignored + mkdir -p /home/secaudit/thispathisignored_dir + chmod 777 /home/secaudit/thispathisignored_dir + + describe Running on blank host + register_test retvalshouldbe 0 + register_test contain "No world writable files or directories requiring remediation found" + # shellcheck disable=2154 + run blank "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe Tests purposely failing with world writable file and directory + local targetfile="/home/secaudit/worldwritable_file" + local targetdir="/home/secaudit/worldwritable_dir" + touch "$targetfile" + mkdir -p "$targetdir" + chmod 777 "$targetfile" "$targetdir" + register_test retvalshouldbe 1 + register_test contain "Some world writable files or directories are present" + run noncompliant "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe Tests failing with find ignore flag + echo 'FIND_IGNORE_NOSUCHFILE_ERR=true' >>"${CIS_CONF_DIR}/conf.d/${script}.cfg" + register_test retvalshouldbe 1 + register_test contain "Some world writable files or directories are present" + run noncompliant_ignore_err "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe correcting situation + sed -i 's/audit/enabled/' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + "${CIS_CHECKS_DIR}/${script}.sh" --apply || true + + describe Checking resolved state + register_test retvalshouldbe 0 + register_test contain "No world writable files or directories requiring remediation found" + run resolved "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + rm -rf /home/secaudit/thispathisignored /home/secaudit/thispathisignored_dir /home/secaudit/worldwritable_file /home/secaudit/worldwritable_dir +} diff --git a/tests/hardening/graphical_warning_banners.sh b/tests/hardening/graphical_warning_banners.sh new file mode 100644 index 00000000..dc7c6df7 --- /dev/null +++ b/tests/hardening/graphical_warning_banners.sh @@ -0,0 +1,96 @@ +# shellcheck shell=bash +# run-shellcheck +test_audit() { + gdm_pkg="" + gdm_installed_before=0 + profile_backup="" + dbdir_backup="" + dbfile_backup="" + test_profile="gdm" + test_profile_file="/etc/dconf/profile/${test_profile}" + test_db_dir="/etc/dconf/db/${test_profile}.d" + test_db_file="/etc/dconf/db/${test_profile}" + + is_pkg_installed_for_test() { + dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null | grep -Eqx 'installed|triggers-awaited|triggers-pending' + } + + if is_pkg_installed_for_test gdm3; then + gdm_pkg="gdm3" + gdm_installed_before=1 + elif is_pkg_installed_for_test gdm; then + gdm_pkg="gdm" + gdm_installed_before=1 + else + for candidate_pkg in gdm3 gdm; do + DEBIAN_FRONTEND=noninteractive apt-get install -y "$candidate_pkg" >/dev/null 2>&1 || true + if is_pkg_installed_for_test "$candidate_pkg"; then + gdm_pkg="$candidate_pkg" + break + fi + done + + if [ -z "$gdm_pkg" ]; then + skip "Cannot install gdm/gdm3, skipping tests" + return + fi + fi + + if [ -f "$test_profile_file" ]; then + profile_backup=$(mktemp) + cp "$test_profile_file" "$profile_backup" + fi + + if [ -d "$test_db_dir" ]; then + dbdir_backup=$(mktemp -d) + cp -a "$test_db_dir" "$dbdir_backup/dbdir" + fi + + if [ -f "$test_db_file" ]; then + dbfile_backup=$(mktemp) + cp "$test_db_file" "$dbfile_backup" + fi + + rm -f "$test_profile_file" + rm -rf "$test_db_dir" + rm -f "$test_db_file" + + describe non compliant gdm login banner state + register_test retvalshouldbe 1 + # shellcheck disable=2154 + run noncompliant "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe correcting situation + # shellcheck disable=2154 + sed -i 's/audit/enabled/' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + # shellcheck disable=2154 + "${CIS_CHECKS_DIR}/${script}.sh" --apply || true + + describe compliant gdm login banner state + register_test retvalshouldbe 0 + # shellcheck disable=2154 + run resolved "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + rm -f "$test_profile_file" + if [ -n "$profile_backup" ]; then + cp "$profile_backup" "$test_profile_file" + rm -f "$profile_backup" + fi + + rm -rf "$test_db_dir" + if [ -n "$dbdir_backup" ] && [ -d "$dbdir_backup/dbdir" ]; then + cp -a "$dbdir_backup/dbdir" "$test_db_dir" + rm -rf "$dbdir_backup" + fi + + rm -f "$test_db_file" + if [ -n "$dbfile_backup" ]; then + cp "$dbfile_backup" "$test_db_file" + rm -f "$dbfile_backup" + fi + + if [ "$gdm_installed_before" -eq 0 ]; then + apt-get purge -y "$gdm_pkg" >/dev/null 2>&1 || true + apt-get autoremove -y >/dev/null 2>&1 || true + fi +} diff --git a/tests/hardening/local_login_banner.sh b/tests/hardening/local_login_banner.sh new file mode 100644 index 00000000..7b47cff7 --- /dev/null +++ b/tests/hardening/local_login_banner.sh @@ -0,0 +1,38 @@ +# shellcheck shell=bash +# run-shellcheck +test_audit() { + describe Create non-compliant state + ISSUE_BAK="/tmp/issue.bak.$$" + if [ -f /etc/issue ]; then + cp /etc/issue "$ISSUE_BAK" + fi + + # Create an issue file with OS information patterns + # Include mingetty escape sequences (\v, \r, \m, \s) + cat >/etc/issue <<'EOF' +Welcome to \n (\s \m) +Kernel: \r version \v +This is a debian system +EOF + + register_test retvalshouldbe 1 + # shellcheck disable=2154 + run noncompliant "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe Correcting situation + # shellcheck disable=2154 + sed -i 's/audit/enabled/' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + "${CIS_CHECKS_DIR}/${script}.sh" --apply || true + + describe Re-audit compliant state + register_test retvalshouldbe 0 + run resolved "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe Restore /etc/issue + if [ -f "$ISSUE_BAK" ]; then + mv "$ISSUE_BAK" /etc/issue + else + rm -f /etc/issue + fi + sed -i 's/enabled/audit/' "${CIS_CONF_DIR}/conf.d/${script}.cfg" +} diff --git a/tests/hardening/nis_server_disabled.sh b/tests/hardening/nis_server_disabled.sh new file mode 100644 index 00000000..3c4d9d53 --- /dev/null +++ b/tests/hardening/nis_server_disabled.sh @@ -0,0 +1,37 @@ +# shellcheck shell=bash +# run-shellcheck +test_audit() { + + describe "Prepare on purpose failed test - install ypserv package" + is_running_in_container() { + grep -qE "(docker|lxc|kubepods)" /proc/self/cgroup + } + + if is_running_in_container; then + skip "Skipping test in container environment - package installation and service management may not be reliable" + register_test retvalshouldbe 0 + return + fi + + DEBIAN_FRONTEND=noninteractive apt-get install -y ypserv || true + + describe "Running on purpose failed test - ypserv installed" + register_test retvalshouldbe 1 + # shellcheck disable=2154 + run failed "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe "Correcting situation - applying remediation" + sed -i 's/audit/enabled/' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + "${CIS_CHECKS_DIR}/${script}.sh" --apply || true + + describe "Checking resolved state - ypserv should be removed or service stopped/masked" + register_test retvalshouldbe 0 + run resolved "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe "Clean installation - remove ypserv" + if dpkg -s ypserv >/dev/null 2>&1; then + apt-get remove -y ypserv || true + fi + apt-get autoremove -y || true + +} diff --git a/tests/hardening/pam_unix_strong_hash_algorithm.sh b/tests/hardening/pam_unix_strong_hash_algorithm.sh new file mode 100644 index 00000000..db87502d --- /dev/null +++ b/tests/hardening/pam_unix_strong_hash_algorithm.sh @@ -0,0 +1,37 @@ +# shellcheck shell=bash +# run-shellcheck +test_audit() { + + describe "Prepare on purpose failed test - remove strong hash from common-password" + + # Backup original config + if [ -f /etc/pam.d/common-password ]; then + cp /etc/pam.d/common-password /etc/pam.d/common-password.bak + fi + + # Remove sha512/yescrypt from common-password to create non-compliant state if present + if [ -f /etc/pam.d/common-password ]; then + sed -Ei '/^[[:space:]]*password[[:space:]]+.*pam_unix\.so/ s/([[:space:]])(md5|bigcrypt|sha256|sha512|blowfish|gost_yescrypt|yescrypt)([[:space:]]|$)/\1/g' /etc/pam.d/common-password || true + fi + + describe "Running on purpose failed test - no strong hash configured" + register_test retvalshouldbe 1 + # shellcheck disable=2154 + run failed "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe "Correcting situation - enabling remediation" + sed -i 's/audit/enabled/' "${CIS_CONF_DIR}/conf.d/${script}.cfg" + "${CIS_CHECKS_DIR}/${script}.sh" --apply || true + + describe "Checking resolved state - strong hash should be configured" + register_test retvalshouldbe 0 + run resolved "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + describe "Restore original common-password" + if [ -f /etc/pam.d/common-password.bak ]; then + mv /etc/pam.d/common-password.bak /etc/pam.d/common-password + # Re-run pam-auth-update to restore original state + pam-auth-update --enable unix >/dev/null 2>&1 || true + fi + +} diff --git a/tests/hardening/password_last_change_past.sh b/tests/hardening/password_last_change_past.sh index 5bb5d862..8dab3359 100644 --- a/tests/hardening/password_last_change_past.sh +++ b/tests/hardening/password_last_change_past.sh @@ -1,11 +1,53 @@ # shellcheck shell=bash # run-shellcheck test_audit() { - describe Running on blank host + shadow_backup=$(mktemp) + shadow_tmp=$(mktemp) + today_days=$(($(date +%s) / 86400)) + future_days=$((today_days + 30)) + test_user="cis_pwd_future_test" + had_test_user=0 + + cp /etc/shadow "$shadow_backup" + + if getent passwd "$test_user" >/dev/null 2>&1; then + had_test_user=1 + else + useradd -M -s /usr/sbin/nologin "$test_user" >/dev/null 2>&1 || { + rm -f "$shadow_backup" "$shadow_tmp" + skip "SKIPPED: unable to create test user" + return + } + echo "$test_user:Passw0rd!" | chpasswd >/dev/null 2>&1 || true + fi + + awk -F: -v OFS=: -v u="$test_user" -v d="$future_days" ' + $1==u { $3=d; print; next } + { print } + ' /etc/shadow >"$shadow_tmp" + cat "$shadow_tmp" >/etc/shadow + + describe user with future last password change date + register_test retvalshouldbe 1 + # shellcheck disable=2154 + run noncompliant "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + awk -F: -v OFS=: -v u="$test_user" -v d="$today_days" ' + $1==u { $3=d; print; next } + { print } + ' /etc/shadow >"$shadow_tmp" + cat "$shadow_tmp" >/etc/shadow + + describe user last password change moved to past register_test retvalshouldbe 0 - dismiss_count_for_test # shellcheck disable=2154 - run blank "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + run resolved "${CIS_CHECKS_DIR}/${script}.sh" --audit-all + + cp "$shadow_backup" /etc/shadow + + if [ "$had_test_user" -eq 0 ]; then + userdel -f "$test_user" >/dev/null 2>&1 || true + fi - # TODO fill comprehensive tests + rm -f "$shadow_backup" "$shadow_tmp" }