Files
2026-05-29 21:37:38 +00:00

502 lines
20 KiB
Bash

#!/usr/bin/env bash
###############################################################################
# Targeted SCA Remediation — Based on sca-verify.sh diagnostic output
#
# Fixes all FAIL items from the diagnostic. Does NOT touch items that
# already PASS. Safe to re-run (idempotent).
#
# Usage: chmod +x sca-fix.sh && sudo ./sca-fix.sh
###############################################################################
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
if [[ $EUID -ne 0 ]]; then echo "Run as root." >&2; exit 1; fi
banner() { printf '\n\e[1;36m>>> %s\e[0m\n' "$1"; }
ADMIN_USER="${ADMIN_USER:-chris}"
###############################################################################
banner "FIX 1: Filesystem module blacklist (35509) — config file MISSING"
###############################################################################
cat > /etc/modprobe.d/cis-filesystems.conf <<'EOF'
install afs /bin/false
install ceph /bin/false
install cifs /bin/false
install exfat /bin/false
install fat /bin/false
install fscache /bin/false
install fuse /bin/false
install gfs2 /bin/false
install nfs_common /bin/false
install nfsd /bin/false
install smbfs_common /bin/false
install cramfs /bin/false
install freevxfs /bin/false
install jffs2 /bin/false
install hfs /bin/false
install hfsplus /bin/false
install squashfs /bin/false
install udf /bin/false
blacklist afs
blacklist ceph
blacklist cifs
blacklist exfat
blacklist fat
blacklist fscache
blacklist fuse
blacklist gfs2
blacklist nfs_common
blacklist nfsd
blacklist smbfs_common
blacklist cramfs
blacklist freevxfs
blacklist jffs2
blacklist hfs
blacklist hfsplus
blacklist squashfs
blacklist udf
EOF
echo " ✓ /etc/modprobe.d/cis-filesystems.conf created"
###############################################################################
banner "FIX 2: Network protocol module blacklist (35604-35607) — config file MISSING"
###############################################################################
cat > /etc/modprobe.d/cis-network-protocols.conf <<'EOF'
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
blacklist dccp
blacklist sctp
blacklist rds
blacklist tipc
EOF
echo " ✓ /etc/modprobe.d/cis-network-protocols.conf created"
###############################################################################
banner "FIX 3: USB storage (uses /bin/true, should be /bin/false)"
###############################################################################
cat > /etc/modprobe.d/cis-usb-storage.conf <<'EOF'
install usb-storage /bin/false
blacklist usb-storage
EOF
echo " ✓ /etc/modprobe.d/cis-usb-storage.conf created"
###############################################################################
banner "FIX 4: Rebuild initramfs with new module blacklists"
###############################################################################
update-initramfs -u
echo " ✓ initramfs rebuilt"
###############################################################################
banner "FIX 5: Sysctl — ip_forward and log_martians (35608, 35616)"
###############################################################################
# Docker overrides ip_forward at runtime. To keep it disabled while
# allowing Docker networking, we need to tell Docker not to touch it
# and handle forwarding via iptables only.
# However, if Docker is actively in use, disabling ip_forward breaks
# container networking. The pragmatic fix:
if command -v docker &>/dev/null; then
echo " Docker is installed. ip_forward=1 is required for container networking."
echo " This is a known conflict — document as compensating control."
echo " Creating /etc/sysctl.d/99-docker-override.conf for non-forwarding params only."
# Don't override ip_forward if Docker is present, but DO fix log_martians
cat > /etc/sysctl.d/99-docker-override.conf <<'EOF'
# Docker requires ip_forward=1 — accepted as compensating control
# These params are safe to enforce alongside Docker:
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
EOF
else
# No Docker — enforce everything
sysctl -w net.ipv4.ip_forward=0
fi
# Apply log_martians immediately regardless
sysctl -w net.ipv4.conf.all.log_martians=1
sysctl -w net.ipv4.conf.default.log_martians=1
sysctl --system > /dev/null 2>&1
echo " ✓ log_martians enabled"
echo " ✓ sysctl reloaded"
###############################################################################
banner "FIX 6: Purge nftables package (35626, 35634)"
###############################################################################
if dpkg -s nftables &>/dev/null; then
apt-get purge -y nftables
echo " ✓ nftables purged"
else
echo " ✓ nftables already absent"
fi
###############################################################################
banner "FIX 7: SSH config — merge drop-in into main config (35644-35661)"
###############################################################################
# Wazuh scans /etc/ssh/sshd_config directly and doesn't parse drop-in files.
# All settings PASS via sshd -T but FAIL in Wazuh because they're only in the drop-in.
# Fix: append drop-in contents to main config and remove the drop-in.
DROPIN="/etc/ssh/sshd_config.d/nist-hardening.conf"
MAIN="/etc/ssh/sshd_config"
if [[ -f "$DROPIN" ]]; then
# Remove any existing lines in main config that we're about to add
# (avoid duplicates)
while IFS= read -r line; do
# Skip comments and empty lines
[[ "$line" =~ ^#|^$ ]] && continue
param=$(echo "$line" | awk '{print $1}')
# Remove existing uncommented instances of this param from main config
sed -i "/^\s*${param}\b/d" "$MAIN" 2>/dev/null || true
done < "$DROPIN"
# Append the hardening config
echo "" >> "$MAIN"
echo "# ===== NIST 800-53 SSH Hardening (merged from drop-in) =====" >> "$MAIN"
cat "$DROPIN" >> "$MAIN"
# Remove the drop-in
rm -f "$DROPIN"
# Test and restart
if sshd -t; then
systemctl restart ssh
echo " ✓ SSH config merged into $MAIN, drop-in removed, SSH restarted"
else
echo " ERROR: sshd config test failed. Check $MAIN"
exit 1
fi
else
echo " ✓ No drop-in to merge (already in main config)"
fi
###############################################################################
banner "FIX 8: Password aging for root and nobody (35694, 35695, 35698)"
###############################################################################
# root — set aging but keep locked (no password login)
chage -M 60 -m 1 -W 14 -I 30 root
echo " ✓ root: max=60 min=1 warn=14 inactive=30"
# nobody — system account, set aging for compliance
chage -M 60 -m 1 -W 14 -I 30 nobody
echo " ✓ nobody: max=60 min=1 warn=14 inactive=30"
###############################################################################
banner "FIX 9: Journald MaxRetentionSec (35708)"
###############################################################################
JOURNALD_DROPIN="/etc/systemd/journald.conf.d/nist.conf"
if [[ -f "$JOURNALD_DROPIN" ]]; then
if ! grep -q 'MaxRetentionSec' "$JOURNALD_DROPIN"; then
echo "MaxRetentionSec=1month" >> "$JOURNALD_DROPIN"
echo " ✓ MaxRetentionSec added to drop-in"
fi
fi
# Also add to main config since Wazuh may only read the main file
JOURNALD_MAIN="/etc/systemd/journald.conf"
if ! grep -q '^\s*SystemMaxUse=' "$JOURNALD_MAIN"; then
# If the main config doesn't have our settings, add them
sed -i '/^\[Journal\]/a SystemMaxUse=1G\nSystemKeepFree=1G\nSystemMaxFileSize=100M\nMaxRetentionSec=1month' "$JOURNALD_MAIN"
echo " ✓ Journald settings added to main config for Wazuh"
else
if ! grep -q '^\s*MaxRetentionSec=' "$JOURNALD_MAIN"; then
sed -i '/^\[Journal\]/a MaxRetentionSec=1month' "$JOURNALD_MAIN"
echo " ✓ MaxRetentionSec added to main journald.conf"
fi
fi
systemctl restart systemd-journald
echo " ✓ journald restarted"
###############################################################################
banner "FIX 10: Log file permissions (35722)"
###############################################################################
# Fix dpkg.log and sysstat files
find /var/log -type f -exec chmod g-wx,o-rwx {} +
find /var/log -type d -exec chmod g-wx,o-rwx {} +
echo " ✓ /var/log permissions tightened"
# Prevent sysstat from creating world-readable files in the future
if [[ -f /etc/sysstat/sysstat ]]; then
# Set umask for sysstat
if ! grep -q 'umask' /etc/sysstat/sysstat; then
echo 'UMASK="0027"' >> /etc/sysstat/sysstat
fi
fi
# Also set default umask in rsyslog
if [[ -f /etc/rsyslog.d/99-nist-perms.conf ]]; then
if ! grep -q 'DirCreateMode' /etc/rsyslog.d/99-nist-perms.conf; then
echo '$DirCreateMode 0750' >> /etc/rsyslog.d/99-nist-perms.conf
echo '$Umask 0027' >> /etc/rsyslog.d/99-nist-perms.conf
fi
fi
echo " ✓ Future log file creation modes tightened"
###############################################################################
banner "FIX 11: Audit rules file (35731-35748) — rules file MISSING"
###############################################################################
# Rules are loaded (50 rules) but the file Wazuh scans doesn't exist.
# The rules seem to be in /etc/audit/rules.d/cis-nist.rules instead of
# nist-800-53.rules. Wazuh scans the rules.d directory, so we need to
# ensure the rules file is there with the correct content.
cat > /etc/audit/rules.d/nist-800-53.rules <<'AUDITRULES'
## ---- Remove any existing rules ----
-D
## ---- Buffer size ----
-b 8192
## ---- Failure mode ----
-f 1
## ---- Time changes (35734) ----
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
-a always,exit -F arch=b32 -S adjtimex,settimeofday,clock_settime -k time-change
-w /etc/localtime -p wa -k time-change
## ---- User/group changes (35737) ----
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/security/opasswd -p wa -k identity
## ---- Network configuration (35735) ----
-a always,exit -F arch=b64 -S sethostname,setdomainname -k network-config
-a always,exit -F arch=b32 -S sethostname,setdomainname -k network-config
-w /etc/hosts -p wa -k network-config
-w /etc/hostname -p wa -k network-config
-w /etc/netplan/ -p wa -k network-config
-w /etc/network/ -p wa -k network-config
-w /etc/networks -p wa -k network-config
## ---- Login/logout events (35741) ----
-w /var/log/lastlog -p wa -k logins
-w /var/log/faillog -p wa -k logins
-w /var/log/wtmp -p wa -k logins
-w /var/log/btmp -p wa -k logins
## ---- Session initiation (35740) ----
-w /var/run/utmp -p wa -k session
## ---- Sudo scope changes (35731) ----
-w /etc/sudoers -p wa -k scope
-w /etc/sudoers.d -p wa -k scope
## ---- Sudo log (35733) ----
-w /var/log/sudo.log -p wa -k actions
## ---- Actions as another user (35732) ----
-a always,exit -F arch=b64 -S execve -C euid!=uid -F auid!=unset -k user_emulation
-a always,exit -F arch=b32 -S execve -C euid!=uid -F auid!=unset -k user_emulation
## ---- Privileged commands ----
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k privileged
-a always,exit -F arch=b32 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k privileged
## ---- Unsuccessful file access (35736) ----
-a always,exit -F arch=b64 -S open,openat,creat,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=4294967295 -k access
-a always,exit -F arch=b64 -S open,openat,creat,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=4294967295 -k access
-a always,exit -F arch=b32 -S open,openat,creat,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=4294967295 -k access
-a always,exit -F arch=b32 -S open,openat,creat,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=4294967295 -k access
## ---- DAC permission changes (35738) ----
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b64 -S chown,fchown,lchown,fchownat -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b64 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b32 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b32 -S chown,fchown,lchown,fchownat -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b32 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=4294967295 -k perm_mod
## ---- File system mounts (35739) ----
-a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts
-a always,exit -F arch=b32 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts
## ---- File deletion events (35742) ----
-a always,exit -F arch=b64 -S unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=4294967295 -k delete
-a always,exit -F arch=b32 -S unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=4294967295 -k delete
## ---- Mandatory access control changes (35743) ----
-w /etc/apparmor/ -p wa -k MAC-policy
-w /etc/apparmor.d/ -p wa -k MAC-policy
## ---- chcon (35744) ----
-a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=4294967295 -k perm_chng
## ---- setfacl (35745) ----
-a always,exit -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=4294967295 -k perm_chng
## ---- chacl (35746) ----
-a always,exit -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=4294967295 -k perm_chng
## ---- usermod (35747) ----
-a always,exit -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=4294967295 -k usermod
## ---- Kernel module loading (35748) ----
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module,create_module,query_module -k kernel_modules
-a always,exit -F arch=b32 -S init_module,finit_module,delete_module,create_module,query_module -k kernel_modules
-w /sbin/insmod -p x -k kernel_modules
-w /sbin/rmmod -p x -k kernel_modules
-w /sbin/modprobe -p x -k kernel_modules
-w /bin/kmod -p x -k kernel_modules
## ---- Make configuration immutable ----
-e 2
AUDITRULES
chmod 640 /etc/audit/rules.d/nist-800-53.rules
# Remove old rules file if it exists and is different
if [[ -f /etc/audit/rules.d/cis-nist.rules ]]; then
rm -f /etc/audit/rules.d/cis-nist.rules
echo " ✓ Removed old cis-nist.rules"
fi
# Load the new rules
augenrules --load 2>/dev/null || true
systemctl restart auditd 2>/dev/null || true
echo " ✓ nist-800-53.rules created and loaded"
echo " Rules loaded: $(auditctl -l 2>/dev/null | wc -l)"
###############################################################################
banner "FIX 12: AIDE audit tool coverage (35760)"
###############################################################################
AIDE_CUSTOM="/etc/aide/aide.conf.d/99_nist_custom"
if [[ -f "$AIDE_CUSTOM" ]]; then
# Check if audit tools are already covered
if ! grep -q 'auditctl' "$AIDE_CUSTOM"; then
cat >> "$AIDE_CUSTOM" <<'EOF'
# CIS 4.1.4.11 - Audit tool integrity
/sbin/auditctl p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/auditd p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/ausearch p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/aureport p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/autrace p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/augenrules p+i+n+u+g+s+b+acl+xattrs+sha512
EOF
echo " ✓ Audit tool integrity rules added to AIDE config"
else
echo " ✓ Audit tools already in AIDE config"
fi
else
# Create the file
cat > "$AIDE_CUSTOM" <<'EOF'
# NIST 800-53 SI-7: File integrity monitoring
/boot Full
/bin Full
/sbin Full
/lib Full
/lib64 Full
/usr/bin Full
/usr/sbin Full
/usr/lib Full
/etc Full
!/etc/mtab
!/etc/adjtime
!/var/log
!/var/spool
!/var/cache
# CIS 4.1.4.11 - Audit tool integrity
/sbin/auditctl p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/auditd p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/ausearch p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/aureport p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/autrace p+i+n+u+g+s+b+acl+xattrs+sha512
/sbin/augenrules p+i+n+u+g+s+b+acl+xattrs+sha512
EOF
echo " ✓ AIDE custom config created with audit tool coverage"
fi
echo " NOTE: Run 'sudo aideinit -y -f' to rebuild the database (takes 10-15 min)"
###############################################################################
banner "FIX 13: at.allow — add admin user (35601)"
###############################################################################
if [[ -f /etc/at.allow ]]; then
if ! grep -q "$ADMIN_USER" /etc/at.allow; then
echo "$ADMIN_USER" >> /etc/at.allow
echo "$ADMIN_USER added to /etc/at.allow"
else
echo "$ADMIN_USER already in /etc/at.allow"
fi
fi
# Same for cron.allow
if [[ -f /etc/cron.allow ]]; then
if ! grep -q "$ADMIN_USER" /etc/cron.allow; then
echo "$ADMIN_USER" >> /etc/cron.allow
echo "$ADMIN_USER added to /etc/cron.allow"
fi
fi
###############################################################################
banner "COMPLETE"
###############################################################################
cat <<'DONE'
╔══════════════════════════════════════════════════════════════════╗
║ SCA Remediation Complete ║
╠══════════════════════════════════════════════════════════════════╣
║ ║
║ Fixed: ║
║ • Filesystem module blacklist file created ║
║ • Network protocol module blacklist file created ║
║ • USB storage module fixed (/bin/false) ║
║ • initramfs rebuilt with blacklists ║
║ • log_martians enabled at runtime ║
║ • nftables package purged ║
║ • SSH config merged from drop-in into main sshd_config ║
║ • Password aging set for root and nobody ║
║ • Journald MaxRetentionSec added ║
║ • Log file permissions tightened ║
║ • Full audit ruleset created in nist-800-53.rules ║
║ • AIDE audit tool integrity rules added ║
║ • at.allow / cron.allow updated ║
║ ║
║ Known exceptions (suppress in Wazuh): ║
║ • 35540 GRUB password (manual step) ║
║ • 35589 systemd-timesyncd (chrony is active) ║
║ • 35608 ip_forward=1 (Docker requires it) ║
║ • 35710 journal-upload (using Wazuh instead) ║
║ • 35720 remote syslog (using Wazuh instead) ║
║ • 35626-35639 firewall cross-checks (using UFW only) ║
║ ║
║ Still needed: ║
║ • Rebuild AIDE database: sudo aideinit -y -f ║
║ • Reboot to apply module blacklists ║
║ • Re-run sca-verify.sh to confirm fixes ║
║ • Re-run Wazuh SCA scan ║
║ ║
╚══════════════════════════════════════════════════════════════════╝
DONE