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

# SELinux Policy Configuration Helper for CentOS 8
#
# Purpose:
#   - Check SELinux status and required tools
#   - Review recent AVC denials
#   - Inspect file and process labels
#   - Suggest the least invasive next step:
#       * fix file contexts
#       * adjust a boolean
#       * create a custom policy module (manual review required)
#
# Safe by design:
#   - No destructive actions
#   - No policy changes are applied automatically
#   - No credentials or environment-specific endpoints are required
#
# Usage examples:
#   ./selinux-policy-helper.sh
#   ./selinux-policy-helper.sh --path /srv/yourservice --process yourservice --boolean httpd_can_network_connect
#   ./selinux-policy-helper.sh --show-audit
#   ./selinux-policy-helper.sh --relabel-command /srv/yourservice

SERVICE_PATH=""
SERVICE_NAME=""
BOOLEAN_NAME=""
SHOW_AUDIT=0
DRY_RUN=0
RELBLABEL_PATH=""

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

Options:
  --path PATH         Path to inspect labels for (example: /srv/yourservice)
  --process NAME      Process name to search in ps -eZ output
  --boolean NAME      SELinux boolean to inspect (example: httpd_can_network_connect)
  --show-audit        Show recent AVC/USER_AVC denials and sealert summary
  --relabel-command PATH
                      Print a safe semanage/restorecon command template for PATH
  --dry-run           Print actions without running extra diagnostics beyond checks
  -h, --help          Show this help

Notes:
  - This script does not change SELinux policy automatically.
  - Review any suggested commands before running them.
EOF
}

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

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

run_cmd() {
  if [[ "$DRY_RUN" -eq 1 ]]; then
    printf '[dry-run] %q' "$1"
    shift
    for arg in "$@"; do
      printf ' %q' "$arg"
    done
    printf '\n'
  else
    "$@"
  fi
}

check_tool() {
  local tool="$1"
  if ! command -v "$tool" >/dev/null 2>&1; then
    warn "Missing required tool: $tool"
    return 1
  fi
}

show_selinux_status() {
  log "== SELinux status =="
  if command -v getenforce >/dev/null 2>&1; then
    log "getenforce: $(getenforce)"
  else
    warn "getenforce not found"
  fi

  if command -v sestatus >/dev/null 2>&1; then
    sestatus | sed 's/^/  /'
  else
    warn "sestatus not found"
  fi
}

show_package_check() {
  log "== SELinux utilities =="
  if command -v rpm >/dev/null 2>&1; then
    rpm -q policycoreutils policycoreutils-python-utils setools-console || true
  else
    warn "rpm not found; skipping package check"
  fi
}

show_audit_traces() {
  log "== Recent AVC denials =="
  if command -v ausearch >/dev/null 2>&1; then
    ausearch -m AVC,USER_AVC -ts recent || log "No recent AVC entries found or access to audit log is restricted."
  else
    warn "ausearch not found"
  fi

  log "== sealert summary =="
  if command -v sealert >/dev/null 2>&1 && [[ -r /var/log/audit/audit.log ]]; then
    sealert -a /var/log/audit/audit.log || true
  else
    warn "sealert not available or audit log unreadable"
  fi
}

show_labels() {
  local path="$1"
  log "== File labels for: $path =="
  if [[ -e "$path" ]]; then
    ls -Zd "$path"
    if [[ -d "$path" ]]; then
      ls -lZ "$path" | head -n 20
    fi
    log "== matchpathcon =="
    if command -v matchpathcon >/dev/null 2>&1; then
      matchpathcon "$path" || true
    fi
  else
    warn "Path does not exist: $path"
  fi
}

show_process_labels() {
  local name="$1"
  log "== Process labels matching: $name =="
  if command -v ps >/dev/null 2>&1; then
    ps -eZ | grep -i -- "$name" || log "No matching process found"
  else
    warn "ps not found"
  fi
}

show_boolean() {
  local boolean_name="$1"
  log "== Boolean: $boolean_name =="
  if command -v getsebool >/dev/null 2>&1; then
    getsebool "$boolean_name" || warn "Boolean not found or unavailable: $boolean_name"
  else
    warn "getsebool not found"
  fi
}

suggest_relabel_command() {
  local path="$1"
  cat <<EOF
== Relabel command template ==
# Review and adjust the SELinux type before using this command:
#   semanage fcontext -a -t <correct_type> '${path}(/.*)?'
# Then apply labels:
#   restorecon -Rv '${path}'
EOF
}

recommend_next_steps() {
  log "== Recommended next step =="
  cat <<'EOF'
1. If the path label is wrong, define a persistent fcontext rule and relabel the path.
2. If the denial matches a documented SELinux boolean, enable only that boolean.
3. If neither fits, collect the exact AVC denial and consider a narrowly scoped custom policy module.
4. Re-test the service under enforcing mode and confirm no new AVC denials appear.
EOF
}

main() {
  if [[ $# -eq 0 ]]; then
    :
  fi

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --path)
        SERVICE_PATH="${2:-}"
        shift 2
        ;;
      --process)
        SERVICE_NAME="${2:-}"
        shift 2
        ;;
      --boolean)
        BOOLEAN_NAME="${2:-}"
        shift 2
        ;;
      --show-audit)
        SHOW_AUDIT=1
        shift
        ;;
      --dry-run)
        DRY_RUN=1
        shift
        ;;
      --relabel-command)
        RELBLABEL_PATH="${2:-}"
        shift 2
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        warn "Unknown argument: $1"
        usage
        exit 1
        ;;
    esac
  done

  show_selinux_status
  echo
  show_package_check
  echo

  if [[ "$SHOW_AUDIT" -eq 1 ]]; then
    show_audit_traces
    echo
  fi

  if [[ -n "$SERVICE_PATH" ]]; then
    show_labels "$SERVICE_PATH"
    echo
  fi

  if [[ -n "$SERVICE_NAME" ]]; then
    show_process_labels "$SERVICE_NAME"
    echo
  fi

  if [[ -n "$BOOLEAN_NAME" ]]; then
    show_boolean "$BOOLEAN_NAME"
    echo
  fi

  if [[ -n "$RELBLABEL_PATH" ]]; then
    suggest_relabel_command "$RELBLABEL_PATH"
    echo
  fi

  recommend_next_steps
}

main "$@"