#!/bin/bash
# aideupdate - review an AIDE database update and acknowledge it
# this file is managed by ansible
#
# See aideupdate(8). Code shared with dailyaidecheck lives in aide-lib.sh.
#
# Reviewing happens in less(1): quitting with q acknowledges the update,
# Ctrl-C aborts it.

set -e
set -u
umask 077

PATH="/sbin:/usr/sbin:/bin:/usr/bin"

# the library is deliberately not overridable from the environment, this
# script is run as root
AIDE_LIB="/usr/share/aide/lib/aide-lib.sh"
AIDE_TMP_SUFFIX=".aideupdate.tmp"
if ! [ -r "$AIDE_LIB" ]; then
    printf >&2 "ERR: %s is not readable\n" "$AIDE_LIB"
    exit 1
fi
# shellcheck source=aide-lib.sh
. "$AIDE_LIB"
if [ "${AIDE_LIB_API:-0}" != "1" ]; then
    printf >&2 "ERR: %s has API version %s, expected 1\n" "$AIDE_LIB" "${AIDE_LIB_API:-0}"
    exit 1
fi

CONFFILE="/etc/aide/aide.conf"
WORKDIR="/var/lib/aide"
OUTPUTFILE="${WORKDIR}/aideupdate.output"
FILTEREDOUTPUTFILE="${WORKDIR}/aideupdate.filteredoutput"
PKG_FILE_LIST="${WORKDIR}/aideupdate.pkg_file_list"
PKGS_LIST="${WORKDIR}/aideupdate.pkgs_list"

LESS_OPTS=(--LONG-PROMPT --shift=15 --chop-long-lines --jump-target=10
    --max-forw-scroll=100 --window=-4 --RAW-CONTROL-CHARS)

# this environment variable configures less
# shellcheck disable=SC2034
LESSHISTFILE="/dev/null"
export LESSHISTFILE
unset LESS LESSOPEN LESSCLOSE

usage() {
    printf >&2 "usage: %s [update | rollbackdb | showlog]\n" "${0##*/}"
}

die() {
    aide_err "$*"
    aide_banner "error"
    exit 1
}

COMMAND="${1:-update}"
if [ "$#" -gt 1 ]; then
    usage
    exit 2
fi
case "$COMMAND" in
    update|rollbackdb|showlog)
        ;;
    -h|--help)
        usage
        exit 0
        ;;
    *)
        usage
        exit 2
        ;;
esac

aide_conf_db_paths "$CONFFILE"
DBINFILE="$AIDE_DB_IN"
DBOUTFILE="$AIDE_DB_OUT"
if [ -z "$DBINFILE" ]; then
    die "no database_in=file:/... found in $CONFFILE"
fi
if [ -z "$DBOUTFILE" ]; then
    die "no database_out=file:/... found in $CONFFILE"
fi
DBBACKUPFILE="$(dirname "$DBINFILE")/backup.$(basename "$DBINFILE")"

case "$COMMAND" in
    rollbackdb)
        if ! [ -f "$DBBACKUPFILE" ]; then
            die "there is no backup database $DBBACKUPFILE to roll back to"
        fi
        aide_info "rolling back database: cp $DBBACKUPFILE $DBINFILE"
        aide_copy_db_atomic "$DBBACKUPFILE" "$DBINFILE" || die "rollback failed, $DBINFILE is unchanged"
        exit 0
        ;;
    showlog)
        if ! [ -f "$OUTPUTFILE" ]; then
            die "$OUTPUTFILE does not exist"
        fi
        less "${LESS_OPTS[@]}" "$OUTPUTFILE"
        exit 0
        ;;
esac

# From here on this is an update. Acknowledging it is done by quitting less,
# so there must be a person looking at a terminal. If stdout was a pipe or a
# file, less would behave like cat and exit successfully without anybody
# having reviewed anything.
if ! [ -t 0 ] || ! [ -t 1 ]; then
    die "stdin and stdout must be a terminal, the review in less is the acknowledgement"
fi

if command -v update-aide.conf > /dev/null; then
    update-aide.conf
fi

DBSTAT_FORMAT="%n %i %s %f %a"

rm -f -- "$OUTPUTFILE"
AIDERET=0
aide_info "executing aide --update"
nice aide --config="$CONFFILE" --update > "$OUTPUTFILE" || AIDERET="$?"
aide_info "full aide output in $OUTPUTFILE"
aide_info "aide --update exited with exit code $AIDERET"

case "$AIDERET" in
    0)
        # no changes: the new database only differs in its time stamps
        grep '^End timestamp' "$OUTPUTFILE" || true
        aide_banner "unchanged"
        if cmp -s "$DBOUTFILE" "$DBINFILE"; then
            printf "databases are identical\n"
        else
            printf "databases do differ, copy done\n"
            printf "cp %s %s\n" "$DBOUTFILE" "$DBINFILE"
            aide_copy_db_atomic "$DBOUTFILE" "$DBINFILE" || die "could not copy $DBOUTFILE to $DBINFILE, the old database is unchanged"
        fi
        aide_info "end."
        exit 0
        ;;
    [1-7])
        # there are changes to review
        ;;
    *)
        die "aide --update failed with exit code $AIDERET, not updating database"
        ;;
esac

