#!/usr/bin/bash
# Refresh-only, and every path exits 0: this runs from %posttrans.

policy_name="${1:-imunify360}"

te_primary_dir="${SELINUX_TE_PRIMARY_DIR:-/opt/imunify360/venv/share/imunify360}"
te_fallback_dir="${SELINUX_TE_FALLBACK_DIR:-/usr/share/imunify360}"
hll_pp="${SELINUX_HLL_PP:-/usr/libexec/selinux/hll/pp}"

note() { printf '[selinux-policy] %s\n' "$1"; }
skip() { note "$1"; exit 0; }
bail() { printf '[selinux-policy] %s\n' "$1" >&2; exit 0; }

case "$policy_name" in
    "" | *[!a-z0-9_-]*)
        bail "refusing an unexpected policy name: $policy_name"
        ;;
esac

# 0 loaded, 1 not loaded, 2 the store could not be listed at all.
policy_is_loaded()
{
    local listing module _rest
    if ! listing="$(semodule -l 2>/dev/null)"; then
        return 2
    fi
    while read -r module _rest; do
        if [ "$module" = "$policy_name" ]; then
            return 0
        fi
    done <<< "$listing"
    return 1
}

# CIL expressions may span lines; pipefail so a dead converter is not "short".
rules_of()
{
    (
    set -o pipefail
    case "$1" in
        *.cil) cat -- "$1" 2>/dev/null ;;
        *)     "$hll_pp" "$1" 2>/dev/null ;;
    esac | awk '
        /^[[:space:]]*(;|$)/ { next }
        /^[[:space:]]/ { sub(/^[[:space:]]+/, " "); stmt = stmt $0; next }
        { if (stmt != "") print stmt; stmt = $0 }
        END { if (stmt != "") print stmt }
    ' | sort
    )
}

extract_loaded_rules()
{
    rm -rf "$extract_dir" && mkdir -p "$extract_dir" || return 1
    ( cd "$extract_dir" && semodule -E "$policy_name" >/dev/null 2>&1 ) || return 1
    # semodule -E names the file after the install language, not always .pp.
    local extracted
    for extracted in "$extract_dir/${policy_name}".*; do
        [ -f "$extracted" ] || continue
        rules_of "$extracted" > "$1" || continue
        [ -s "$1" ] && return 0
    done
    return 1
}

command -v semodule >/dev/null 2>&1 \
    || skip "semodule not found; skipping"
command -v getenforce >/dev/null 2>&1 \
    || skip "getenforce not found; skipping"

# A failed probe is not an answer, and the message is the only signal left.
if ! enforce_state="$(getenforce 2>/dev/null)"; then
    bail "getenforce failed; cannot tell whether SELinux is enabled"
fi
case "$enforce_state" in
    "" | Disabled)
        skip "SELinux is disabled; skipping"
        ;;
esac

policy_is_loaded
case "$?" in
    0) ;;
    2) bail "semodule -l failed; cannot tell whether $policy_name is loaded" ;;
    *) skip "$policy_name is not loaded; leaving the first load to the deploy script" ;;
esac

te_path=""
for candidate in \
    "${te_primary_dir}/${policy_name}.te" \
    "${te_fallback_dir}/${policy_name}.te"; do
    if [ -f "$candidate" ]; then
        te_path="$candidate"
        break
    fi
done
[ -n "$te_path" ] || skip "${policy_name}.te not found; skipping"

command -v checkmodule >/dev/null 2>&1 \
    || skip "checkmodule not found; skipping"
command -v semodule_package >/dev/null 2>&1 \
    || skip "semodule_package not found; skipping"
# Absent on the pre-CIL store, where the loaded module cannot be read back.
[ -x "$hll_pp" ] \
    || skip "$hll_pp not found; skipping"

workdir="$(mktemp -d -t imunify-selinux-XXXXXX)"
[ -d "$workdir" ] || bail "failed to create a temp directory"
trap 'rm -rf "$workdir"' EXIT

extract_dir="$workdir/loaded"
# checkmodule and semodule both want the base name to match the module name.
mod_path="$workdir/${policy_name}.mod"
shipped_pp="$workdir/${policy_name}.pp"

checkmodule -M -m -o "$mod_path" "$te_path" >/dev/null \
    || bail "checkmodule failed for $te_path"
semodule_package -o "$shipped_pp" -m "$mod_path" >/dev/null \
    || bail "semodule_package failed"

rules_of "$shipped_pp" > "$workdir/shipped.rules" \
    || bail "could not read the rules of $te_path"
[ -s "$workdir/shipped.rules" ] || bail "could not read the rules of $te_path"

extract_loaded_rules "$workdir/loaded.rules" \
    || bail "could not read the loaded $policy_name module"

if cmp -s "$workdir/shipped.rules" "$workdir/loaded.rules"; then
    skip "$policy_name policy already matches the loaded module"
fi

note "$policy_name policy differs from the loaded module ($(
    comm -23 "$workdir/shipped.rules" "$workdir/loaded.rules" | wc -l
) shipped rule(s) missing, $(
    comm -13 "$workdir/shipped.rules" "$workdir/loaded.rules" | wc -l
) stale); reloading"

semodule -i "$shipped_pp" || bail "semodule -i failed"

# Confirm the reload landed instead of trusting semodule's exit status.
if extract_loaded_rules "$workdir/after.rules" \
    && cmp -s "$workdir/shipped.rules" "$workdir/after.rules"; then
    note "$policy_name SELinux policy reloaded"
else
    bail "$policy_name was reloaded but still differs from $te_path"
fi
