Cybersecurity-Projects/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/04_logging.sh

386 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# ©AngelaMos | 2026
# 04_logging.sh
#
# CIS Section 4 checks: Logging and Auditing
#
# Implements controls 4.1.1-4.2.4 for the audit subsystem and syslog
# configuration. Verifies auditd is installed, enabled, and configured
# for boot-time auditing (audit=1 in GRUB_CMDLINE_LINUX) with a
# sufficient backlog limit (>=8192). Validates ten categories of audit
# rules by searching /etc/audit/rules.d/ and audit.rules: time changes
# (adjtimex, settimeofday, clock_settime), user/group modifications,
# network environment changes (sethostname, /etc/issue, /etc/hosts),
# MAC policy changes (SELinux/AppArmor paths), login/logout events,
# session initiation (utmp/wtmp/btmp), DAC permission changes (chmod,
# chown family), unauthorized access attempts (EACCES/EPERM), file
# system mounts, and file deletions (unlink/rename). Checks rsyslog
# installation, service status, FileCreateMode, and logging rules.
#
# Connects to:
# lib/registry.sh - record_result for each control
# lib/utils.sh - package_is_installed, file_exists, service_is_enabled
check_4_1_1() {
local id="4.1.1"
local status="$STATUS_PASS"
local evidence=""
if package_is_installed "auditd"; then
evidence="auditd package is installed"
elif file_exists "/usr/sbin/auditd"; then
evidence="auditd binary found at /usr/sbin/auditd"
else
status="$STATUS_FAIL"
evidence="auditd is not installed"
fi
record_result "$id" "$status" "$evidence"
}
check_4_1_2() {
local id="4.1.2"
local status="$STATUS_PASS"
local evidence=""
if service_is_enabled "auditd"; then
evidence="auditd service is enabled"
else
local symlink="${SYSROOT}/etc/systemd/system/multi-user.target.wants/auditd.service"
if [[ -L "$symlink" || -f "$symlink" ]]; then
evidence="auditd enabled via systemd symlink"
else
status="$STATUS_FAIL"
evidence="auditd service is not enabled"
fi
fi
record_result "$id" "$status" "$evidence"
}
check_4_1_3() {
local id="4.1.3"
local status="$STATUS_PASS"
local evidence=""
local grub_default="${SYSROOT}/etc/default/grub"
if [[ ! -f "$grub_default" ]]; then
status="$STATUS_SKIP"
evidence="/etc/default/grub not found"
record_result "$id" "$status" "$evidence"
return
fi
local cmdline
cmdline=$(grep -E '^\s*GRUB_CMDLINE_LINUX=' "$grub_default" | tail -1) || true
if [[ -z "$cmdline" ]]; then
status="$STATUS_FAIL"
evidence="GRUB_CMDLINE_LINUX not configured"
elif echo "$cmdline" | grep -q 'audit=1'; then
evidence="Pre-auditd auditing enabled (audit=1 in GRUB_CMDLINE_LINUX)"
else
status="$STATUS_FAIL"
evidence="audit=1 not found in GRUB_CMDLINE_LINUX"
fi
record_result "$id" "$status" "$evidence"
}
check_4_1_4() {
local id="4.1.4"
local status="$STATUS_PASS"
local evidence=""
local grub_default="${SYSROOT}/etc/default/grub"
if [[ ! -f "$grub_default" ]]; then
status="$STATUS_SKIP"
evidence="/etc/default/grub not found"
record_result "$id" "$status" "$evidence"
return
fi
local cmdline
cmdline=$(grep -E '^\s*GRUB_CMDLINE_LINUX=' "$grub_default" | tail -1) || true
if [[ -z "$cmdline" ]]; then
status="$STATUS_FAIL"
evidence="GRUB_CMDLINE_LINUX not configured"
elif echo "$cmdline" | grep -qE 'audit_backlog_limit=[0-9]+'; then
local limit
limit=$(echo "$cmdline" | grep -oE 'audit_backlog_limit=[0-9]+' | cut -d= -f2)
if (( limit >= 8192 )); then
evidence="Audit backlog limit is sufficient (${limit})"
else
status="$STATUS_FAIL"
evidence="Audit backlog limit is ${limit} (expected >= 8192)"
fi
else
status="$STATUS_FAIL"
evidence="audit_backlog_limit not found in GRUB_CMDLINE_LINUX"
fi
record_result "$id" "$status" "$evidence"
}
_check_audit_rules() {
local id="$1"
shift
local description="$1"
shift
local search_patterns=("$@")
local status="$STATUS_PASS"
local evidence=""
local rules_dir="${SYSROOT}/etc/audit/rules.d"
local audit_rules="${SYSROOT}/etc/audit/audit.rules"
local missing=()
for pattern in "${search_patterns[@]}"; do
local found="false"
if [[ -d "$rules_dir" ]]; then
for rule_file in "$rules_dir"/*.rules; do
[[ -f "$rule_file" ]] || continue
if grep -q "$pattern" "$rule_file"; then
found="true"
break
fi
done
fi
if [[ "$found" == "false" && -f "$audit_rules" ]]; then
if grep -q "$pattern" "$audit_rules"; then
found="true"
fi
fi
if [[ "$found" == "false" ]]; then
missing+=("$pattern")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
status="$STATUS_FAIL"
evidence="Missing audit rules for: ${missing[*]}"
else
evidence="${description} audit rules are configured"
fi
record_result "$id" "$status" "$evidence"
}
check_4_1_5() {
_check_audit_rules "4.1.5" "Time change" \
"adjtimex" "settimeofday" "clock_settime" "/etc/localtime"
}
check_4_1_6() {
_check_audit_rules "4.1.6" "User/group change" \
"/etc/group" "/etc/passwd" "/etc/gshadow" "/etc/shadow" "/etc/security/opasswd"
}
check_4_1_7() {
_check_audit_rules "4.1.7" "Network environment" \
"sethostname" "setdomainname" "/etc/issue" "/etc/hosts" "/etc/networks"
}
check_4_1_8() {
local id="4.1.8"
local status="$STATUS_PASS"
local evidence=""
local rules_dir="${SYSROOT}/etc/audit/rules.d"
local audit_rules="${SYSROOT}/etc/audit/audit.rules"
local found="false"
local mac_patterns=("/etc/selinux/" "/etc/apparmor/")
for pattern in "${mac_patterns[@]}"; do
if [[ -d "$rules_dir" ]]; then
for rule_file in "$rules_dir"/*.rules; do
[[ -f "$rule_file" ]] || continue
if grep -q "$pattern" "$rule_file"; then
found="true"
break 2
fi
done
fi
if [[ "$found" == "false" && -f "$audit_rules" ]]; then
if grep -q "$pattern" "$audit_rules"; then
found="true"
break
fi
fi
done
if [[ "$found" == "false" ]]; then
status="$STATUS_FAIL"
evidence="Missing audit rules for MAC policy changes"
else
evidence="MAC policy change audit rules are configured"
fi
record_result "$id" "$status" "$evidence"
}
check_4_1_9() {
_check_audit_rules "4.1.9" "Login/logout event" \
"/var/log/lastlog" "/var/run/faillock/"
}
check_4_1_10() {
_check_audit_rules "4.1.10" "Session initiation" \
"/var/run/utmp" "/var/log/wtmp" "/var/log/btmp"
}
check_4_1_11() {
_check_audit_rules "4.1.11" "DAC permission change" \
"chmod" "chown" "fchmod" "fchown" "lchown" "setxattr"
}
check_4_1_12() {
local id="4.1.12"
local status="$STATUS_PASS"
local evidence=""
local rules_dir="${SYSROOT}/etc/audit/rules.d"
local audit_rules="${SYSROOT}/etc/audit/audit.rules"
local found_eacces="false"
local found_eperm="false"
if [[ -d "$rules_dir" ]]; then
for rule_file in "$rules_dir"/*.rules; do
[[ -f "$rule_file" ]] || continue
grep -q "EACCES" "$rule_file" && found_eacces="true"
grep -q "EPERM" "$rule_file" && found_eperm="true"
done
fi
if [[ -f "$audit_rules" ]]; then
grep -q "EACCES" "$audit_rules" && found_eacces="true"
grep -q "EPERM" "$audit_rules" && found_eperm="true"
fi
if [[ "$found_eacces" == "false" || "$found_eperm" == "false" ]]; then
local missing_items=""
[[ "$found_eacces" == "false" ]] && missing_items="EACCES"
[[ "$found_eperm" == "false" ]] && missing_items="${missing_items:+${missing_items} }EPERM"
status="$STATUS_FAIL"
evidence="Missing audit rules for unauthorized access: ${missing_items}"
else
evidence="Unauthorized file access audit rules are configured"
fi
record_result "$id" "$status" "$evidence"
}
check_4_1_13() {
_check_audit_rules "4.1.13" "File system mount" \
"mount"
}
check_4_1_14() {
_check_audit_rules "4.1.14" "File deletion" \
"unlink" "rename"
}
check_4_2_1() {
local id="4.2.1"
local status="$STATUS_PASS"
local evidence=""
if package_is_installed "rsyslog"; then
evidence="rsyslog package is installed"
elif file_exists "/usr/sbin/rsyslogd"; then
evidence="rsyslogd binary found at /usr/sbin/rsyslogd"
else
status="$STATUS_FAIL"
evidence="rsyslog is not installed"
fi
record_result "$id" "$status" "$evidence"
}
check_4_2_2() {
local id="4.2.2"
local status="$STATUS_PASS"
local evidence=""
if service_is_enabled "rsyslog"; then
evidence="rsyslog service is enabled"
else
local symlink="${SYSROOT}/etc/systemd/system/multi-user.target.wants/rsyslog.service"
if [[ -L "$symlink" || -f "$symlink" ]]; then
evidence="rsyslog enabled via systemd symlink"
else
status="$STATUS_FAIL"
evidence="rsyslog service is not enabled"
fi
fi
record_result "$id" "$status" "$evidence"
}
check_4_2_3() {
local id="4.2.3"
local status="$STATUS_PASS"
local evidence=""
local rsyslog_conf="${SYSROOT}/etc/rsyslog.conf"
if [[ ! -f "$rsyslog_conf" ]]; then
status="$STATUS_SKIP"
evidence="/etc/rsyslog.conf not found"
record_result "$id" "$status" "$evidence"
return
fi
local file_mode
# shellcheck disable=SC2016 # literal $FileCreateMode and $2 are regex/awk syntax
file_mode=$(grep -E '^\s*\$FileCreateMode' "$rsyslog_conf" | tail -1 | awk '{print $2}') || true
if [[ -z "$file_mode" ]]; then
status="$STATUS_FAIL"
evidence="\$FileCreateMode not set in rsyslog.conf"
elif (( 8#$file_mode <= 8#0640 )); then
evidence="rsyslog FileCreateMode is ${file_mode}"
else
status="$STATUS_FAIL"
evidence="rsyslog FileCreateMode is ${file_mode} (expected 0640 or stricter)"
fi
record_result "$id" "$status" "$evidence"
}
check_4_2_4() {
local id="4.2.4"
local status="$STATUS_PASS"
local evidence=""
local rsyslog_conf="${SYSROOT}/etc/rsyslog.conf"
if [[ ! -f "$rsyslog_conf" ]]; then
status="$STATUS_SKIP"
evidence="/etc/rsyslog.conf not found"
record_result "$id" "$status" "$evidence"
return
fi
local rule_count
rule_count=$(grep -cE '^\s*[a-z]+\.\*\s+/|^\s*\*\.\*\s+/' "$rsyslog_conf") || true
if [[ "$rule_count" -gt 0 ]]; then
evidence="Found ${rule_count} logging rule(s) in rsyslog.conf"
else
local include_count
# shellcheck disable=SC2016 # literal $IncludeConfig is regex syntax, not shell
include_count=$(grep -cE '^\s*\$IncludeConfig|^\s*include\(' "$rsyslog_conf") || true
if [[ "$include_count" -gt 0 ]]; then
evidence="Logging configured via included config files (${include_count} include directive(s))"
else
status="$STATUS_FAIL"
evidence="No logging rules found in rsyslog.conf"
fi
fi
record_result "$id" "$status" "$evidence"
}