Sync all scripts from website downloads — 352 scripts total

Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
This commit is contained in:
2026-05-25 03:31:08 +02:00
parent dbd6bf0324
commit a1a17e81a1
332 changed files with 174509 additions and 1106 deletions
+924
View File
@@ -0,0 +1,924 @@
#!/usr/bin/env bash
#########################################################################################
#### linux-baseline-checks.sh — Post-provision baseline security validation ####
#### Zero external dependencies. Validates freshly provisioned or hardened servers. ####
#### Requires: bash 4+, curl (optional), openssl (optional), ss ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.00 ####
#### ####
#### Usage: ####
#### ./linux-baseline-checks.sh ####
#### ./linux-baseline-checks.sh --skip-updates --format tap ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
SKIP_SSH="${SKIP_SSH:-false}"
SKIP_FIREWALL="${SKIP_FIREWALL:-false}"
SKIP_UPDATES="${SKIP_UPDATES:-false}"
EXPECTED_SERVICES="${EXPECTED_SERVICES:-}"
CURL_TIMEOUT="${CURL_TIMEOUT:-10}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}" # text, tap, junit
JUNIT_FILE="${JUNIT_FILE:-baseline-results.xml}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
# ── State ─────────────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
TOTAL=0
RESULTS=()
START_TIME=""
# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }
# ── Test Result Recording ─────────────────────────────────────────────
record_pass() {
local name="$1"
local detail="${2:-}"
((PASS++)) || true
((TOTAL++)) || true
RESULTS+=("PASS|${name}|${detail}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
echo "ok ${TOTAL} - ${name}"
else
echo -e " ${GREEN}${RESET} ${name}${detail:+ — ${detail}}"
fi
}
record_fail() {
local name="$1"
local detail="${2:-}"
((FAIL++)) || true
((TOTAL++)) || true
RESULTS+=("FAIL|${name}|${detail}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
echo "not ok ${TOTAL} - ${name}"
[[ -n "$detail" ]] && echo " # ${detail}"
else
echo -e " ${RED}${RESET} ${name}${detail:+ — ${detail}}"
fi
}
record_skip() {
local name="$1"
local reason="${2:-}"
((SKIP++)) || true
((TOTAL++)) || true
RESULTS+=("SKIP|${name}|${reason}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
echo "ok ${TOTAL} - ${name} # SKIP ${reason}"
else
echo -e " ${YELLOW}${RESET} ${name}${reason:+ — ${reason}}"
fi
}
# ── Helper: read sshd config value ───────────────────────────────────
sshd_config_value() {
local key="$1"
local value=""
# Check drop-in configs first, then main config
if [[ -d /etc/ssh/sshd_config.d ]]; then
value=$(grep -rhi "^${key}" /etc/ssh/sshd_config.d/ 2>/dev/null | tail -1 | awk '{print $2}') || true
fi
if [[ -z "$value" ]]; then
value=$(grep -hi "^${key}" /etc/ssh/sshd_config 2>/dev/null | tail -1 | awk '{print $2}') || true
fi
echo "$value"
}
# ══════════════════════════════════════════════════════════════════════
# TEST SUITES
# ══════════════════════════════════════════════════════════════════════
# ── 1. SSH Hardening ─────────────────────────────────────────────────
test_ssh() {
if [[ "$SKIP_SSH" == "true" ]]; then
echo ""
echo -e "${BOLD}SSH Hardening${RESET}"
record_skip "SSH hardening" "SKIP_SSH=true"
return
fi
echo ""
echo -e "${BOLD}SSH Hardening${RESET}"
if [[ ! -f /etc/ssh/sshd_config ]]; then
record_skip "SSH hardening" "sshd_config not found"
return
fi
# PermitRootLogin
local root_login
root_login=$(sshd_config_value "PermitRootLogin")
verbose "PermitRootLogin = ${root_login:-<unset>}"
if [[ "$root_login" == "no" || "$root_login" == "prohibit-password" ]]; then
record_pass "PermitRootLogin disabled" "${root_login}"
elif [[ -z "$root_login" ]]; then
record_fail "PermitRootLogin disabled" "not explicitly set (default may allow root)"
else
record_fail "PermitRootLogin disabled" "set to ${root_login}"
fi
# PasswordAuthentication
local pass_auth
pass_auth=$(sshd_config_value "PasswordAuthentication")
verbose "PasswordAuthentication = ${pass_auth:-<unset>}"
if [[ "$pass_auth" == "no" ]]; then
record_pass "PasswordAuthentication disabled" "key-only auth enforced"
elif [[ -z "$pass_auth" ]]; then
record_fail "PasswordAuthentication disabled" "not explicitly set (default is yes)"
else
record_fail "PasswordAuthentication disabled" "set to ${pass_auth}"
fi
# Protocol 2 (older systems may have Protocol directive)
local protocol
protocol=$(sshd_config_value "Protocol")
verbose "Protocol = ${protocol:-<unset>}"
if [[ -z "$protocol" || "$protocol" == "2" ]]; then
record_pass "SSH Protocol 2" "${protocol:-default (2)}"
else
record_fail "SSH Protocol 2" "set to ${protocol}"
fi
# Non-standard port detection
local ssh_port
ssh_port=$(sshd_config_value "Port")
if [[ -z "$ssh_port" ]]; then
ssh_port="22"
fi
verbose "SSH Port = ${ssh_port}"
if [[ "$ssh_port" != "22" ]]; then
record_pass "SSH non-standard port" "port ${ssh_port}"
else
record_fail "SSH non-standard port" "still using default port 22"
fi
# PubkeyAuthentication
local pubkey_auth
pubkey_auth=$(sshd_config_value "PubkeyAuthentication")
verbose "PubkeyAuthentication = ${pubkey_auth:-<unset>}"
if [[ -z "$pubkey_auth" || "$pubkey_auth" == "yes" ]]; then
record_pass "PubkeyAuthentication enabled" "${pubkey_auth:-default (yes)}"
else
record_fail "PubkeyAuthentication enabled" "set to ${pubkey_auth}"
fi
}
# ── 2. Firewall ──────────────────────────────────────────────────────
test_firewall() {
if [[ "$SKIP_FIREWALL" == "true" ]]; then
echo ""
echo -e "${BOLD}Firewall${RESET}"
record_skip "Firewall" "SKIP_FIREWALL=true"
return
fi
echo ""
echo -e "${BOLD}Firewall${RESET}"
local fw_found=false
# ufw
if command -v ufw &>/dev/null; then
local ufw_status
ufw_status=$(ufw status 2>/dev/null | head -1) || ufw_status=""
verbose "ufw status: ${ufw_status}"
if [[ "$ufw_status" == *"active"* ]]; then
record_pass "ufw active" "firewall enabled"
fw_found=true
# Default deny
local ufw_default
ufw_default=$(ufw status verbose 2>/dev/null | grep "Default:" | head -1) || ufw_default=""
if [[ "$ufw_default" == *"deny"* || "$ufw_default" == *"reject"* ]]; then
record_pass "ufw default deny policy" "${ufw_default}"
else
record_fail "ufw default deny policy" "${ufw_default:-unknown}"
fi
else
record_fail "ufw active" "${ufw_status:-not running}"
fi
fi
# firewalld
if command -v firewall-cmd &>/dev/null; then
local fwd_state
fwd_state=$(firewall-cmd --state 2>/dev/null) || fwd_state=""
verbose "firewalld state: ${fwd_state}"
if [[ "$fwd_state" == "running" ]]; then
record_pass "firewalld active" "firewall running"
fw_found=true
# Default zone
local default_zone
default_zone=$(firewall-cmd --get-default-zone 2>/dev/null) || default_zone=""
if [[ -n "$default_zone" ]]; then
record_pass "firewalld default zone" "${default_zone}"
fi
elif [[ -z "$fwd_state" ]]; then
verbose "firewalld not running"
else
record_fail "firewalld active" "${fwd_state}"
fi
fi
# iptables (fallback)
if [[ "$fw_found" == "false" ]] && command -v iptables &>/dev/null; then
local ipt_rules
ipt_rules=$(iptables -L INPUT -n 2>/dev/null | wc -l) || ipt_rules=0
verbose "iptables INPUT rules: ${ipt_rules}"
if [[ "$ipt_rules" -gt 2 ]]; then
record_pass "iptables rules present" "$((ipt_rules - 2)) rules in INPUT chain"
fw_found=true
# Check default policy
local ipt_policy
ipt_policy=$(iptables -L INPUT -n 2>/dev/null | head -1 | grep -oP '\(policy \K[^)]+') || ipt_policy=""
if [[ "$ipt_policy" == "DROP" || "$ipt_policy" == "REJECT" ]]; then
record_pass "iptables default deny policy" "INPUT policy ${ipt_policy}"
else
record_fail "iptables default deny policy" "INPUT policy ${ipt_policy:-ACCEPT}"
fi
fi
fi
if [[ "$fw_found" == "false" ]]; then
record_fail "Firewall active" "no firewall detected (ufw/firewalld/iptables)"
fi
}
# ── 3. Time Sync ─────────────────────────────────────────────────────
test_timesync() {
echo ""
echo -e "${BOLD}Time Sync${RESET}"
local sync_found=false
# chrony
if command -v chronyc &>/dev/null; then
local chrony_tracking
chrony_tracking=$(chronyc tracking 2>/dev/null) || chrony_tracking=""
if [[ -n "$chrony_tracking" ]]; then
record_pass "chronyd running" "chrony active"
sync_found=true
# Check sources
local chrony_sources
chrony_sources=$(chronyc sources 2>/dev/null | grep -c '^\^' 2>/dev/null) || chrony_sources=0
if [[ "$chrony_sources" -gt 0 ]]; then
record_pass "NTP sources reachable" "${chrony_sources} source(s) configured"
else
record_fail "NTP sources reachable" "no sources responding"
fi
# Drift check (system time offset)
local offset
offset=$(echo "$chrony_tracking" | grep -oP 'System time\s*:\s*\K[0-9.]+') || offset=""
if [[ -n "$offset" ]]; then
# Compare as string — anything under 1 second is fine
local int_offset
int_offset=$(echo "$offset" | cut -d. -f1)
if [[ "${int_offset:-0}" -eq 0 ]]; then
record_pass "Time drift acceptable" "${offset} seconds offset"
else
record_fail "Time drift acceptable" "${offset} seconds offset (>1s)"
fi
fi
fi
fi
# systemd-timesyncd
if [[ "$sync_found" == "false" ]] && command -v timedatectl &>/dev/null; then
local timesync_status
timesync_status=$(timedatectl show --property=NTPSynchronized --value 2>/dev/null) || timesync_status=""
verbose "NTPSynchronized = ${timesync_status}"
if [[ "$timesync_status" == "yes" ]]; then
record_pass "systemd-timesyncd synchronized" "NTP active"
sync_found=true
elif systemctl is-active systemd-timesyncd &>/dev/null; then
record_pass "systemd-timesyncd running" "service active"
sync_found=true
record_fail "NTP synchronized" "timesyncd running but not synchronized"
fi
fi
if [[ "$sync_found" == "false" ]]; then
record_fail "Time sync service" "no NTP service detected (chrony/systemd-timesyncd)"
fi
}
# ── 4. Services ──────────────────────────────────────────────────────
test_services() {
echo ""
echo -e "${BOLD}Services${RESET}"
# sshd running
if systemctl is-active sshd &>/dev/null || systemctl is-active ssh &>/dev/null; then
record_pass "sshd running" "SSH daemon active"
else
record_fail "sshd running" "SSH daemon not active"
fi
# cron / systemd-timers
if systemctl is-active cron &>/dev/null || systemctl is-active crond &>/dev/null; then
record_pass "cron active" "cron daemon running"
elif systemctl list-timers --no-pager 2>/dev/null | grep -q "\.timer"; then
record_pass "systemd timers active" "timers configured"
else
record_fail "cron/timers active" "no cron or systemd timers found"
fi
# Unnecessary services check
local unnecessary_services=("cups" "cups-browsed" "avahi-daemon" "telnet" "rsh" "rlogin" "tftp" "xinetd")
local bad_found=false
local bad_list=""
for svc in "${unnecessary_services[@]}"; do
if systemctl is-active "$svc" &>/dev/null; then
bad_found=true
bad_list="${bad_list:+${bad_list}, }${svc}"
fi
done
if [[ "$bad_found" == "true" ]]; then
record_fail "No unnecessary services" "running: ${bad_list}"
else
record_pass "No unnecessary services" "cups/avahi/telnet/rsh/tftp not running"
fi
# Expected services (user-defined)
if [[ -n "$EXPECTED_SERVICES" ]]; then
IFS=',' read -ra exp_svcs <<< "$EXPECTED_SERVICES"
for svc in "${exp_svcs[@]}"; do
svc=$(echo "$svc" | xargs) # trim whitespace
if systemctl is-active "$svc" &>/dev/null; then
record_pass "Expected service: ${svc}" "running"
else
record_fail "Expected service: ${svc}" "not running"
fi
done
fi
}
# ── 5. Users ─────────────────────────────────────────────────────────
test_users() {
echo ""
echo -e "${BOLD}Users${RESET}"
# UID 0 accounts (should only be root)
local uid0_accounts
uid0_accounts=$(awk -F: '$3 == 0 { print $1 }' /etc/passwd 2>/dev/null) || uid0_accounts=""
local uid0_count
uid0_count=$(echo "$uid0_accounts" | grep -c . 2>/dev/null) || uid0_count=0
if [[ "$uid0_count" -eq 1 && "$uid0_accounts" == "root" ]]; then
record_pass "No extra UID 0 accounts" "only root has UID 0"
elif [[ "$uid0_count" -gt 1 ]]; then
record_fail "No extra UID 0 accounts" "UID 0: ${uid0_accounts//$'\n'/, }"
else
record_pass "No extra UID 0 accounts" "only root has UID 0"
fi
# Empty passwords
if [[ -r /etc/shadow ]]; then
local empty_real
empty_real=$(awk -F: '$2 == "" { print $1 }' /etc/shadow 2>/dev/null) || empty_real=""
if [[ -z "$empty_real" ]]; then
record_pass "No empty passwords" "all accounts have passwords or are locked"
else
record_fail "No empty passwords" "empty password: ${empty_real//$'\n'/, }"
fi
else
record_skip "No empty passwords" "/etc/shadow not readable (run as root)"
fi
# Password aging (PASS_MAX_DAYS in login.defs)
if [[ -f /etc/login.defs ]]; then
local max_days
max_days=$(grep -E "^PASS_MAX_DAYS" /etc/login.defs 2>/dev/null | awk '{print $2}') || max_days=""
verbose "PASS_MAX_DAYS = ${max_days:-<unset>}"
if [[ -n "$max_days" && "$max_days" -le 365 && "$max_days" -gt 0 ]]; then
record_pass "Password aging configured" "PASS_MAX_DAYS=${max_days}"
elif [[ -n "$max_days" && "$max_days" -gt 365 ]]; then
record_fail "Password aging configured" "PASS_MAX_DAYS=${max_days} (>365)"
else
record_fail "Password aging configured" "PASS_MAX_DAYS not set or invalid"
fi
else
record_skip "Password aging configured" "/etc/login.defs not found"
fi
# /etc/shadow permissions
if [[ -f /etc/shadow ]]; then
local shadow_perms
shadow_perms=$(stat -c '%a' /etc/shadow 2>/dev/null) || shadow_perms=""
verbose "/etc/shadow permissions: ${shadow_perms}"
if [[ "$shadow_perms" == "640" || "$shadow_perms" == "600" || "$shadow_perms" == "000" ]]; then
record_pass "/etc/shadow permissions" "${shadow_perms}"
else
record_fail "/etc/shadow permissions" "${shadow_perms} (expected 640 or stricter)"
fi
fi
}
# ── 6. Filesystem ────────────────────────────────────────────────────
test_filesystem() {
echo ""
echo -e "${BOLD}Filesystem${RESET}"
# /tmp noexec/nosuid
local tmp_opts
tmp_opts=$(findmnt -n -o OPTIONS /tmp 2>/dev/null) || tmp_opts=""
verbose "/tmp mount options: ${tmp_opts:-<not mounted separately>}"
if [[ -n "$tmp_opts" ]]; then
local tmp_pass=true
local tmp_detail=""
if [[ "$tmp_opts" == *"noexec"* ]]; then
tmp_detail="noexec"
else
tmp_pass=false
fi
if [[ "$tmp_opts" == *"nosuid"* ]]; then
tmp_detail="${tmp_detail:+${tmp_detail},}nosuid"
else
tmp_pass=false
fi
if [[ "$tmp_pass" == "true" ]]; then
record_pass "/tmp noexec,nosuid" "${tmp_detail}"
else
record_fail "/tmp noexec,nosuid" "missing options (current: ${tmp_opts})"
fi
else
record_fail "/tmp noexec,nosuid" "/tmp not mounted as separate partition"
fi
# /var separate partition
local var_mount
var_mount=$(findmnt -n -o SOURCE /var 2>/dev/null) || var_mount=""
local root_mount
root_mount=$(findmnt -n -o SOURCE / 2>/dev/null) || root_mount=""
verbose "/var source: ${var_mount:-<same as root>}"
if [[ -n "$var_mount" && "$var_mount" != "$root_mount" ]]; then
record_pass "/var separate partition" "${var_mount}"
else
record_fail "/var separate partition" "not on separate partition"
fi
# /home separate partition
local home_mount
home_mount=$(findmnt -n -o SOURCE /home 2>/dev/null) || home_mount=""
verbose "/home source: ${home_mount:-<same as root>}"
if [[ -n "$home_mount" && "$home_mount" != "$root_mount" ]]; then
record_pass "/home separate partition" "${home_mount}"
else
record_fail "/home separate partition" "not on separate partition"
fi
# Sticky bit on /tmp
local sticky
sticky=$(stat -c '%a' /tmp 2>/dev/null) || sticky=""
verbose "/tmp mode: ${sticky}"
if [[ "${sticky:0:1}" == "1" || "$sticky" == "1777" ]]; then
record_pass "Sticky bit on /tmp" "mode ${sticky}"
else
record_fail "Sticky bit on /tmp" "mode ${sticky:-unknown} (expected 1777)"
fi
}
# ── 7. Kernel ─────────────────────────────────────────────────────────
test_kernel() {
echo ""
echo -e "${BOLD}Kernel${RESET}"
# ASLR
local aslr
aslr=$(cat /proc/sys/kernel/randomize_va_space 2>/dev/null) || aslr=""
verbose "ASLR (randomize_va_space) = ${aslr}"
if [[ "$aslr" == "2" ]]; then
record_pass "ASLR enabled" "randomize_va_space=2 (full)"
elif [[ "$aslr" == "1" ]]; then
record_pass "ASLR enabled" "randomize_va_space=1 (partial)"
else
record_fail "ASLR enabled" "randomize_va_space=${aslr:-not readable}"
fi
# SYN cookies
local syncookies
syncookies=$(cat /proc/sys/net/ipv4/tcp_syncookies 2>/dev/null) || syncookies=""
verbose "tcp_syncookies = ${syncookies}"
if [[ "$syncookies" == "1" ]]; then
record_pass "SYN cookies enabled" "tcp_syncookies=1"
else
record_fail "SYN cookies enabled" "tcp_syncookies=${syncookies:-not readable}"
fi
# IP forwarding (should be disabled unless router)
local ip_forward
ip_forward=$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null) || ip_forward=""
verbose "ip_forward = ${ip_forward}"
if [[ "$ip_forward" == "0" ]]; then
record_pass "IP forwarding disabled" "ip_forward=0"
else
record_fail "IP forwarding disabled" "ip_forward=${ip_forward:-not readable} (enabled — expected unless router)"
fi
# Core dumps restricted
local core_pattern
core_pattern=$(cat /proc/sys/kernel/core_pattern 2>/dev/null) || core_pattern=""
local core_limit
core_limit=$(ulimit -c 2>/dev/null) || core_limit=""
verbose "core_pattern = ${core_pattern}"
verbose "ulimit -c = ${core_limit}"
if [[ "$core_limit" == "0" ]]; then
record_pass "Core dumps restricted" "ulimit -c = 0"
elif [[ "$core_pattern" == *"systemd-coredump"* || "$core_pattern" == "|"* ]]; then
record_pass "Core dumps restricted" "handled by ${core_pattern%% *}"
else
record_fail "Core dumps restricted" "core dumps enabled (ulimit -c = ${core_limit})"
fi
}
# ── 8. Updates ────────────────────────────────────────────────────────
test_updates() {
if [[ "$SKIP_UPDATES" == "true" ]]; then
echo ""
echo -e "${BOLD}Updates${RESET}"
record_skip "Pending updates" "SKIP_UPDATES=true"
return
fi
echo ""
echo -e "${BOLD}Updates${RESET}"
# Detect package manager and check for pending security updates
if command -v apt-get &>/dev/null; then
verbose "Package manager: apt"
local updates
updates=$(apt-get -s upgrade 2>/dev/null | grep -c "^Inst" 2>/dev/null) || updates=0
local security
security=$(apt-get -s upgrade 2>/dev/null | grep -c "security" 2>/dev/null) || security=0
if [[ "$security" -gt 0 ]]; then
record_fail "Security updates pending" "${security} security, ${updates} total"
elif [[ "$updates" -gt 0 ]]; then
record_pass "No security updates pending" "${updates} non-security updates available"
else
record_pass "System up to date" "no pending updates"
fi
elif command -v dnf &>/dev/null; then
verbose "Package manager: dnf"
local sec_updates
sec_updates=$(dnf updateinfo list --security 2>/dev/null | grep -c "security" 2>/dev/null) || sec_updates=0
if [[ "$sec_updates" -gt 0 ]]; then
record_fail "Security updates pending" "${sec_updates} security updates"
else
record_pass "No security updates pending" "dnf reports no security updates"
fi
elif command -v yum &>/dev/null; then
verbose "Package manager: yum"
local yum_sec
yum_sec=$(yum updateinfo list security 2>/dev/null | grep -c "security" 2>/dev/null) || yum_sec=0
if [[ "$yum_sec" -gt 0 ]]; then
record_fail "Security updates pending" "${yum_sec} security updates"
else
record_pass "No security updates pending" "yum reports no security updates"
fi
else
record_skip "Pending updates" "no supported package manager found (apt/dnf/yum)"
fi
}
# ── 9. Permissions ───────────────────────────────────────────────────
test_permissions() {
echo ""
echo -e "${BOLD}Permissions${RESET}"
# /etc/passwd (should be 644)
local passwd_perms
passwd_perms=$(stat -c '%a' /etc/passwd 2>/dev/null) || passwd_perms=""
verbose "/etc/passwd permissions: ${passwd_perms}"
if [[ "$passwd_perms" == "644" ]]; then
record_pass "/etc/passwd permissions" "${passwd_perms}"
else
record_fail "/etc/passwd permissions" "${passwd_perms} (expected 644)"
fi
# /etc/shadow (should be 640 or stricter)
if [[ -f /etc/shadow ]]; then
local shadow_perms
shadow_perms=$(stat -c '%a' /etc/shadow 2>/dev/null) || shadow_perms=""
if [[ "$shadow_perms" == "640" || "$shadow_perms" == "600" || "$shadow_perms" == "000" ]]; then
record_pass "/etc/shadow permissions" "${shadow_perms}"
else
record_fail "/etc/shadow permissions" "${shadow_perms} (expected 640 or stricter)"
fi
fi
# /etc/gshadow (should be 640 or stricter)
if [[ -f /etc/gshadow ]]; then
local gshadow_perms
gshadow_perms=$(stat -c '%a' /etc/gshadow 2>/dev/null) || gshadow_perms=""
verbose "/etc/gshadow permissions: ${gshadow_perms}"
if [[ "$gshadow_perms" == "640" || "$gshadow_perms" == "600" || "$gshadow_perms" == "000" ]]; then
record_pass "/etc/gshadow permissions" "${gshadow_perms}"
else
record_fail "/etc/gshadow permissions" "${gshadow_perms} (expected 640 or stricter)"
fi
fi
# World-writable files in /etc
local ww_files
ww_files=$(find /etc -maxdepth 2 -type f -perm -0002 2>/dev/null | head -10) || ww_files=""
local ww_count
ww_count=$(echo "$ww_files" | grep -c . 2>/dev/null) || ww_count=0
if [[ -z "$ww_files" ]]; then
record_pass "No world-writable files in /etc" "clean"
else
record_fail "No world-writable files in /etc" "${ww_count} found"
fi
}
# ── 10. Logging ──────────────────────────────────────────────────────
test_logging() {
echo ""
echo -e "${BOLD}Logging${RESET}"
# rsyslog or journald
local logging_found=false
if systemctl is-active rsyslog &>/dev/null; then
record_pass "rsyslog running" "syslog daemon active"
logging_found=true
fi
if systemctl is-active systemd-journald &>/dev/null; then
record_pass "journald running" "systemd journal active"
logging_found=true
fi
if [[ "$logging_found" == "false" ]]; then
record_fail "Logging service" "neither rsyslog nor journald running"
fi
# /var/log permissions
local varlog_perms
varlog_perms=$(stat -c '%a' /var/log 2>/dev/null) || varlog_perms=""
verbose "/var/log permissions: ${varlog_perms}"
if [[ "$varlog_perms" == "755" || "$varlog_perms" == "750" || "$varlog_perms" == "700" ]]; then
record_pass "/var/log permissions" "${varlog_perms}"
else
record_fail "/var/log permissions" "${varlog_perms} (expected 755 or stricter)"
fi
}
# ══════════════════════════════════════════════════════════════════════
# OUTPUT
# ══════════════════════════════════════════════════════════════════════
print_summary() {
local end_time
end_time=$(date +%s)
local duration=$(( end_time - START_TIME ))
local hostname
hostname=$(hostname 2>/dev/null || echo "unknown")
echo ""
echo -e "${BOLD}────────────────────────────────────────${RESET}"
echo -e "${BOLD}Summary${RESET} ${hostname}"
echo -e " ${GREEN}${PASS} passed${RESET} ${RED}${FAIL} failed${RESET} ${YELLOW}${SKIP} skipped${RESET} (${duration}s)"
echo -e "${BOLD}────────────────────────────────────────${RESET}"
if [[ $FAIL -eq 0 ]]; then
echo -e "${GREEN}${BOLD}All checks passed.${RESET}"
else
echo -e "${RED}${BOLD}${FAIL} check(s) failed.${RESET}"
fi
}
print_tap_header() {
echo "TAP version 13"
}
print_tap_footer() {
echo "1..${TOTAL}"
echo "# pass ${PASS}"
echo "# fail ${FAIL}"
echo "# skip ${SKIP}"
}
write_junit() {
local end_time
end_time=$(date +%s)
local duration=$(( end_time - START_TIME ))
cat > "$JUNIT_FILE" <<JUNIT_EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
<testsuite name="linux-baseline-checks" tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
JUNIT_EOF
for result in "${RESULTS[@]}"; do
local status name detail
status=$(echo "$result" | cut -d'|' -f1)
name=$(echo "$result" | cut -d'|' -f2)
detail=$(echo "$result" | cut -d'|' -f3)
# XML-escape the values
name=$(echo "$name" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
detail=$(echo "$detail" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
case "$status" in
PASS)
echo " <testcase name=\"${name}\" classname=\"baseline\">" >> "$JUNIT_FILE"
[[ -n "$detail" ]] && echo " <system-out>${detail}</system-out>" >> "$JUNIT_FILE"
echo " </testcase>" >> "$JUNIT_FILE"
;;
FAIL)
echo " <testcase name=\"${name}\" classname=\"baseline\">" >> "$JUNIT_FILE"
echo " <failure message=\"${detail}\">FAILED: ${name}${detail}</failure>" >> "$JUNIT_FILE"
echo " </testcase>" >> "$JUNIT_FILE"
;;
SKIP)
echo " <testcase name=\"${name}\" classname=\"baseline\">" >> "$JUNIT_FILE"
echo " <skipped message=\"${detail}\"/>" >> "$JUNIT_FILE"
echo " </testcase>" >> "$JUNIT_FILE"
;;
esac
done
echo " </testsuite>" >> "$JUNIT_FILE"
echo "</testsuites>" >> "$JUNIT_FILE"
log "JUnit report written to ${JUNIT_FILE}"
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Post-provision baseline security validation for Linux servers.
Zero external dependencies — bash, coreutils, ss only. curl/openssl optional.
Options:
--skip-ssh Skip SSH hardening checks
--skip-firewall Skip firewall checks
--skip-updates Skip pending updates check
--format FORMAT Output: text (default), tap, junit
--junit-file FILE JUnit output path (default: baseline-results.xml)
--verbose Show debug output
--no-color Disable colored output
--help Show this help
Environment variables:
SKIP_SSH Skip SSH checks (same as --skip-ssh)
SKIP_FIREWALL Skip firewall checks (same as --skip-firewall)
SKIP_UPDATES Skip updates check (same as --skip-updates)
EXPECTED_SERVICES Comma-separated list of services to verify running
CURL_TIMEOUT HTTP timeout in seconds (default: 10)
OUTPUT_FORMAT Output format (same as --format)
JUNIT_FILE JUnit output path (same as --junit-file)
Examples:
# Run all checks
sudo ./$(basename "$0")
# Skip SSH and updates, JUnit output
sudo ./$(basename "$0") --skip-ssh --skip-updates --format junit
# TAP output for CI
sudo ./$(basename "$0") --format tap
# Check specific expected services
EXPECTED_SERVICES="nginx,prometheus-node-exporter" sudo ./$(basename "$0")
EOF
}
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-ssh) SKIP_SSH=true ;;
--skip-firewall) SKIP_FIREWALL=true ;;
--skip-updates) SKIP_UPDATES=true ;;
--format) OUTPUT_FORMAT="$2"; shift ;;
--junit-file) JUNIT_FILE="$2"; shift ;;
--verbose) VERBOSE=true ;;
--no-color) COLOR=never ;;
--help|-h) usage; exit 0 ;;
*) err "Unknown option: $1"; usage; exit 1 ;;
esac
shift
done
setup_colors
START_TIME=$(date +%s)
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
print_tap_header
else
echo ""
echo -e "${BOLD}Linux Baseline Checks${RESET}"
echo -e "Host: $(hostname 2>/dev/null || echo 'unknown')"
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo -e "Kernel: $(uname -r)"
echo ""
fi
# Run test suites
test_ssh
test_firewall
test_timesync
test_services
test_users
test_filesystem
test_kernel
test_updates
test_permissions
test_logging
# Output
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
print_tap_footer
elif [[ "$OUTPUT_FORMAT" == "junit" ]]; then
print_summary
write_junit
else
print_summary
fi
# Exit code
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
}
main "$@"