#!/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