# Remember what the new database looks like right now and what AIDE says it
# looks like. Both are checked again after the review.
DBSTAT_BEFORE="$(stat --format "$DBSTAT_FORMAT" "$DBOUTFILE")" || die "cannot stat $DBOUTFILE"
CHKSUM_FROM_LOG="$(aide_log_checksum "$OUTPUTFILE" "$DBOUTFILE")"
if [ -z "$CHKSUM_FROM_LOG" ]; then
    die "checksum parsed from log is empty"
fi

# Take packages out of the report that dpkg installed or upgraded since the
# last database was written.
DPKGLOG="$(aide_dpkg_log)"
if [ -s "$DPKGLOG" ]; then
    aide_pkg_files "$(aide_db_date "$DBINFILE")" "install|upgrade" "$DPKGLOG" "$PKG_FILE_LIST" "$PKGS_LIST"
    if [ -s "$PKGS_LIST" ]; then
        rm -f -- "$FILTEREDOUTPUTFILE"
        if aide_filter_report "$OUTPUTFILE" "${FILTEREDOUTPUTFILE}.body" "$(aide_conf_grouped "$CONFFILE")" "$PKG_FILE_LIST" "$PKGS_LIST"; then
            {
                grep '^End timestamp' "$OUTPUTFILE" || true
                printf -- "------------------------------------------------------------\n"
                printf -- "aide output after filtering changes done by dpkg\n"
                printf -- "------------------------------------------------------------\n\n"
                cat "${FILTEREDOUTPUTFILE}.body"
                printf -- "\n\n============================================================\n"
                printf -- "full unfiltered aide output\n"
                printf -- "============================================================\n\n"
                cat "$OUTPUTFILE"
            } > "$FILTEREDOUTPUTFILE"
            mv -f -- "$FILTEREDOUTPUTFILE" "$OUTPUTFILE"
        else
            aide_err "could not filter package changes from the report, showing the unfiltered report"
        fi
        rm -f -- "${FILTEREDOUTPUTFILE}.body"
    fi
    rm -f -- "$PKG_FILE_LIST" "$PKGS_LIST"
fi

# The review. q acknowledges, Ctrl-C aborts (--quit-on-intr makes less quit on
# Ctrl-C, and the same SIGINT reaches this shell, which runs the trap once
# less has exited). Anything else that makes less exit unsuccessfully is an
# abort as well.
trap '{ aide_info "INT received, not updating db and terminating"; aide_banner "not updated"; exit 1; }' INT
LESSRET=0
less "${LESS_OPTS[@]}" --quit-on-intr "$OUTPUTFILE" || LESSRET="$?"
trap - INT
if [ "$LESSRET" -ne 0 ]; then
    aide_info "less exited with status $LESSRET, not updating db"
    aide_banner "not updated"
    exit 1
fi
aide_info "user ACKed database by quitting less"

# Show the top of the report again so that it stays in the scrollback.
ROWS="$(stty size | awk '{print $1}')"
PRINTROWS="$(( ROWS - 20 ))"
if [ "$PRINTROWS" -lt 1 ]; then
    PRINTROWS=1
fi
head -n "$PRINTROWS" "$OUTPUTFILE"
aide_info "full aide output in $OUTPUTFILE"

# Stage the database that is going to be installed and check that copy: the
# file that was checked is the file that gets installed.
STAGED=""
if ! STAGED="$(aide_stage_db "$DBOUTFILE" "$DBINFILE")"; then
    die "could not copy $DBOUTFILE next to $DBINFILE, the old database is unchanged"
fi
CHKSUM_OF_DB="$(aide_sha512_b64 "$STAGED")" || CHKSUM_OF_DB=""
if [ -z "$CHKSUM_OF_DB" ]; then
    rm -f -- "$STAGED"
    die "checksum of database is empty"
fi
DBSTAT_AFTER="$(stat --format "$DBSTAT_FORMAT" "$DBOUTFILE")" || DBSTAT_AFTER="stat failed"

if [ "$CHKSUM_OF_DB" = "$CHKSUM_FROM_LOG" ] && [ "$DBSTAT_BEFORE" = "$DBSTAT_AFTER" ]; then
    aide_info "expected database checksum matches actual checksum"
    aide_info "updating database"
    if ! aide_copy_db_atomic "$DBINFILE" "$DBBACKUPFILE"; then
        rm -f -- "$STAGED"
        die "could not write backup database $DBBACKUPFILE, the old database is unchanged"
    fi
    if ! mv -f -- "$STAGED" "$DBINFILE"; then
        rm -f -- "$STAGED"
        die "could not install $DBINFILE, the old database is unchanged"
    fi
    aide_info "database backup in $DBBACKUPFILE"
    aide_info "to roll back: ${0##*/} rollbackdb"
    aide_banner "updated"
else
    rm -f -- "$STAGED"
    if [ "$CHKSUM_OF_DB" != "$CHKSUM_FROM_LOG" ]; then
        aide_err "checksum mismatch"
        aide_err "$DBOUTFILE actual checksum $CHKSUM_OF_DB"
        aide_err "expected checksum from aide --update log is $CHKSUM_FROM_LOG"
    fi
    if [ "$DBSTAT_BEFORE" != "$DBSTAT_AFTER" ]; then
        aide_err "stat mismatch"
        aide_err "$DBOUTFILE  actual stat $DBSTAT_AFTER"
        aide_err "expected stat $DBSTAT_BEFORE"
    fi
    aide_err "checksum or stat mismatch, this is a possible sign of a manipulated database"
    aide_info "not updating database"
    aide_banner "DB mismatch"
    aide_info "end."
    exit 1
fi
aide_info "end."

# vim: ts=4 sw=4 sts=4 sr et
