#!/usr/bin/bash
# eldric-logs-apply-netflow - the logs role's privileged step for NetFlow (5.0.179, MAIN GO 2026-10-10).
#
# The unprivileged eldric daemon describes the two nfcapd receivers in ONE fixed drop file, as plain
# KEY=VALUE lines from a fixed schema. This script (root, started by eldric-logs-apply.path) builds the
# units ITSELF from a fixed template - no command line comes from the drop file - and enables/restarts
# only eldric-nfcapd-a and eldric-nfcapd-b. It refuses, and changes nothing, when:
#   - the drop file is not a regular file, is a symlink, is not owned by eldric, is group/other-writable,
#     is larger than 4 KiB, or lacks the generator's header
#   - it has any line other than the header, comments and exactly PORT, ROTATE, IDENT, DIR_A, DIR_B
#   - PORT is not 1024..65534, ROTATE not 60..3600, IDENT not 1-32 of a-z 0-9 _
#   - DIR_A / DIR_B are not "<root>/netflow" with <root> an allowed archive root: the data dir or an
#     archive copy named in the ROOT-OWNED /etc/eldric/eldric-aios.env (ELDRIC_DATA_DIR,
#     ELDRIC_LOGS_PRIMARY_DIR, ELDRIC_LOGS_SECONDARY_DIR), or a line of the root-owned
#     /etc/eldric/logs-netflow-roots. Never from the daemon's own settings.
# Result: netflow-result.json (ok, step, detail, sha256 of the drop file). An unchanged drop file that
# was applied before is not applied again (the path unit also fires for the rsyslog drop file).
# The *_OVERRIDE variables exist for the test rig only; the packaged unit sets none of them.
set -u
umask 022
DIR="${ELDRIC_LOGS_APPLY_DIR_OVERRIDE:-/var/lib/eldric-logs-apply}"
DROP="$DIR/netflow.conf"
RESULT="$DIR/netflow-result.json"
UNIT_DIR="${ELDRIC_LOGS_APPLY_UNIT_DIR_OVERRIDE:-/etc/systemd/system}"
SYSTEMCTL="${ELDRIC_LOGS_APPLY_SYSTEMCTL_OVERRIDE:-systemctl}"
ENV_FILE="${ELDRIC_LOGS_APPLY_ENV_FILE_OVERRIDE:-/etc/eldric/eldric-aios.env}"
ROOTS_FILE="${ELDRIC_LOGS_APPLY_ROOTS_FILE_OVERRIDE:-/etc/eldric/logs-netflow-roots}"
OWNER="${ELDRIC_LOGS_APPLY_OWNER_OVERRIDE:-eldric}"
NFCAPD="/usr/bin/nfcapd"
HEADER='# Generated by Eldric (logs module).'
SUM=""

json_str() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | tr '\n\t' '  '; }
result() {  # result <true|false> <step> <detail>
  printf '{"ok":%s,"step":"%s","detail":"%s","sha256":"%s","at":"%s"}\n' "$1" "$2" "$(json_str "$3")" "$SUM" \
    "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$RESULT.tmp" && chmod 0644 "$RESULT.tmp" && mv -f "$RESULT.tmp" "$RESULT"
  [ "$1" = true ] && exit 0 || exit 1
}

[ -e "$DROP" ] || [ -L "$DROP" ] || exit 0
exec 9>"$DIR/.apply-netflow.lock" || exit 1
flock 9 || exit 1
[ -L "$DROP" ] && result false refused "the drop file is a symlink"
[ -f "$DROP" ] || result false refused "the drop file is not a regular file"
SUM=$(sha256sum "$DROP" | cut -d' ' -f1)
if [ -f "$RESULT" ] && grep -q '"ok":true' "$RESULT" && grep -q "\"sha256\":\"$SUM\"" "$RESULT"; then exit 0; fi
own=$(stat -c %U "$DROP"); [ "$own" = "$OWNER" ] || result false refused "the drop file is owned by '$own', not '$OWNER'"
mode=$(stat -c %a "$DROP")
case "$mode" in *[2367][0-7]|*[0-7][2367]) result false refused "the drop file is writable by group or others (mode $mode)";; esac
[ "$(wc -c < "$DROP")" -le 4096 ] || result false refused "the drop file is larger than 4 KiB"
head -n1 "$DROP" | grep -qF "$HEADER" || result false refused "the drop file was not written by the Eldric generator"

# Parse: exactly the five keys, once each; nothing else but the header and comments.
PORT=""; ROTATE=""; IDENT=""; DIR_A=""; DIR_B=""; seen=" "
while IFS= read -r line || [ -n "$line" ]; do
  case "$line" in ''|'#'*) continue;; esac
  if ! [[ "$line" =~ ^(PORT|ROTATE|IDENT|DIR_A|DIR_B)=(.*)$ ]]; then
    result false refused "line not allowed: $(printf '%s' "$line" | cut -c1-80)"
  fi
  k="${BASH_REMATCH[1]}"; v="${BASH_REMATCH[2]}"
  case "$seen" in *" $k "*) result false refused "$k is given twice";; esac
  seen="$seen$k "
  printf -v "$k" '%s' "$v"
