Files
linux-scripts/audit-log-analyzer.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

576 lines
20 KiB
Bash

#!/bin/bash
#############################################################
#### Audit Log Analyzer Script for SELinux and AppArmor ####
#### Parses denial logs and suggests fix commands ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.00 ####
#### ####
#### To use this script chmod it to 755 ####
#### or simply type bash <filename.sh> ####
#############################################################
# ── Colors ────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# ── Defaults ──────────────────────────────────────────────
MODE="recent"
OUTPUT_FILE=""
QUIET=0
TOTAL_DENIALS=0
UNIQUE_TYPES=0
SUGGESTED_FIXES=0
# ── Functions ─────────────────────────────────────────────
usage() {
echo -e "${BOLD}Audit Log Analyzer — SELinux & AppArmor${NC}"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --help Show this help message"
echo " --recent Analyze denials from the last hour only (default)"
echo " --all Analyze all denials in the log"
echo " --output FILE Save suggested fixes to FILE"
echo " --quiet Show suggestions only, suppress raw denial lines"
echo ""
echo "Examples:"
echo " sudo bash $0 --recent"
echo " sudo bash $0 --all --output fixes.txt"
echo " sudo bash $0 --quiet --output /tmp/fixes.txt"
exit 0
}
check_root() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Error: This script must be run as root.${NC}"
echo "Please run with: sudo bash $0"
exit 1
fi
}
detect_mac_system() {
SELINUX_ACTIVE=0
APPARMOR_ACTIVE=0
# Check SELinux
if command -v getenforce &>/dev/null; then
SELINUX_STATUS=$(getenforce 2>/dev/null)
if [[ "$SELINUX_STATUS" == "Enforcing" || "$SELINUX_STATUS" == "Permissive" ]]; then
SELINUX_ACTIVE=1
fi
fi
# Check AppArmor
if command -v aa-status &>/dev/null; then
if aa-status &>/dev/null; then
APPARMOR_ACTIVE=1
fi
elif [[ -d /sys/module/apparmor ]]; then
APPARMOR_ACTIVE=1
fi
if [[ $SELINUX_ACTIVE -eq 0 && $APPARMOR_ACTIVE -eq 0 ]]; then
echo -e "${YELLOW}Warning: Neither SELinux nor AppArmor appears to be active on this system.${NC}"
exit 1
fi
}
output_line() {
local line="$1"
echo -e "$line"
if [[ -n "$OUTPUT_FILE" ]]; then
echo -e "$line" | sed 's/\x1b\[[0-9;]*m//g' >> "$OUTPUT_FILE"
fi
}
# ── SELinux Analysis ──────────────────────────────────────
parse_selinux_denial() {
local line="$1"
local scontext tcontext tclass perm comm name path
scontext=$(echo "$line" | grep -oP 'scontext=\K[^ ]+')
tcontext=$(echo "$line" | grep -oP 'tcontext=\K[^ ]+')
tclass=$(echo "$line" | grep -oP 'tclass=\K[^ ]+')
perm=$(echo "$line" | grep -oP '\{ \K[^}]+')
comm=$(echo "$line" | grep -oP 'comm="\K[^"]+')
name=$(echo "$line" | grep -oP 'name="\K[^"]+')
path=$(echo "$line" | grep -oP 'path="\K[^"]+')
if [[ $QUIET -eq 0 ]]; then
output_line "${RED}DENIAL:${NC} $line"
fi
output_line "${CYAN} Source context:${NC} $scontext"
output_line "${CYAN} Target context:${NC} $tcontext"
output_line "${CYAN} Class:${NC} $tclass"
output_line "${CYAN} Permission:${NC} $perm"
[[ -n "$comm" ]] && output_line "${CYAN} Command:${NC} $comm"
[[ -n "$path" ]] && output_line "${CYAN} Path:${NC} $path"
suggest_selinux_fix "$scontext" "$tcontext" "$tclass" "$perm" "$path" "$name"
output_line ""
}
suggest_selinux_fix() {
local scontext="$1" tcontext="$2" tclass="$3" perm="$4" path="$5" name="$6"
((SUGGESTED_FIXES++))
# Port binding denials
if [[ "$tclass" == "tcp_socket" || "$tclass" == "udp_socket" ]]; then
local stype
stype=$(echo "$scontext" | cut -d: -f3)
output_line "${GREEN} Suggested fix (port rule):${NC}"
output_line "${GREEN} semanage port -a -t ${stype} -p tcp <PORT_NUMBER>${NC}"
output_line "${YELLOW} (Replace <PORT_NUMBER> with the actual port)${NC}"
return
fi
# File access denials
if [[ -n "$path" && ("$tclass" == "file" || "$tclass" == "dir" || "$tclass" == "lnk_file") ]]; then
local ttype
ttype=$(echo "$tcontext" | cut -d: -f3)
output_line "${GREEN} Suggested fix (file context):${NC}"
output_line "${GREEN} semanage fcontext -a -t ${ttype} \"${path}\"${NC}"
output_line "${GREEN} restorecon -Rv \"${path}\"${NC}"
# Also check for boolean solutions
suggest_selinux_boolean "$scontext" "$tcontext" "$tclass" "$perm"
return
fi
# General boolean suggestion
suggest_selinux_boolean "$scontext" "$tcontext" "$tclass" "$perm"
}
suggest_selinux_boolean() {
local scontext="$1" tcontext="$2" tclass="$3" perm="$4"
local stype
stype=$(echo "$scontext" | cut -d: -f3)
# Try to find relevant booleans
if command -v getsebool &>/dev/null; then
local booleans
booleans=$(getsebool -a 2>/dev/null | grep -i "${stype%%_t}" | head -5)
if [[ -n "$booleans" ]]; then
output_line "${GREEN} Possibly relevant booleans:${NC}"
while IFS= read -r bool_line; do
local bool_name
bool_name=$(echo "$bool_line" | cut -d' ' -f1)
output_line "${GREEN} setsebool -P ${bool_name} on${NC}"
done <<< "$booleans"
fi
fi
output_line "${YELLOW} If no boolean applies, consider generating a custom policy module (see below).${NC}"
}
categorize_selinux_denial() {
local line="$1"
local tclass
tclass=$(echo "$line" | grep -oP 'tclass=\K[^ ]+')
case "$tclass" in
file|dir|lnk_file|fifo_file|sock_file)
echo "file_access"
;;
tcp_socket|udp_socket|rawip_socket|netlink_socket)
echo "network"
;;
*_port_t)
echo "port_binding"
;;
process|process2)
echo "process"
;;
*)
echo "other"
;;
esac
}
analyze_selinux() {
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
output_line "${BOLD} SELinux Audit Log Analysis${NC}"
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
output_line ""
local selinux_status
selinux_status=$(getenforce 2>/dev/null)
output_line "${CYAN}SELinux status:${NC} $selinux_status"
output_line ""
# Gather denials
local denials=""
if [[ "$MODE" == "recent" ]]; then
if command -v ausearch &>/dev/null; then
denials=$(ausearch -m avc -ts recent 2>/dev/null | grep "type=AVC")
fi
# Fallback to log file
if [[ -z "$denials" && -f /var/log/audit/audit.log ]]; then
local one_hour_ago
one_hour_ago=$(date -d '1 hour ago' '+%s' 2>/dev/null)
if [[ -n "$one_hour_ago" ]]; then
denials=$(awk -v cutoff="$one_hour_ago" '
/type=AVC/ {
match($0, /msg=audit\(([0-9]+)\./, arr)
if (arr[1] >= cutoff) print
}
' /var/log/audit/audit.log)
fi
fi
else
if [[ -f /var/log/audit/audit.log ]]; then
denials=$(grep "type=AVC" /var/log/audit/audit.log)
fi
fi
if [[ -z "$denials" ]]; then
output_line "${GREEN}No AVC denials found.${NC}"
output_line ""
return
fi
# Group denials by category
declare -A categories
local denial_count=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
((denial_count++))
local category
category=$(categorize_selinux_denial "$line")
categories["$category"]+="$line"$'\n'
done <<< "$denials"
TOTAL_DENIALS=$denial_count
# Count unique types
local unique
unique=$(echo "$denials" | grep -oP 'tclass=\K[^ ]+' | sort -u | wc -l)
UNIQUE_TYPES=$unique
# Display grouped results
for category in "file_access" "network" "port_binding" "process" "other"; do
if [[ -n "${categories[$category]}" ]]; then
local label
case "$category" in
file_access) label="File Access Denials" ;;
network) label="Network Denials" ;;
port_binding) label="Port Binding Denials" ;;
process) label="Process Denials" ;;
other) label="Other Denials" ;;
esac
output_line "${BOLD}── ${label} ──────────────────────────────────${NC}"
output_line ""
while IFS= read -r denial_line; do
[[ -z "$denial_line" ]] && continue
parse_selinux_denial "$denial_line"
done <<< "${categories[$category]}"
fi
done
# Generate policy module suggestion with audit2allow
if command -v audit2allow &>/dev/null; then
output_line "${BOLD}── Policy Module Suggestion ──────────────────────${NC}"
output_line ""
local policy
if [[ "$MODE" == "recent" ]]; then
policy=$(ausearch -m avc -ts recent 2>/dev/null | audit2allow 2>/dev/null)
else
policy=$(audit2allow < /var/log/audit/audit.log 2>/dev/null)
fi
if [[ -n "$policy" ]]; then
output_line "${GREEN}audit2allow suggests the following policy:${NC}"
output_line "$policy"
output_line ""
output_line "${YELLOW}To create and install a custom module:${NC}"
output_line "${GREEN} ausearch -m avc -ts recent | audit2allow -M my_custom_policy${NC}"
output_line "${GREEN} semodule -i my_custom_policy.pp${NC}"
else
output_line "${CYAN}No policy suggestions generated by audit2allow.${NC}"
fi
output_line ""
else
output_line "${YELLOW}Note: Install audit2allow (policycoreutils-python-utils) for automatic policy generation.${NC}"
output_line ""
fi
}
# ── AppArmor Analysis ────────────────────────────────────
find_apparmor_log_source() {
if [[ -f /var/log/syslog ]]; then
echo "syslog"
elif [[ -f /var/log/kern.log ]]; then
echo "kern.log"
elif command -v journalctl &>/dev/null; then
echo "journalctl"
else
echo "none"
fi
}
parse_apparmor_denial() {
local line="$1"
local profile operation denied_mask path info
profile=$(echo "$line" | grep -oP 'profile="\K[^"]+')
[[ -z "$profile" ]] && profile=$(echo "$line" | grep -oP 'apparmor="\K[^"]+')
operation=$(echo "$line" | grep -oP 'operation="\K[^"]+')
denied_mask=$(echo "$line" | grep -oP 'requested_mask="\K[^"]+')
[[ -z "$denied_mask" ]] && denied_mask=$(echo "$line" | grep -oP 'denied_mask="\K[^"]+')
path=$(echo "$line" | grep -oP 'name="\K[^"]+')
info=$(echo "$line" | grep -oP 'info="\K[^"]+')
if [[ $QUIET -eq 0 ]]; then
output_line "${RED}DENIAL:${NC} $line"
fi
[[ -n "$profile" ]] && output_line "${CYAN} Profile:${NC} $profile"
[[ -n "$operation" ]] && output_line "${CYAN} Operation:${NC} $operation"
[[ -n "$path" ]] && output_line "${CYAN} Path:${NC} $path"
[[ -n "$denied_mask" ]] && output_line "${CYAN} Denied mask:${NC} $denied_mask"
[[ -n "$info" ]] && output_line "${CYAN} Info:${NC} $info"
suggest_apparmor_fix "$profile" "$operation" "$path" "$denied_mask"
output_line ""
}
suggest_apparmor_fix() {
local profile="$1" operation="$2" path="$3" denied_mask="$4"
((SUGGESTED_FIXES++))
# Build the permission string from the denied mask
local perm_str=""
case "$denied_mask" in
r) perm_str="r" ;;
w) perm_str="w" ;;
rw) perm_str="rw" ;;
x) perm_str="ix" ;;
rx) perm_str="rix" ;;
rwx) perm_str="rwix" ;;
k) perm_str="k" ;;
l) perm_str="l" ;;
m) perm_str="m" ;;
*) perm_str="$denied_mask" ;;
esac
if [[ -n "$path" && -n "$perm_str" ]]; then
output_line "${GREEN} Suggested rule to add to profile:${NC}"
output_line "${GREEN} ${path} ${perm_str},${NC}"
fi
# Show the profile file path
if [[ -n "$profile" ]]; then
local profile_file="/etc/apparmor.d/${profile//\//.}"
# Try to find the actual profile file
if [[ -f "/etc/apparmor.d/$profile" ]]; then
profile_file="/etc/apparmor.d/$profile"
elif [[ -f "/etc/apparmor.d/${profile//\//.}" ]]; then
profile_file="/etc/apparmor.d/${profile//\//.}"
else
# Search for it
local found
found=$(grep -rl "profile $profile" /etc/apparmor.d/ 2>/dev/null | head -1)
[[ -n "$found" ]] && profile_file="$found"
fi
output_line "${CYAN} Profile file:${NC} $profile_file"
fi
output_line "${YELLOW} Or run interactively:${NC}"
output_line "${GREEN} aa-logprof${NC}"
}
analyze_apparmor() {
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
output_line "${BOLD} AppArmor Audit Log Analysis${NC}"
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
output_line ""
# Show AppArmor status
if command -v aa-status &>/dev/null; then
local enforced loaded
enforced=$(aa-status 2>/dev/null | grep -c "enforce")
loaded=$(aa-status 2>/dev/null | grep -c "loaded")
output_line "${CYAN}AppArmor profiles loaded:${NC} $loaded"
output_line "${CYAN}Profiles in enforce mode:${NC} $enforced"
output_line ""
fi
# Find log source
local log_source
log_source=$(find_apparmor_log_source)
if [[ "$log_source" == "none" ]]; then
output_line "${RED}Error: Cannot find AppArmor log source.${NC}"
output_line "${YELLOW}Checked: /var/log/syslog, /var/log/kern.log, journalctl${NC}"
return
fi
# Gather denials
local denials=""
if [[ "$log_source" == "journalctl" ]]; then
if [[ "$MODE" == "recent" ]]; then
denials=$(journalctl --since "1 hour ago" --no-pager 2>/dev/null | grep -i "apparmor.*DENIED")
else
denials=$(journalctl --no-pager 2>/dev/null | grep -i "apparmor.*DENIED")
fi
else
local log_file
[[ "$log_source" == "syslog" ]] && log_file="/var/log/syslog"
[[ "$log_source" == "kern.log" ]] && log_file="/var/log/kern.log"
if [[ "$MODE" == "recent" ]]; then
local one_hour_ago
one_hour_ago=$(date -d '1 hour ago' '+%b %e %H:%M' 2>/dev/null)
if [[ -n "$one_hour_ago" ]]; then
denials=$(awk -v cutoff="$(date -d '1 hour ago' '+%s' 2>/dev/null)" '
/apparmor.*DENIED/ || /apparmor.*denied/ {
print
}
' "$log_file" | tail -100)
else
# Fallback: last 100 denial lines
denials=$(grep -i "apparmor.*DENIED" "$log_file" | tail -100)
fi
else
denials=$(grep -i "apparmor.*DENIED" "$log_file")
fi
fi
if [[ -z "$denials" ]]; then
output_line "${GREEN}No AppArmor denials found.${NC}"
output_line ""
return
fi
local denial_count=0
local -A seen_profiles
output_line "${BOLD}── AppArmor Denials ─────────────────────────────${NC}"
output_line ""
while IFS= read -r line; do
[[ -z "$line" ]] && continue
((denial_count++))
parse_apparmor_denial "$line"
local p
p=$(echo "$line" | grep -oP 'profile="\K[^"]+')
[[ -n "$p" ]] && seen_profiles["$p"]=1
done <<< "$denials"
TOTAL_DENIALS=$denial_count
UNIQUE_TYPES=${#seen_profiles[@]}
# Suggest aa-logprof for interactive fixing
output_line "${BOLD}── Interactive Fix Suggestion ────────────────────${NC}"
output_line ""
output_line "${YELLOW}For interactive profile updates, run:${NC}"
output_line "${GREEN} aa-logprof${NC}"
output_line ""
output_line "${YELLOW}To set a profile to complain mode for testing:${NC}"
for prof in "${!seen_profiles[@]}"; do
output_line "${GREEN} aa-complain $prof${NC}"
done
output_line ""
}
# ── Summary ───────────────────────────────────────────────
print_summary() {
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
output_line "${BOLD} Summary${NC}"
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
output_line ""
output_line " Total denials found: ${BOLD}${TOTAL_DENIALS}${NC}"
output_line " Unique denial types: ${BOLD}${UNIQUE_TYPES}${NC}"
output_line " Suggested fixes: ${BOLD}${SUGGESTED_FIXES}${NC}"
output_line ""
if [[ -n "$OUTPUT_FILE" ]]; then
output_line "${GREEN}Suggestions saved to: ${OUTPUT_FILE}${NC}"
output_line ""
fi
}
# ── Parse Arguments ───────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
usage
;;
--recent)
MODE="recent"
shift
;;
--all)
MODE="all"
shift
;;
--output)
if [[ -z "$2" || "$2" == --* ]]; then
echo -e "${RED}Error: --output requires a filename argument.${NC}"
exit 1
fi
OUTPUT_FILE="$2"
shift 2
;;
--quiet|-q)
QUIET=1
shift
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
echo "Use --help for usage information."
exit 1
;;
esac
done
# ── Main ──────────────────────────────────────────────────
check_root
# Clear output file if specified
if [[ -n "$OUTPUT_FILE" ]]; then
true > "$OUTPUT_FILE"
fi
echo -e "${BOLD}Audit Log Analyzer v1.00${NC}"
echo -e "${CYAN}Mode: ${MODE}${NC}"
echo ""
detect_mac_system
if [[ $SELINUX_ACTIVE -eq 1 ]]; then
analyze_selinux
fi
if [[ $APPARMOR_ACTIVE -eq 1 ]]; then
analyze_apparmor
fi
print_summary