Dotfiles/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh

158 lines
6.3 KiB
Bash

#!/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/<hostname>/ 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."