#!/usr/bin/env bash
set -euo pipefail

# selinux-triage.sh
#
# Purpose:
#   Collect and summarize common SELinux denial evidence on Red Hat Enterprise Linux.
#   This script is read-only by default and does not change policy, labels, or booleans.
#
# What it checks:
#   - SELinux status
#   - Recent AVC denials from audit logs or journal
#   - File/dir labels for a provided path
#   - SELinux booleans relevant to a provided service name
#
# Usage examples:
#   ./selinux-triage.sh
#   ./selinux-triage.sh --path /var/www/html
#   ./selinux-triage.sh --service httpd
#   ./selinux-triage.sh --path /srv/app --service myservice --since "1 hour ago"
#
# Notes:
#   - If auditd is not available, the script falls back to journalctl where possible.
#   - This script intentionally avoids semanage, setenforce, restorecon, chcon, and policy changes.

PATH_TO_CHECK=""
SERVICE_NAME=""
SINCE_TIME="24 hours ago"
MAX_LINES=20

usage() {
  cat <<'EOF'
Usage: selinux-triage.sh [options]

Options:
  --path PATH         Check SELinux label for a file or directory path
  --service NAME      Show SELinux booleans related to a service name
  --since TIME        Time window for recent AVC messages (default: "24 hours ago")
  --max-lines N       Limit displayed denial lines (default: 20)
  -h, --help          Show this help message

Examples:
  selinux-triage.sh --path /var/www/html
  selinux-triage.sh --service httpd
  selinux-triage.sh --path /srv/app --service myservice --since "2 hours ago"
EOF
}

log() {
  printf '%s\n' "$*"
}

warn() {
  printf 'WARN: %s\n' "$*" >&2
}

have_cmd() {
  command -v "$1" >/dev/null 2>&1
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --path)
      [[ $# -ge 2 ]] || { warn "--path requires a value"; usage; exit 1; }
      PATH_TO_CHECK="$2"
      shift 2
      ;;
    --service)
      [[ $# -ge 2 ]] || { warn "--service requires a value"; usage; exit 1; }
      SERVICE_NAME="$2"
      shift 2
      ;;
    --since)
      [[ $# -ge 2 ]] || { warn "--since requires a value"; usage; exit 1; }
      SINCE_TIME="$2"
      shift 2
      ;;
    --max-lines)
      [[ $# -ge 2 ]] || { warn "--max-lines requires a value"; usage; exit 1; }
      MAX_LINES="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      warn "Unknown option: $1"
      usage
      exit 1
      ;;
  esac
done

if ! [[ "$MAX_LINES" =~ ^[0-9]+$ ]] || [[ "$MAX_LINES" -le 0 ]]; then
  warn "--max-lines must be a positive integer"
  exit 1
fi

log "== SELinux triage =="

if have_cmd getenforce; then
  log "SELinux mode: $(getenforce)"
else
  warn "getenforce not found; cannot directly query SELinux mode"
fi

if have_cmd sestatus; then
  log ""
  log "-- SELinux status --"
  sestatus || warn "sestatus returned a non-zero exit status"
fi

log ""
log "-- Recent AVC denials --"
FOUND_DENIALS=0

if have_cmd ausearch; then
  # auditd may not be enabled on every system; suppress failures gracefully.
  if AVC_OUTPUT=$(ausearch -m avc -ts "$SINCE_TIME" 2>/dev/null | tail -n "$MAX_LINES"); then
    if [[ -n "$AVC_OUTPUT" ]]; then
      printf '%s\n' "$AVC_OUTPUT"
      FOUND_DENIALS=1
    fi
  fi
fi

if [[ "$FOUND_DENIALS" -eq 0 ]] && have_cmd journalctl; then
  if JOURNAL_OUTPUT=$(journalctl --since "$SINCE_TIME" 2>/dev/null | grep -Ei 'avc:|selinux' | tail -n "$MAX_LINES" || true); then
    if [[ -n "$JOURNAL_OUTPUT" ]]; then
      printf '%s\n' "$JOURNAL_OUTPUT"
      FOUND_DENIALS=1
    fi
  fi
fi

if [[ "$FOUND_DENIALS" -eq 0 ]]; then
  log "No AVC denial messages found in the selected time window, or required tools are unavailable."
fi

if [[ -n "$PATH_TO_CHECK" ]]; then
  log ""
  log "-- Label check for path: $PATH_TO_CHECK --"
  if [[ -e "$PATH_TO_CHECK" ]]; then
    if have_cmd ls; then
      ls -Zd -- "$PATH_TO_CHECK" || warn "Could not read label for $PATH_TO_CHECK"
    else
      warn "ls not found; cannot display file context"
    fi
  else
    warn "Path does not exist: $PATH_TO_CHECK"
  fi
fi

if [[ -n "$SERVICE_NAME" ]]; then
  log ""
  log "-- Relevant booleans for service: $SERVICE_NAME --"
  if have_cmd getsebool; then
    # Best-effort grep by service name; output may vary by distribution and installed policy packages.
    getsebool -a 2>/dev/null | grep -i -- "$SERVICE_NAME" || log "No matching booleans found for '$SERVICE_NAME'"
  else
    warn "getsebool not found; cannot list SELinux booleans"
  fi
fi

log ""
log "-- Next steps --"
cat <<'EOF'
1. Match the denial source domain, target label, and denied action.
2. Check whether the blocked path, port, or transition matches the service design.
3. Prefer fixing labels or service placement over weakening policy.
4. If the behavior is legitimate and stable, evaluate a narrow boolean or tightly scoped policy exception.
5. Verify the service again while SELinux remains enforcing.
EOF

exit 0