#!/bin/bash set -u set -o pipefail PROG="$(basename "$0")" MODE="" MAILTO="" CONFIG="/etc/aide/aide.conf" QUIET=0 REPORT=0 NOEMAIL_IF_NOCHANGE=0 # if 1: do NOT email when there are no changes (even if -mailto is set) log() { if [[ "$QUIET" -eq 0 ]]; then echo "$@" fi } die() { echo "ERROR: $*" exit 2 } usage() { cat <] [-config ] [-quiet] [-noemailifnochange] $PROG -update [-mailto ] [-config ] [-quiet] [-noemailifnochange] Options: -check Check only (runs: aide --check) -update Check and update DB (runs: aide --update, then moves DB out -> DB) -mailto Send an email report to one or more recipients. If changes are detected, subject includes how many files/entries changed. -config AIDE config file (default: /etc/aide/aide.conf) -quiet Print nothing to stdout/stderr (except fatal errors) -report Print AIDE report in stdout -noemailifnochange Do NOT send email when there are no changes (only email on changes) Examples: $PROG -check $PROG -check -mailto admin@example.com,sec@example.com -noemailifnochange -quiet $PROG -update -config /etc/aide/aide.conf -mailto sec@example.com EOF } need_bin() { command -v "$1" >/dev/null 2>&1 || die "Required binary not found in PATH: $1" } # Extract AIDE DB paths from config: # database=file:/var/lib/aide/aide.db # database_out=file:/var/lib/aide/aide.db.new # Returns: database_path database_out_path parse_db_paths() { local cfg="$1" local db db_out db="$(grep ^database_in: "$cfg" | awk '{print $2}')" db_out="$(grep ^database_out: "$cfg" | awk '{print $2}')" db="${db#file:}" db_out="${db_out#file:}" [[ -n "${db:-}" ]] || db="/var/lib/aide/aide.db" [[ -n "${db_out:-}" ]] || db_out="/var/lib/aide/aide.db.new" echo "$db" "$db_out" } # Parse summary counters from AIDE output. # Extracts the number of added, removed, and changed entries. # Returns: total added removed changed # If it cannot parse, returns: -1 0 0 0 parse_change_counts() { local f="$1" local added removed changed total added="$(grep -E 'Added entries' "$f" 2>/dev/null | grep -Eo '[0-9]+' | head -n1 || true)" removed="$(grep -E 'Removed entries' "$f" 2>/dev/null | grep -Eo '[0-9]+' | head -n1 || true)" changed="$(grep -E 'Changed entries' "$f" 2>/dev/null | grep -Eo '[0-9]+' | head -n1 || true)" [[ -n "${added:-}" ]] || added=0 [[ -n "${removed:-}" ]] || removed=0 [[ -n "${changed:-}" ]] || changed=0 total=$((added + removed + changed)) if ! grep -qE '(Added|Removed|Changed) entries' "$f" 2>/dev/null; then echo "-1 0 0 0" else echo "$total" "$added" "$removed" "$changed" fi } # Send email using available mail command (mail or sendmail) # Arguments: to subject body_file send_mail() { local to="$1" local subject="$2" local body_file="$3" local recip RECIPS local context="[AIDE][$HOST]" if [[ "$(basename "$CONFIG")" != "aide.conf" ]]; then context="${context}[$(basename -s .conf "$CONFIG")]" fi subject="$context $subject in $TIME" # Split recipients IFS=',' read -ra RECIPS <<<"$to" local rc=0 for recip in "${RECIPS[@]}"; do recip="$(echo "$recip" | xargs)" if command -v mail >/dev/null 2>&1; then mail -s "$subject" "$recip" <"$body_file" || rc=$? else { echo "To: $recip" echo "Subject: $subject" echo "MIME-Version: 1.0" echo "Content-Type: text/plain; charset=UTF-8" echo cat "$body_file" } | sendmail -t || rc=$? fi if [[ $rc -ne 0 ]]; then log "Failed sending email to $recip" fi done } # ----------------- Parse args ----------------- [[ $# -ge 1 ]] || { usage exit 1 } while [[ $# -gt 0 ]]; do case "$1" in -check) MODE="check" shift ;; -update) MODE="update" shift ;; -mailto) shift [[ $# -ge 1 ]] || die "-mailto requires at least one email address" MAILTO="$1" if ! command -v mail >/dev/null 2>&1 && ! command -v sendmail >/dev/null 2>&1; then die "Failed to find mail or sendmail." fi shift ;; -config) shift [[ $# -ge 1 ]] || die "-config requires a file path" CONFIG="$1" shift ;; -quiet) QUIET=1 shift ;; -report) REPORT=1 shift ;; -noemailifnochange) NOEMAIL_IF_NOCHANGE=1 shift ;; -h | --help | -help) usage exit 0 ;; *) die "Unknown argument: $1 (use --help)" ;; esac done [[ -n "$MODE" ]] || die "You must specify exactly one mode: -check or -update" [[ -r "$CONFIG" ]] || die "Cannot read config file: $CONFIG" need_bin aide # Optional warning (AIDE usually needs root to read everything) if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then log "WARNING: not running as root; AIDE may be unable to read some paths." fi TMP_OUT="$(mktemp -t aide-report.XXXXXX)" trap 'rm -f "$TMP_OUT"' EXIT # if ! aide --config "$CONFIG" --config-check -v &>"$TMP_OUT"; then SUBJECT="Bad configuration at '$CONFIG' (check your AIDE config)." if [[ -n "$MAILTO" ]]; then send_mail "$MAILTO" "$SUBJECT" "$TMP_OUT" fi die "$SUBJECT" fi read -r DB DB_OUT < <(parse_db_paths "$TMP_OUT") if [[ ! "$DB" ]]; then die "Database_in not found (check your AIDE config)." fi if [[ ! "$DB_OUT" ]]; then die "Database_out not found (check your AIDE config)." fi if [[ ! -f "$DB" ]]; then log "Database not found, initializing: $DB" MODE="init" fi # Avoid concurrent runs CONTEXT=${PROG}_$(basename "$CONFIG") exec 9>"/run/${CONTEXT}.lock" 2>/dev/null || exec 9>"/tmp/${CONTEXT}.lock" if command -v flock >/dev/null 2>&1; then flock -n 9 || die "Another instance is already running (lock: $LOCK)" fi HOST="$(hostname --fqdn 2>/dev/null || true)" log "[$(date -Is 2>/dev/null || true)] Running AIDE ($MODE) with config: $CONFIG" # ----------------- Run AIDE ----------------- RC=0 START=$(date +%s) case "$MODE" in check) aide --config "$CONFIG" --check >"$TMP_OUT" 2>/dev/null || RC=$? ;; update) aide --config "$CONFIG" --update >"$TMP_OUT" 2>/dev/null || RC=$? ;; init) aide --config "$CONFIG" --init >"$TMP_OUT" 2>/dev/null || RC=$? ;; *) die "Unknown mode: $MODE" ;; esac ((result = $(date +%s) - START)) TIME="$((result / 60))m $((result % 60))s" if [[ "$RC" -eq 0 ]]; then log "[$(date -Is 2>/dev/null || true)] Finish AIDE in ${TIME}" else log "[$(date -Is 2>/dev/null || true)] Finish AIDE with err ($RC) in ${TIME}" fi if [[ "$REPORT" -eq 1 ]]; then cat "$TMP_OUT" fi # ----------------- Interpret results ----------------- read -r TOTAL ADDED REMOVED CHANGED < <(parse_change_counts "$TMP_OUT") CHANGES_DETECTED=0 if [[ "$TOTAL" -gt 0 ]]; then CHANGES_DETECTED=1 elif [[ "$TOTAL" -eq 0 ]]; then CHANGES_DETECTED=0 else # TOTAL=-1: couldn't parse summary. If RC != 0 assume something happened (changes or error). [[ "$RC" -ne 0 ]] && CHANGES_DETECTED=1 fi if [[ "$MODE" == "init" ]]; then if [[ -f "$DB_OUT" && "$RC" -eq 0 ]]; then mv -f "$DB_OUT" "$DB" || die "Failed moving DB: $DB_OUT -> $DB" log "Database initialized: $DB" else SUBJECT="Bad Database initialization: $DB" if [[ -n "$MAILTO" ]]; then send_mail "$MAILTO" "$SUBJECT" "$TMP_OUT" fi die "$SUBJECT" fi else if [[ "$MODE" == "update" && "$RC" -ne 0 ]]; then # Move database_out -> database (as defined in config) if [[ -f "$DB_OUT" ]]; then mv -f "$DB_OUT" "$DB" || die "Failed moving updated DB: $DB_OUT -> $DB" log "Database updated: $DB" else log "NOTE: database_out not found at '$DB_OUT' (check your AIDE config)." fi fi if [[ "$CHANGES_DETECTED" -eq 1 ]]; then if [[ "$TOTAL" -ge 0 ]]; then log "Changes detected: total=$TOTAL (added=$ADDED removed=$REMOVED changed=$CHANGED)" else log "Changes detected (could not calculate exact count from output)." fi else log "No changes detected." fi fi # ----------------- Mail logic ----------------- # Default: if -mailto is set, send report ALWAYS. # If -noemailifnochange is set, only email when changes are detected. if [[ -n "$MAILTO" ]]; then SHOULD_MAIL=1 if [[ "$NOEMAIL_IF_NOCHANGE" -eq 1 && "$CHANGES_DETECTED" -eq 0 ]]; then SHOULD_MAIL=0 fi if [[ "$SHOULD_MAIL" -eq 1 ]]; then if [[ "$CHANGES_DETECTED" -eq 1 || "$RC" -ne 0 ]]; then if [[ "$TOTAL" -ge 0 ]]; then SUBJECT="Changes detected: $TOTAL files/entries" else SUBJECT="Changes detected (count unknown)" fi else SUBJECT="No changes" fi send_mail "$MAILTO" "$SUBJECT" "$TMP_OUT" fi fi if [[ "$QUIET" -eq 0 ]]; then exit "$RC" fi exit 0