#!/bin/bash
#
# This script exit codes
# 0 means OK
# 1 some error or csf is not installed
#
# Paths are overridable for tests only; production callers rely on the
# defaults.
CSF_ALLOW_FILE="${CSF_ALLOW_FILE:-/etc/csf/csf.allow}"
IMUNIFY_ALLOW_CONF="${IMUNIFY_ALLOW_CONF:-/etc/csf/imunify_allow.conf}"

function is_csf_installed {
  if which csf >/dev/null 2>&1; then
    echo "csf is installed"
    return 0
  else
    echo "csf is not installed"
    return 1
  fi
}

function enable_csf {
  systemctl start csf
  systemctl start lfd
  csf --enable # add csf to autostart
}

function disable_csf {
  systemctl stop csf
  systemctl stop lfd
  csf --disable # remove csf from autostart
}

function purge_allow_entries {
    # $1 conf (ip;comment source), $2 target csf.allow-format file.
    # Entries are matched by IP and by comment: comments stay stable
    # across package versions while IPs rotate between builds, so
    # matching by comment also drops entries an older package wrote
    # (DEB upgrades never run "remove", only "add").
    local conf=$1 target=$2 instance ip comment esc ctime
    mapfile -t ip_comment < "$conf"
    for instance in "${ip_comment[@]}"
        do
            IFS=';' read -r ip comment <<< "$instance"
            [ -n "$ip" ] || continue
            # word boundaries keep 203.0.113.1 from also purging a
            # customer's 203.0.113.10 line
            esc=$(printf '%s' "$ip" | sed 's/[][\.*^$/]/\\&/g')
            sed -i "/\b${esc}\b/d" "$target"
            # legacy "csf --add" bare lines are ours only when the
            # comment ends with csf's full ctime stamp (e.g.
            # "... # imunify360 server - Wed Mar 27 03:33:24 2024"),
            # so a customer row that merely reuses the comment text is
            # never touched
            if [ -n "$comment" ]; then
                esc=$(printf '%s' "$comment" | sed 's/[][\.*^$/]/\\&/g')
                ctime='[A-Za-z]\{3\} [A-Za-z]\{3\} [ 0-9][0-9]'
                ctime="${ctime} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\} [0-9]\{4\}"
                sed -i "/ # ${esc} - ${ctime}\$/d" "$target"
            fi
        done
    # our advanced filters carry the "# csf_tool: " ownership marker, so
    # they purge by marker alone even after their IPs rotate out; a
    # customer rule in the same shape but without the marker survives
    sed -i "/^tcp|out|d=443|d=.* # csf_tool: /d" "$target"
    sed -i "/^# csf_tool/d" "$target"
}

function edit_allow_list {
    FILE=$1
    mapfile -t ip_comment < $FILE
    if [ "$2" = "remove" ] ; then
        purge_allow_entries "$FILE" "$CSF_ALLOW_FILE"
        # remove imunify360 whitelist file
        sed -i "/imunify360.txt/d" "$CSF_ALLOW_FILE"
        remove_bins_from_pignore
    fi
    if [ "$2" = "add" ] ; then
        add_bins_to_pignore
        local tmp instance ip comment has_entries=false
        for instance in "${ip_comment[@]}"
            do
                IFS=';' read -r ip comment <<< "$instance"
                if [ -n "$ip" ]; then
                    has_entries=true
                    break
                fi
            done
        # an empty or unreadable conf must not reach the rewrite below:
        # it would purge every marked entry, re-add nothing, and still
        # report success
        if [ "$has_entries" != "true" ]; then
            echo "warning: no allowlist entries in ${FILE};" \
                 "leaving ${CSF_ALLOW_FILE} unchanged" >&2
            return 0
        fi
        # Build the result in a temp file in the same directory, verify
        # it, then atomically rename it over csf.allow. A rename can't
        # half-write, so any earlier failure leaves the live file intact.
        tmp=$(mktemp "${CSF_ALLOW_FILE}.XXXXXX") || {
            echo "error: failed to update ${CSF_ALLOW_FILE}" >&2
            return 1
        }
        if [ -f "$CSF_ALLOW_FILE" ]; then
            if ! cp "$CSF_ALLOW_FILE" "$tmp"; then
                rm -f "$tmp"
                echo "error: failed to update ${CSF_ALLOW_FILE}" >&2
                return 1
            fi
            # carry over csf.allow's ownership and mode to the replacement
            chown --reference="$CSF_ALLOW_FILE" "$tmp" 2>/dev/null || :
            chmod --reference="$CSF_ALLOW_FILE" "$tmp" 2>/dev/null || :
        fi
        purge_allow_entries "$FILE" "$tmp"
        printf '\n# csf_tool: \n' >> "$tmp"
        for instance in "${ip_comment[@]}"
            do
                IFS=';' read -r ip comment <<< "$instance"
                [ -n "$ip" ] || continue
                echo "tcp|out|d=443|d=${ip} # csf_tool: ${comment}" >> "$tmp"
            done
        for instance in "${ip_comment[@]}"
            do
                IFS=';' read -r ip comment <<< "$instance"
                [ -n "$ip" ] || continue
                if ! grep -qF "tcp|out|d=443|d=${ip} # csf_tool: ${comment}" "$tmp"
                then
                    rm -f "$tmp"
                    echo "error: failed to build ${CSF_ALLOW_FILE}" >&2
                    return 1
                fi
            done
        if ! mv -f "$tmp" "$CSF_ALLOW_FILE"; then
            rm -f "$tmp"
            echo "error: failed to update ${CSF_ALLOW_FILE}" >&2
            return 1
        fi
        # entries go into the file directly (csf --add cannot take
        # advanced port filters), so csf must reload to apply them; a
        # reload failure is reported but must not abort package installs
        # on hosts whose csf config was already broken
        if ! csf -r >/dev/null 2>&1; then
            echo "warning: csf reload failed;" \
                 "run 'csf -r' manually to apply the allowlist" >&2
        fi
    fi
}
BINS_TO_IGNORE="exe:/var/ossec/bin/ossec-monitord \
                exe:/var/ossec/bin/ossec-analysisd\
                exe:/var/ossec/bin/ossec-remoted"
ignore_file="${CSF_PIGNORE_FILE:-/etc/csf/csf.pignore}"

function remove_bins_from_pignore {
  for exe in $BINS_TO_IGNORE; do
    sed -i "\|${exe}|d" $ignore_file
  done
}

function add_bins_to_pignore {

    should_restart=false

    for exe in $BINS_TO_IGNORE; do
        if ! grep -q $exe $ignore_file ; then
            echo "$exe" >> $ignore_file
            should_restart=true
        fi
    done

    if $should_restart; then
        echo 'Restarting CSF to apply pignore changes'
        csf --restartall > /dev/null
    fi
}

if [ "$1" = "" ] ; then
    echo "Usage: $0 enable/disable/status/coop_install"
    exit 1
fi
if [ "$1" = "status" ] ; then
    is_csf_installed
    exit $?
fi
if [ "$1" = "enable" ]
then
    is_csf_installed >/dev/null || exit 1
    enable_csf
    exit $?
fi
if [ "$1" = "disable" ]
then
    is_csf_installed >/dev/null || exit 1
    disable_csf
    exit $?
fi
if [ "$1" = "coop_install" ]
then
    is_csf_installed >/dev/null || exit 1
    edit_allow_list "$IMUNIFY_ALLOW_CONF" add
    exit $?
fi
if [ "$1" = "remove" ]
then
    edit_allow_list "$IMUNIFY_ALLOW_CONF" remove
    exit $?
fi