done < "$DROP"
for k in PORT ROTATE IDENT DIR_A DIR_B; do case "$seen" in *" $k "*) ;; *) result false refused "$k is missing";; esac; done
[[ "$PORT" =~ ^[0-9]{4,5}$ ]] && [ "$PORT" -ge 1024 ] && [ "$PORT" -le 65534 ] || result false refused "PORT must be 1024..65534"
[[ "$ROTATE" =~ ^[0-9]{2,4}$ ]] && [ "$ROTATE" -ge 60 ] && [ "$ROTATE" -le 3600 ] || result false refused "ROTATE must be 60..3600"
[[ "$IDENT" =~ ^[a-z0-9_]{1,32}$ ]] || result false refused "IDENT must be 1-32 of a-z 0-9 _"

# Allowed archive roots - from root-owned files only.
roots=()
root_owned() { [ -f "$1" ] && [ ! -L "$1" ] && [ "$(stat -c %U "$1")" = root ] && case "$(stat -c %a "$1")" in *[2367][0-7]|*[0-7][2367]) false;; *) true;; esac; }
if root_owned "$ENV_FILE"; then
  for key in ELDRIC_DATA_DIR ELDRIC_LOGS_PRIMARY_DIR ELDRIC_LOGS_SECONDARY_DIR; do
    v=$(grep -E "^[[:space:]]*$key=" "$ENV_FILE" | tail -1 | sed -E "s/^[[:space:]]*$key=//; s/^[\"']//; s/[\"'][[:space:]]*$//")
    [ -n "$v" ] && roots+=("$v")
  done
fi
if root_owned "$ROOTS_FILE"; then
  while IFS= read -r r || [ -n "$r" ]; do case "$r" in ''|'#'*) ;; *) roots+=("$r");; esac; done < "$ROOTS_FILE"
fi
allowed() {  # allowed <dir>: "<root>/netflow" under an allowed root, plain characters, no ".."
  local d="$1" r
  [[ "$d" =~ ^/[A-Za-z0-9._/-]+/netflow$ ]] || return 1
  case "$d" in *..*|*//*) return 1;; esac
  for r in "${roots[@]+"${roots[@]}"}"; do
    r="${r%/}"; [[ "$r" =~ ^/[A-Za-z0-9._/-]+$ ]] || continue
    case "$d" in "$r"/*) return 0;; esac
  done
  return 1
}
allowed "$DIR_A" || result false refused "DIR_A is not <archive root>/netflow under an allowed root: $DIR_A"
allowed "$DIR_B" || result false refused "DIR_B is not <archive root>/netflow under an allowed root: $DIR_B"
[ "$DIR_A" != "$DIR_B" ] || result false refused "DIR_A and DIR_B are the same directory"

# Build the units from the fixed template.
PORT2=$((PORT + 1))
unit() {  # unit <description> <dir> <exec>
  printf '%s\n' "# Generated by eldric-logs-apply-netflow from $DROP. Do not edit." \
    "[Unit]" "Description=$1" "After=network-online.target" "" \
    "[Service]" "Type=simple" "ExecStartPre=/usr/bin/mkdir -p $2" "ExecStart=$3" "Restart=always" "RestartSec=5" "" \
    "[Install]" "WantedBy=multi-user.target"
}
unit "Eldric NetFlow receiver, first copy (repeats to the second)" "$DIR_A" \
  "$NFCAPD -p $PORT -w $DIR_A -S 1 -t $ROTATE -I $IDENT -z=lz4 -R 127.0.0.1/$PORT2 -A" > "$UNIT_DIR/eldric-nfcapd-a.service.new" &&
unit "Eldric NetFlow receiver, second copy" "$DIR_B" \
  "$NFCAPD -b 127.0.0.1 -p $PORT2 -w $DIR_B -S 1 -t $ROTATE -I $IDENT -z=lz4" > "$UNIT_DIR/eldric-nfcapd-b.service.new" &&
mv -f "$UNIT_DIR/eldric-nfcapd-a.service.new" "$UNIT_DIR/eldric-nfcapd-a.service" &&
mv -f "$UNIT_DIR/eldric-nfcapd-b.service.new" "$UNIT_DIR/eldric-nfcapd-b.service" ||
  result false install "could not write the units to $UNIT_DIR"
out=$($SYSTEMCTL daemon-reload 2>&1 && $SYSTEMCTL enable eldric-nfcapd-a.service eldric-nfcapd-b.service 2>&1 &&
      $SYSTEMCTL restart eldric-nfcapd-a.service eldric-nfcapd-b.service 2>&1) ||
  result false start "the units are installed, but starting them failed: $out"
result true started "eldric-nfcapd-a, eldric-nfcapd-b"
