Files
linux-scripts/zero-trust-audit.sh
T
chiefgeek a1a17e81a1 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.
2026-05-25 03:31:08 +02:00

782 lines
33 KiB
Bash

#!/usr/bin/env bash
#########################################################################################
#### zero-trust-audit.sh — Audit Linux systems for zero-trust security posture ####
#### Identity, network, encryption, least privilege, audit logging, supply chain ####
#### Requires: bash 4+, root recommended ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.01 ####
#### ####
#### Usage: ####
#### sudo ./zero-trust-audit.sh --audit ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
RUN_MODE=""
SECTION_FILTER="${ZT_SECTION:-all}"
OUTPUT_FORMAT="${ZT_FORMAT:-text}"
OUTPUT_FILE=""
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""
PASS_COUNT=0
FAIL_COUNT=0
WARN_COUNT=0
TOTAL_POSSIBLE=0
REMEDIATIONS=()
# ── Colors ────────────────────────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" 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'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" 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 "${DIM}[DEBUG]${RESET} $*"; fi; }
die() { err "$*"; exit 1; }
# ── Helpers ───────────────────────────────────────────────────────────
section_header() {
echo ""
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
echo ""
}
field() {
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
}
field_color() {
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
}
elapsed() {
local end_time
end_time=$(date +%s)
echo "$(( end_time - START_TIME ))s"
}
# ── Check helpers ────────────────────────────────────────────────────
check_pass() {
local name="$1" detail="${2:-}"
((PASS_COUNT++)) || true
((TOTAL_POSSIBLE++)) || true
printf " ${GREEN}✓ PASS${RESET} %-38s %b\n" "$name" "${DIM}${detail}${RESET}"
}
check_fail() {
local name="$1" detail="${2:-}" remediation="${3:-}"
((FAIL_COUNT++)) || true
((TOTAL_POSSIBLE++)) || true
printf " ${RED}✗ FAIL${RESET} %-38s %b\n" "$name" "${DIM}${detail}${RESET}"
if [[ -n "$remediation" ]]; then
REMEDIATIONS+=("${name}|${remediation}")
fi
}
check_warn() {
local name="$1" detail="${2:-}" remediation="${3:-}"
((WARN_COUNT++)) || true
((TOTAL_POSSIBLE++)) || true
printf " ${YELLOW}! WARN${RESET} %-38s %b\n" "$name" "${DIM}${detail}${RESET}"
if [[ -n "$remediation" ]]; then
REMEDIATIONS+=("${name}|${remediation}")
fi
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT: IDENTITY
# ══════════════════════════════════════════════════════════════════════
audit_identity() {
section_header "Identity & Authentication"
# SSH: PermitRootLogin
if [[ -f /etc/ssh/sshd_config ]]; then
local root_login
root_login=$(grep -i "^[[:space:]]*PermitRootLogin" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo "")
if [[ "${root_login,,}" == "no" ]]; then
check_pass "SSH root login disabled" "PermitRootLogin no"
else
check_fail "SSH root login enabled" "Current: ${root_login:-not set}" \
"Set 'PermitRootLogin no' in /etc/ssh/sshd_config"
fi
# SSH: PasswordAuthentication
local pass_auth
pass_auth=$(grep -i "^[[:space:]]*PasswordAuthentication" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo "")
if [[ "${pass_auth,,}" == "no" ]]; then
check_pass "SSH password auth disabled" "PasswordAuthentication no"
else
check_fail "SSH password auth enabled" "Current: ${pass_auth:-not set}" \
"Set 'PasswordAuthentication no' in /etc/ssh/sshd_config"
fi
# SSH: PubkeyAuthentication
local pubkey_auth
pubkey_auth=$(grep -i "^[[:space:]]*PubkeyAuthentication" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo "")
if [[ "${pubkey_auth,,}" != "no" ]]; then
check_pass "SSH pubkey auth enabled" "PubkeyAuthentication yes"
else
check_fail "SSH pubkey auth disabled" "PubkeyAuthentication no" \
"Set 'PubkeyAuthentication yes' in /etc/ssh/sshd_config"
fi
else
check_warn "SSH config not found" "/etc/ssh/sshd_config missing"
fi
# Password policy: minlen
if [[ -f /etc/security/pwquality.conf ]]; then
local minlen
minlen=$(grep -i "^[[:space:]]*minlen" /etc/security/pwquality.conf 2>/dev/null | awk -F= '{print $2}' | tr -d ' ' || echo "")
if [[ -n "$minlen" ]] && [[ "$minlen" -ge 12 ]]; then
check_pass "Password minimum length" "minlen=${minlen}"
else
check_fail "Password min length weak" "minlen=${minlen:-not set}" \
"Set 'minlen = 14' in /etc/security/pwquality.conf"
fi
else
check_warn "Password quality config missing" "/etc/security/pwquality.conf not found" \
"Install libpam-pwquality and configure /etc/security/pwquality.conf"
fi
# Sudoers: NOPASSWD
local nopasswd_count=0
if [[ -f /etc/sudoers ]]; then
nopasswd_count=$(grep -c "NOPASSWD" /etc/sudoers 2>/dev/null || true)
fi
if [[ -d /etc/sudoers.d ]]; then
local extra
extra=$(grep -r "NOPASSWD" /etc/sudoers.d/ 2>/dev/null | wc -l || true)
nopasswd_count=$(( nopasswd_count + extra ))
fi
if [[ "$nopasswd_count" -eq 0 ]]; then
check_pass "No NOPASSWD sudo rules" "0 entries"
else
check_warn "NOPASSWD sudo rules found" "${nopasswd_count} entries" \
"Review sudoers for unnecessary NOPASSWD rules"
fi
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT: NETWORK
# ══════════════════════════════════════════════════════════════════════
audit_network() {
section_header "Network Security"
# Firewall active
local fw_active=false
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
fw_active=true
check_pass "Firewall active" "ufw enabled"
elif command -v nft &>/dev/null && nft list ruleset 2>/dev/null | grep -q "chain"; then
fw_active=true
check_pass "Firewall active" "nftables loaded"
elif iptables -L -n 2>/dev/null | grep -qv "^Chain .* (policy" | grep -q .; then
fw_active=true
check_pass "Firewall active" "iptables rules present"
fi
if [[ "$fw_active" == "false" ]]; then
check_fail "No firewall detected" "No ufw/nftables/iptables rules" \
"Install and enable ufw or nftables"
fi
# Open ports
local listen_count=0
if command -v ss &>/dev/null; then
listen_count=$(ss -tulnH 2>/dev/null | wc -l || true)
fi
if [[ "$listen_count" -le 10 ]]; then
check_pass "Listening ports minimal" "${listen_count} ports"
else
check_warn "Many listening ports" "${listen_count} ports" \
"Review listening services and disable unnecessary ones"
fi
# IP forwarding
local ipv4_fwd
ipv4_fwd=$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo "0")
if [[ "$ipv4_fwd" == "0" ]]; then
check_pass "IP forwarding disabled" "net.ipv4.ip_forward=0"
else
check_fail "IP forwarding enabled" "net.ipv4.ip_forward=1" \
"Set 'net.ipv4.ip_forward=0' in /etc/sysctl.conf"
fi
# ICMP redirect
local icmp_redirect
icmp_redirect=$(cat /proc/sys/net/ipv4/conf/all/accept_redirects 2>/dev/null || echo "0")
if [[ "$icmp_redirect" == "0" ]]; then
check_pass "ICMP redirects disabled" "accept_redirects=0"
else
check_warn "ICMP redirects accepted" "accept_redirects=1" \
"Set 'net.ipv4.conf.all.accept_redirects=0' in /etc/sysctl.conf"
fi
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT: TRANSIT ENCRYPTION
# ══════════════════════════════════════════════════════════════════════
audit_transit() {
section_header "Transit Encryption"
# SSH protocol
if [[ -f /etc/ssh/sshd_config ]]; then
local protocol
protocol=$(grep -i "^[[:space:]]*Protocol" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo "")
if [[ -z "$protocol" ]] || [[ "$protocol" == "2" ]]; then
check_pass "SSH protocol version 2" "Protocol 2 (default)"
else
check_fail "SSH protocol includes v1" "Protocol ${protocol}" \
"Remove Protocol 1 from /etc/ssh/sshd_config"
fi
# SSH ciphers restricted
local ciphers
ciphers=$(grep -i "^[[:space:]]*Ciphers" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo "")
if [[ -n "$ciphers" ]]; then
check_pass "SSH ciphers restricted" "Custom cipher list configured"
else
check_warn "SSH ciphers use defaults" "No explicit cipher restriction" \
"Set 'Ciphers' in /etc/ssh/sshd_config to restrict to modern ciphers"
fi
# SSH MACs restricted
local macs
macs=$(grep -i "^[[:space:]]*MACs" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo "")
if [[ -n "$macs" ]]; then
check_pass "SSH MACs restricted" "Custom MAC list configured"
else
check_warn "SSH MACs use defaults" "No explicit MAC restriction" \
"Set 'MACs' in /etc/ssh/sshd_config to restrict to etm variants"
fi
else
check_warn "SSH config not found" "Cannot verify SSH encryption settings"
fi
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT: ENCRYPTION AT REST
# ══════════════════════════════════════════════════════════════════════
audit_encryption() {
section_header "Encryption at Rest"
# LUKS volumes
local luks_count=0
if command -v lsblk &>/dev/null; then
luks_count=$(lsblk --fs 2>/dev/null | grep -c "crypto_LUKS" || true)
fi
if [[ "$luks_count" -gt 0 ]]; then
check_pass "LUKS encrypted volumes" "${luks_count} volume(s)"
else
check_warn "No LUKS encryption detected" "0 encrypted volumes" \
"Consider encrypting data volumes with LUKS"
fi
# Swap encryption
local swap_active
swap_active=$(swapon --show --noheadings 2>/dev/null | wc -l || true)
if [[ "$swap_active" -eq 0 ]]; then
check_pass "No unencrypted swap" "Swap disabled or absent"
else
local swap_encrypted=false
if swapon --show --noheadings 2>/dev/null | awk '{print $1}' | while read -r dev; do
if [[ "$dev" == /dev/dm-* ]] || [[ "$dev" == /dev/mapper/* ]]; then
return 0
fi
done; then
swap_encrypted=true
fi
if [[ "$swap_encrypted" == "true" ]]; then
check_pass "Swap appears encrypted" "Swap on dm/mapper device"
else
check_warn "Swap may be unencrypted" "${swap_active} swap device(s)" \
"Configure encrypted swap or disable swap"
fi
fi
# /tmp on tmpfs
if mount | grep -q "tmpfs on /tmp"; then
check_pass "/tmp on tmpfs" "In-memory tmpfs"
else
check_warn "/tmp not on tmpfs" "Persistent /tmp filesystem" \
"Mount /tmp as tmpfs in /etc/fstab"
fi
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT: LEAST PRIVILEGE
# ══════════════════════════════════════════════════════════════════════
audit_least_privilege() {
section_header "Least Privilege"
# SUID binaries
local suid_count=0
suid_count=$(find / -perm -4000 -type f 2>/dev/null | wc -l || true)
if [[ "$suid_count" -le 20 ]]; then
check_pass "SUID binaries minimal" "${suid_count} found"
elif [[ "$suid_count" -le 40 ]]; then
check_warn "Elevated SUID count" "${suid_count} found" \
"Review SUID binaries: find / -perm -4000 -type f"
else
check_fail "High SUID binary count" "${suid_count} found" \
"Audit and remove unnecessary SUID bits"
fi
# SELinux / AppArmor
local mac_active=false
if command -v getenforce &>/dev/null; then
local se_status
se_status=$(getenforce 2>/dev/null || echo "Disabled")
if [[ "$se_status" == "Enforcing" ]]; then
mac_active=true
check_pass "SELinux enforcing" "Mode: ${se_status}"
elif [[ "$se_status" == "Permissive" ]]; then
mac_active=true
check_warn "SELinux permissive" "Not enforcing" \
"Set SELINUX=enforcing in /etc/selinux/config"
fi
fi
if [[ "$mac_active" == "false" ]] && command -v aa-status &>/dev/null; then
local aa_profiles
aa_profiles=$(aa-status --profiled 2>/dev/null || true)
if [[ "$aa_profiles" -gt 0 ]]; then
mac_active=true
check_pass "AppArmor active" "${aa_profiles} profiles loaded"
fi
fi
if [[ "$mac_active" == "false" ]]; then
check_fail "No MAC framework active" "Neither SELinux nor AppArmor" \
"Enable SELinux or AppArmor for mandatory access control"
fi
# Default umask
local umask_val
umask_val=$(umask 2>/dev/null || echo "0022")
if [[ "$umask_val" == "0027" ]] || [[ "$umask_val" == "0077" ]]; then
check_pass "Restrictive umask" "umask ${umask_val}"
elif [[ "$umask_val" == "0022" ]]; then
check_warn "Standard umask" "umask ${umask_val}" \
"Set umask to 0027 in /etc/profile or /etc/login.defs"
else
check_warn "Non-standard umask" "umask ${umask_val}" \
"Review umask setting for appropriateness"
fi
# World-writable directories (outside /tmp, /var/tmp)
local ww_count=0
ww_count=$(find / -maxdepth 3 -type d -perm -0002 \
! -path "/tmp*" ! -path "/var/tmp*" ! -path "/dev*" \
! -path "/proc*" ! -path "/sys*" ! -path "/run*" \
2>/dev/null | wc -l || true)
if [[ "$ww_count" -eq 0 ]]; then
check_pass "No world-writable dirs" "Outside /tmp, /var/tmp"
else
check_warn "World-writable dirs found" "${ww_count} directories" \
"Review: find / -maxdepth 3 -type d -perm -0002"
fi
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT: AUDIT LOGGING
# ══════════════════════════════════════════════════════════════════════
audit_logging() {
section_header "Audit & Logging"
# auditd
if command -v auditctl &>/dev/null; then
if systemctl is-active auditd &>/dev/null; then
check_pass "auditd running" "Active and enabled"
# Audit rules
local rule_count
rule_count=$(auditctl -l 2>/dev/null | grep -cv "^No rules" || true)
if [[ "$rule_count" -gt 0 ]]; then
check_pass "Audit rules configured" "${rule_count} rules"
else
check_warn "No audit rules defined" "auditd has no rules" \
"Add audit rules in /etc/audit/rules.d/"
fi
else
check_fail "auditd not running" "Service inactive" \
"Enable with: systemctl enable --now auditd"
fi
else
check_fail "auditd not installed" "No audit framework" \
"Install auditd: apt install auditd / yum install audit"
fi
# Journal persistence
if [[ -d /var/log/journal ]]; then
check_pass "Journal persistence" "Logs stored in /var/log/journal"
else
check_warn "Journal volatile only" "Logs lost on reboot" \
"Create /var/log/journal and restart systemd-journald"
fi
# Syslog active
if systemctl is-active rsyslog &>/dev/null || systemctl is-active syslog-ng &>/dev/null; then
check_pass "Syslog daemon active" "rsyslog or syslog-ng running"
else
check_warn "No syslog daemon detected" "Relying on journal only" \
"Install rsyslog for remote logging capability"
fi
# Log rotation
if [[ -f /etc/logrotate.conf ]] || [[ -d /etc/logrotate.d ]]; then
check_pass "Log rotation configured" "logrotate present"
else
check_warn "No log rotation" "logrotate not found" \
"Install logrotate to prevent disk exhaustion"
fi
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT: SUPPLY CHAIN
# ══════════════════════════════════════════════════════════════════════
audit_supply_chain() {
section_header "Supply Chain"
# Package signing
if command -v apt-get &>/dev/null; then
local unsigned_repos=0
if [[ -d /etc/apt/sources.list.d ]]; then
unsigned_repos=$(grep -r "^\s*deb\s" /etc/apt/sources.list.d/ /etc/apt/sources.list 2>/dev/null \
| grep -c "\[.*trusted=yes.*\]" || true)
fi
if [[ "$unsigned_repos" -eq 0 ]]; then
check_pass "APT repos signed" "No trusted=yes overrides"
else
check_fail "Unsigned APT repos" "${unsigned_repos} repos skip GPG" \
"Remove trusted=yes from APT source entries"
fi
elif command -v dnf &>/dev/null || command -v yum &>/dev/null; then
local gpg_disabled=0
gpg_disabled=$(grep -r "gpgcheck=0" /etc/yum.repos.d/ 2>/dev/null | wc -l || true)
if [[ "$gpg_disabled" -eq 0 ]]; then
check_pass "RPM repos GPG enabled" "All repos have gpgcheck=1"
else
check_fail "GPG check disabled" "${gpg_disabled} repos without gpgcheck" \
"Set gpgcheck=1 in all repo files under /etc/yum.repos.d/"
fi
else
check_warn "Unknown package manager" "Cannot verify signing"
fi
# Automatic updates
if command -v unattended-upgrade &>/dev/null || \
dpkg -l unattended-upgrades &>/dev/null 2>&1; then
check_pass "Automatic security updates" "unattended-upgrades installed"
elif command -v dnf &>/dev/null && rpm -q dnf-automatic &>/dev/null 2>&1; then
check_pass "Automatic security updates" "dnf-automatic installed"
else
check_warn "No automatic updates" "Manual patching required" \
"Install unattended-upgrades (Debian) or dnf-automatic (RHEL)"
fi
# Package integrity
if command -v debsums &>/dev/null; then
check_pass "Package integrity tool" "debsums available"
elif command -v rpm &>/dev/null; then
check_pass "Package integrity tool" "rpm -V available"
else
check_warn "No integrity checker" "Cannot verify package files" \
"Install debsums (Debian) for package integrity checks"
fi
}
# ══════════════════════════════════════════════════════════════════════
# RUN SECTION DISPATCHER
# ══════════════════════════════════════════════════════════════════════
run_section() {
local name="$1"
case "$name" in
identity) audit_identity ;;
network) audit_network ;;
transit) audit_transit ;;
encryption) audit_encryption ;;
least-privilege) audit_least_privilege ;;
logging) audit_logging ;;
supply-chain) audit_supply_chain ;;
*) die "Unknown section: ${name}" ;;
esac
}
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════
print_summary() {
local score=0 rating=""
if [[ "$TOTAL_POSSIBLE" -gt 0 ]]; then
score=$(( PASS_COUNT * 100 / TOTAL_POSSIBLE ))
fi
if [[ "$score" -ge 90 ]]; then
rating="${GREEN}STRONG${RESET}"
elif [[ "$score" -ge 70 ]]; then
rating="${BLUE}GOOD${RESET}"
elif [[ "$score" -ge 50 ]]; then
rating="${YELLOW}MODERATE${RESET}"
else
rating="${RED}WEAK${RESET}"
fi
section_header "Zero Trust Score"
field "Total checks:" "$TOTAL_POSSIBLE"
field_color "Passed:" "${GREEN}${PASS_COUNT}${RESET}"
field_color "Failed:" "${RED}${FAIL_COUNT}${RESET}"
field_color "Warnings:" "${YELLOW}${WARN_COUNT}${RESET}"
field "Score:" "${score}% (${PASS_COUNT}/${TOTAL_POSSIBLE})"
field_color "Rating:" "$rating"
echo ""
field "Duration:" "$(elapsed)"
}
# ══════════════════════════════════════════════════════════════════════
# MODES
# ══════════════════════════════════════════════════════════════════════
do_audit() {
log "Running zero-trust security audit..."
local sections=("identity" "network" "transit" "encryption" "least-privilege" "logging" "supply-chain")
if [[ "$SECTION_FILTER" != "all" ]]; then
IFS=',' read -ra sections <<< "$SECTION_FILTER"
fi
for s in "${sections[@]}"; do
run_section "$s"
done
print_summary
}
do_remediate() {
log "Running audit with remediation suggestions..."
local sections=("identity" "network" "transit" "encryption" "least-privilege" "logging" "supply-chain")
if [[ "$SECTION_FILTER" != "all" ]]; then
IFS=',' read -ra sections <<< "$SECTION_FILTER"
fi
for s in "${sections[@]}"; do
run_section "$s"
done
print_summary
if [[ ${#REMEDIATIONS[@]} -gt 0 ]]; then
section_header "Remediation Steps"
printf " ${BOLD}%-40s %s${RESET}\n" "FINDING" "REMEDIATION"
printf " %s\n" "$(printf '%.0s─' {1..72})"
for entry in "${REMEDIATIONS[@]}"; do
local finding remediation
finding="${entry%%|*}"
remediation="${entry#*|}"
printf " %-40s %s\n" "$finding" "$remediation"
done
else
log "No remediation steps needed — all checks passed"
fi
}
do_export() {
[[ -z "$OUTPUT_FILE" ]] && die "No output file specified (--output-file)"
log "Running audit and exporting results..."
local sections=("identity" "network" "transit" "encryption" "least-privilege" "logging" "supply-chain")
for s in "${sections[@]}"; do
run_section "$s"
done
local score=0
if [[ "$TOTAL_POSSIBLE" -gt 0 ]]; then
score=$(( PASS_COUNT * 100 / TOTAL_POSSIBLE ))
fi
case "$OUTPUT_FORMAT" in
json)
if command -v jq &>/dev/null; then
jq -n \
--arg host "$(hostname -f 2>/dev/null || hostname)" \
--arg date "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
--argjson pass "$PASS_COUNT" \
--argjson fail "$FAIL_COUNT" \
--argjson warn "$WARN_COUNT" \
--argjson total "$TOTAL_POSSIBLE" \
--argjson score "$score" \
'{host:$host, date:$date, pass:$pass, fail:$fail, warn:$warn, total:$total, score:$score}' \
> "$OUTPUT_FILE"
else
cat > "$OUTPUT_FILE" <<EOF
{"host":"$(hostname -f 2>/dev/null || hostname)","date":"$(date -u '+%Y-%m-%dT%H:%M:%SZ')","pass":${PASS_COUNT},"fail":${FAIL_COUNT},"warn":${WARN_COUNT},"total":${TOTAL_POSSIBLE},"score":${score}}
EOF
fi
;;
*)
{
echo "Zero Trust Audit Report"
echo "======================"
echo "Host: $(hostname -f 2>/dev/null || hostname)"
echo "Date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo ""
echo "Pass: ${PASS_COUNT}"
echo "Fail: ${FAIL_COUNT}"
echo "Warn: ${WARN_COUNT}"
echo "Total: ${TOTAL_POSSIBLE}"
echo "Score: ${score}%"
} > "$OUTPUT_FILE"
;;
esac
print_summary
log "Results exported to ${OUTPUT_FILE}"
}
# ══════════════════════════════════════════════════════════════════════
# HELP
# ══════════════════════════════════════════════════════════════════════
show_help() {
cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} — Zero Trust Security Audit
Audit Linux systems for zero-trust security posture across identity,
network, encryption, least privilege, logging, and supply chain.
${BOLD}MODES${RESET}
--audit Run full zero-trust audit (default: all sections)
--section NAMES Audit specific sections (comma-separated)
--remediate Audit with remediation suggestions
--export Export results to file (requires --output-file)
${BOLD}SECTIONS${RESET}
identity SSH, PAM, passwords, sudoers
network Firewall, open ports, IP forwarding
transit TLS, SSH protocol, cipher restrictions
encryption LUKS, swap encryption, tmpfs
least-privilege SUID, capabilities, SELinux/AppArmor, umask
logging auditd, journal, syslog, log rotation
supply-chain Package signing, auto-updates, integrity
${BOLD}OPTIONS${RESET}
--format FORMAT Output format for export: text, json (default: text)
--output-file FILE Write export results to file
--verbose Debug output
--no-color Disable colored output
--help Show this help message
${BOLD}ENVIRONMENT VARIABLES${RESET}
ZT_SECTION Default section filter (default: all)
ZT_FORMAT Default output format (default: text)
VERBOSE Enable verbose output (true/false)
COLOR Color mode: auto, always, never
${BOLD}EXAMPLES${RESET}
# Full audit
sudo ${SCRIPT_NAME} --audit
# Audit specific sections
sudo ${SCRIPT_NAME} --audit --section identity,network
# Audit with fix suggestions
sudo ${SCRIPT_NAME} --remediate
# Export results as JSON
sudo ${SCRIPT_NAME} --export --format json --output-file report.json
# CI pipeline check
sudo ${SCRIPT_NAME} --audit --no-color
${BOLD}EXIT CODES${RESET}
0 Audit completed (any score)
1 Runtime error
2 Audit score below 50% (with --strict, not yet implemented)
EOF
}
# ══════════════════════════════════════════════════════════════════════
# PARSE ARGS
# ══════════════════════════════════════════════════════════════════════
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--audit) RUN_MODE="audit"; shift ;;
--section) RUN_MODE="audit"; SECTION_FILTER="${2:?--section requires names}"; shift 2 ;;
--remediate) RUN_MODE="remediate"; shift ;;
--export) RUN_MODE="export"; shift ;;
--format) OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
--output-file) OUTPUT_FILE="${2:?--output-file requires a path}"; shift 2 ;;
--verbose) VERBOSE="true"; shift ;;
--no-color) COLOR="never"; shift ;;
--help|-h) setup_colors; show_help; exit 0 ;;
*) die "Unknown option: $1 (see --help)" ;;
esac
done
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
setup_colors
if [[ -z "$RUN_MODE" ]]; then
err "No mode specified"
echo ""
show_help
exit 1
fi
START_TIME=$(date +%s)
echo ""
echo -e "${BOLD}Zero Trust Audit${RESET}"
echo "Host: $(hostname -f 2>/dev/null || hostname)"
echo "Mode: ${RUN_MODE}"
echo "Time: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
if [[ $EUID -ne 0 ]]; then
warn "Running without root — some checks may be incomplete"
fi
case "$RUN_MODE" in
audit) do_audit ;;
remediate) do_remediate ;;
export) do_export ;;
*) die "Unknown mode: ${RUN_MODE}" ;;
esac
}
main "$@"