#!/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 #### ############################################################# # ── 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 ${NC}" output_line "${YELLOW} (Replace 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