#!/bin/bash
PKG_INTEGRITY_VERSION="1.0.0"
# imunify-check-pkg-integrity: verify imunify* package file integrity
# via rpm -V / dpkg -V. Outputs results to stdout.
#
# Deployed to (both RPM and DEB, spec overrides _prefix):
#   /opt/imunify360/venv/share/imunify360/scripts/imunify-check-pkg-integrity
# Package: imunify-core
#
# This script is also sourced by imunify-force-update.sh for shared functions
# (filter_verify_noise, filter_whitelisted_files, INTEGRITY_WHITELIST_FILES, etc.).
# set -euo pipefail is only applied when run directly, not when sourced.

DEBIAN_VERSION_FILE="${DEBIAN_VERSION_FILE:-/etc/debian_version}"

# Files always reported as modified on a healthy system.
# On RPM most of these are %config and already filtered by filter_verify_noise;
# on Debian they are NOT marked as conffiles, so we whitelist explicitly.
# Uses substring matching (grep -v -F).
INTEGRITY_WHITELIST_FILES=(
    "imunify360.config.defaults.example"
    # imunify-ui-*-cpanel: triggerin runs sed to patch IMUNIFY_PACKAGE in config.js
    "imunify/assets/js/config.js"
    # imunify360-php-i360: postinst copies .ini/.so per PHP version from templates
    "/i360.ini"
    "/i360.so"
    # i360-php-opts: module.ini modified during config (conffile on RPM, not on DEB)
    "i360-php-opts/module.ini"
    # imunify360-ossec-server: postinst sed-patches analysisd.stats_maxdiff
    "internal_options.conf"
    # imunify360-webshield-bundle: service updates these at runtime (conffile on RPM, not on DEB)
    "imunify360-webshield/common-proxies.conf"
    "imunify360-webshield/country_ips.conf"
    "imunify360-webshield/whitelisted-domains.conf"
)

detect_pkg_manager() {
    if [ -f "$DEBIAN_VERSION_FILE" ]; then
        echo "dpkg"
    elif command -v rpm >/dev/null 2>&1; then
        echo "rpm"
    else
        echo "unknown"
    fi
}

check_dpkg_verify_supported() {
    # dpkg treats -V and --help as conflicting actions, so verify a known package instead
    dpkg -V dpkg >/dev/null 2>&1
}

# Filter rpm -V / dpkg -V output to keep only lines indicating real corruption.
#
# Both tools use a 9-char flag string + optional type flag + path.
# rpm: SM5DLUGTP  [c|d|g|l|r| ] /path    dpkg: ??5??????  [c| ] /path
# rpm also emits: missing     /path
#
# Excluded:
#   - config files (type 'c') and ghost files (type 'g')
#   - .pyc files (Python bytecode cache, regenerated at import)
#   - metadata-only changes (no S or 5 flag - just mode/group/time drift)
# Kept:
#   - 'missing' lines for non-pyc files
#   - lines with S (size, pos 1) or 5 (digest, pos 3) change
filter_verify_noise() {
    grep -v -E '^.{9} +[cg] ' \
        | grep -v '\.pyc$' \
        | grep -E '^(S|..5|[?][?]5|missing)' \
        || true
}

filter_whitelisted_files() {
    local input
    input=$(cat)
    if [[ ${#INTEGRITY_WHITELIST_FILES[@]} -eq 0 ]]; then
        echo "$input"
        return
    fi
    local pattern=""
    for entry in "${INTEGRITY_WHITELIST_FILES[@]}"; do
        if [[ -n "$pattern" ]]; then
            pattern+=$'\n'
        fi
        pattern+="$entry"
    done
    echo "$input" | grep -v -F "$pattern" || true
}

list_packages_rpm() {
    rpm -qa 'imunify*' 2>/dev/null | sort
}

list_packages_dpkg() {
    dpkg-query -W -f '${Status} ${Package}\n' 'imunify*' 2>/dev/null \
        | sed -n 's/.* installed \(.*\)/\1/p' \
        | sort
}

verify_package_rpm() {
    local pkg="$1"
    local raw_output=""
    raw_output=$(rpm -V "$pkg" 2>&1) || raw_output="${raw_output}"
    local filtered=""
    filtered=$(echo "$raw_output" | filter_verify_noise | filter_whitelisted_files)
    if [[ -n "$filtered" ]]; then
        echo "$filtered"
        return 1
    fi
    return 0
}

verify_package_dpkg() {
    local pkg="$1"
    local raw_output=""
    raw_output=$(dpkg -V "$pkg" 2>&1) || raw_output="${raw_output}"
    local filtered=""
    filtered=$(echo "$raw_output" | filter_verify_noise | filter_whitelisted_files)
    if [[ -n "$filtered" ]]; then
        echo "$filtered"
        return 1
    fi
    return 0
}

check_packages() {
    local pkg_manager
    pkg_manager=$(detect_pkg_manager)

    local verify_supported="yes"
    local overall_status="ok"
    local broken_output=""

    local packages=""
    case "$pkg_manager" in
        rpm)
            packages=$(list_packages_rpm)
            ;;
        dpkg)
            packages=$(list_packages_dpkg)
            if ! check_dpkg_verify_supported; then
                verify_supported="no"
            fi
            ;;
        *)
            verify_supported="no"
            ;;
    esac

    if [[ "$verify_supported" == "yes" ]] && [[ -n "$packages" ]]; then
        while IFS= read -r pkg; do
            [[ -z "$pkg" ]] && continue
            local output=""
            local rc=0
            case "$pkg_manager" in
                rpm)
                    output=$(verify_package_rpm "$pkg") || rc=$?
                    ;;
                dpkg)
                    output=$(verify_package_dpkg "$pkg") || rc=$?
                    ;;
            esac
            if [[ $rc -ne 0 ]]; then
                overall_status="broken"
                broken_output+=$'\n'"BROKEN: ${pkg}"$'\n'"${output}"$'\n'
            fi
        done <<< "$packages"
    fi

    echo "PKG_MANAGER: ${pkg_manager}"
    echo "VERIFY_SUPPORTED: ${verify_supported}"
    echo "STATUS: ${overall_status}"
    if [[ -n "$broken_output" ]]; then
        echo "$broken_output"
    fi
}

main_entrypoint() {
    local output
    output=$(check_packages)
    echo "$output"
    if echo "$output" | grep -q "^STATUS: broken"; then
        return 1
    fi
    return 0
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    set -euo pipefail
    main_entrypoint
fi
