#!/usr/bin/env bash # ansipa-fetch-alerts.sh — fetch security alerts from the server SMB share. # Runs as root every 10 minutes via ansipa-fetch-alerts.timer (usr_scan-notify). # # For each alert on the server that hasn't been acknowledged yet: # - Downloads it to ~/administration// for every active login session. # - A local file that has been deleted counts as acknowledged and is removed # from the server alerts directory on the next run. # # Prerequisites: # /etc/ansipa-smb.creds — Samba credentials file (deployed by deploy-ansipa-policies.yml) # /etc/ipa/default.conf — FreeIPA client config (provides server hostname) # smbclient — from the samba-client package set -euo pipefail LOG_TAG="ansipa-fetch-alerts" ADMIN_SUBDIR="administration" CREDS_FILE="/etc/ansipa-smb.creds" SMB_SHARE="ansipa-scans" STATE_DIR="/var/lib/ansipa-policies" FETCHED_STATE="$STATE_DIR/fetched-alerts" log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; } warn() { echo "[$LOG_TAG][WARN] $*" >&2; logger -t "$LOG_TAG" "WARN: $*" 2>/dev/null || true; } # ── Prerequisites ───────────────────────────────────────────────────────────── if [[ ! -f "$CREDS_FILE" ]]; then warn "Credentials file $CREDS_FILE not found — run deploy-ansipa-policies.yml first." exit 0 fi if ! command -v smbclient &>/dev/null; then warn "smbclient not installed — install samba-client." exit 0 fi IPA_SERVER=$(awk '/^server[[:space:]]*=/{print $3}' /etc/ipa/default.conf 2>/dev/null || echo "") if [[ -z "$IPA_SERVER" ]]; then warn "Cannot read IPA server from /etc/ipa/default.conf — host enrolled?" exit 0 fi HOSTNAME=$(hostname -f 2>/dev/null || hostname) mkdir -p "$STATE_DIR" touch "$FETCHED_STATE" smb() { smbclient "//$IPA_SERVER/$SMB_SHARE" -A "$CREDS_FILE" "$@" 2>/dev/null; } # ── List active login sessions (usr_scan-notify members only) ───────────────── # loginctl list-sessions --no-legend: SESSION UID USER SEAT TTY → $3 = USER # who: USER TTY DATE TIME (HOST) → $1 = USER # Only deliver to members of the usr_scan-notify FreeIPA user group. ACTIVE_USERS=() _parse_session_user() { local line="$1" src="$2" local user if [[ "$src" == "loginctl" ]]; then user=$(awk '{print $3}' <<< "$line") else user=$(awk '{print $1}' <<< "$line") fi [[ -z "$user" || "$user" == "root" || "$user" == "USER" ]] && return # Only deliver to members of usr_scan-notify. id -nG "$user" 2>/dev/null | tr ' ' '\n' | grep -qxF "usr_scan-notify" || return local home home=$(getent passwd "$user" | cut -d: -f6 2>/dev/null) || return [[ -d "$home" ]] && ACTIVE_USERS+=("$user:$home") } if loginctl list-sessions --no-legend &>/dev/null; then while IFS= read -r LINE; do _parse_session_user "$LINE" "loginctl" done < <(loginctl list-sessions --no-legend 2>/dev/null) else while IFS= read -r LINE; do _parse_session_user "$LINE" "who" done < <(who 2>/dev/null) fi # Deduplicate by user. mapfile -t ACTIVE_USERS < <(printf '%s\n' "${ACTIVE_USERS[@]+"${ACTIVE_USERS[@]}"}" | sort -u) # ── List alerts on server for this host ─────────────────────────────────────── # smbclient `ls path\` treats the argument as a pattern, not a cd target; # use `cd path; ls` to list directory contents instead. SERVER_ALERTS=() while IFS= read -r LINE; do # smbclient ls output: " filename.alert A 1234 date" FILE=$(awk '{print $1}' <<< "$LINE") [[ "$FILE" == *.alert ]] && SERVER_ALERTS+=("$FILE") done < <(smb -c "cd alerts\\$HOSTNAME; ls" 2>/dev/null || true) # ── Check for locally deleted alerts (acknowledged) ─────────────────────────── ACKNOWLEDGED=() while IFS= read -r ALERT_NAME; do [[ -z "$ALERT_NAME" ]] && continue # If none of the active users still have this alert file, it was acknowledged. ALL_DELETED=true for USER_INFO in "${ACTIVE_USERS[@]}"; do HOME_DIR="${USER_INFO#*:}" USER="${USER_INFO%%:*}" LOCAL_FILE="$HOME_DIR/$ADMIN_SUBDIR/$HOSTNAME/$ALERT_NAME" if [[ -f "$LOCAL_FILE" ]]; then ALL_DELETED=false break fi done if [[ "$ALL_DELETED" == true ]] && [[ ${#ACTIVE_USERS[@]} -gt 0 ]]; then log "Alert acknowledged (deleted locally): $ALERT_NAME — removing from server" smb -c "del alerts\\$HOSTNAME\\$ALERT_NAME" 2>/dev/null || true # Remove from state file and skip re-download in this run. sed -i "/^$ALERT_NAME\$/d" "$FETCHED_STATE" 2>/dev/null || true ACKNOWLEDGED+=("$ALERT_NAME") fi done < "$FETCHED_STATE" # ── Download new/pending alerts to user home dirs ───────────────────────────── TMP_DIR=$(mktemp -d /tmp/ansipa-alerts.XXXXXX) trap 'rm -rf "$TMP_DIR"' EXIT for ALERT_NAME in "${SERVER_ALERTS[@]}"; do # Skip alerts already acknowledged (and deleted from server) in this run. [[ " ${ACKNOWLEDGED[*]+"${ACKNOWLEDGED[*]}"} " == *" $ALERT_NAME "* ]] && continue TMP_FILE="$TMP_DIR/$ALERT_NAME" # Download alert content from server. smb -c "get alerts\\$HOSTNAME\\$ALERT_NAME $TMP_FILE" 2>/dev/null || { warn "Failed to download alert: $ALERT_NAME" continue } NEW=false for USER_INFO in "${ACTIVE_USERS[@]}"; do HOME_DIR="${USER_INFO#*:}" USER="${USER_INFO%%:*}" LOCAL_DIR="$HOME_DIR/$ADMIN_SUBDIR/$HOSTNAME" LOCAL_FILE="$LOCAL_DIR/$ALERT_NAME" mkdir -p "$LOCAL_DIR" chown "$USER" "$LOCAL_DIR" 2>/dev/null || true if [[ ! -f "$LOCAL_FILE" ]]; then cp "$TMP_FILE" "$LOCAL_FILE" chown "$USER" "$LOCAL_FILE" chmod 644 "$LOCAL_FILE" NEW=true fi done # Track fetched alerts so we can detect acknowledgment on the next run. if ! grep -qx "$ALERT_NAME" "$FETCHED_STATE" 2>/dev/null; then echo "$ALERT_NAME" >> "$FETCHED_STATE" fi [[ "$NEW" == true ]] && log "New alert delivered: $ALERT_NAME" done log "Done. ${#SERVER_ALERTS[@]} server alert(s) for $HOSTNAME."