#!/usr/bin/env bash
# check-ivanti-epm-csa.sh: strictly read-only, offline version check for
#   Ivanti Endpoint Manager (EPM) CVE-2026-121653 (SQL injection in the Core, 9.8 Critical per Ivanti as CNA, CVSS 3.x)
#   Ivanti Cloud Services Application (CSA) CVE-2026-121683 (missing authentication, 9.4 Critical per Ivanti as CNA, CVSS 3.x)
# It only compares the version strings you pass or a CSV you saved (for example an RMM export).
# No network, no file writes, no payloads, never contacts an EPM Core or a CSA.
#
# Usage:
#   check-ivanti-epm-csa.sh --product epm|csa --version VER [--name NAME]
#   check-ivanti-epm-csa.sh --inventory FILE      (CSV header: name,product,version)
# Fixed releases (Ivanti advisories, 10 Nov 2026):
#   EPM: affected 2024 SU7 and prior, resolved 2024 SU8.
#   CSA: affected 5.0.8 only, resolved 5.0.8.1; 5.0.7 and prior not impacted.
# Exit: 0 PATCHED or NOT AFFECTED, 1 VULNERABLE, 3 UNKNOWN (none vulnerable), 4 usage or read error. 2 is not used.
set -u

usage() {
  cat >&2 <<'U'
Usage: check-ivanti-epm-csa.sh --product epm|csa --version VER [--name NAME]
       check-ivanti-epm-csa.sh --inventory FILE
  --product    epm (Endpoint Manager Core) or csa (Cloud Services Application)
  --version    EPM release such as 2024 SU7 (quote it), or CSA version such as 5.0.8.1
  --name       label for the output (default: input)
  --inventory  CSV: name,product,version
Exit: 0 PATCHED or NOT AFFECTED, 1 VULNERABLE, 3 UNKNOWN, 4 usage or read error
U
  exit 4
}
die() { echo "ERROR: $1" >&2; exit 4; }
lc() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
trim() { local s=$1; s="${s#"${s%%[![:space:]]*}"}"; s="${s%"${s##*[![:space:]]}"}"; printf '%s' "$s"; }

norm_product() {
  local p; p=$(lc "$(trim "$1")")
  case $p in
    epm|"epm core"|"endpoint manager"|"ivanti endpoint manager"|"ivanti epm"|"ivanti endpoint manager (epm)") echo epm;;
    csa|"cloud services application"|"cloud services appliance"|"ivanti csa"|"ivanti cloud services application"|"ivanti cloud services appliance") echo csa;;
    *) echo "$p";;
  esac
}

N=0; NP=0; NN=0; NV=0; NU=0
add() { # verdict
  N=$((N+1))
  case $1 in PATCHED) NP=$((NP+1));; "NOT AFFECTED") NN=$((NN+1));; VULNERABLE) NV=$((NV+1));; *) NU=$((NU+1));; esac
}
say() { # verdict where reason
  add "$1"; echo "$1: $2: $3"
}

judge_epm() { # where version
  local w=$1 v r y s
  v=$(lc "$(trim "$2")")
  if [[ $v =~ ^(20[0-9][0-9])$ ]]; then y=${BASH_REMATCH[1]}; s=0
  elif [[ $v =~ ^(20[0-9][0-9])[\ _-]?su[\ _-]?([0-9]{1,2})$ ]]; then y=${BASH_REMATCH[1]}; s=$((10#${BASH_REMATCH[2]}))
  elif [[ $v =~ ^(20[0-9][0-9])[\ _-]?ga$ ]]; then y=${BASH_REMATCH[1]}; s=0
  else say UNKNOWN "$w" "not an EPM release name this pack can judge (use the form 2024 SU8). Build numbers are not mapped."; return; fi
  if [ "$y" -gt 2024 ]; then r="a release year after 2024 that Ivanti's November 2026 advisory does not cover. Confirm the fix in the console."; say UNKNOWN "$w" "$r"; return; fi
  if [ "$y" -lt 2024 ]; then say VULNERABLE "$w" "older than the 2024 branch. The CVE record says affected before 2024 SU8 and Ivanti's only fix is 2024 SU8. Upgrade the Core to 2024 SU8."; return; fi
  if [ "$s" -ge 8 ]; then say PATCHED "$w" "at or above 2024 SU8, the fixed release for CVE-2026-121653 (and CVE-2026-18128)."; return; fi
  say VULNERABLE "$w" "below 2024 SU8. Ivanti lists 2024 SU7 and prior as affected by CVE-2026-121653 (and CVE-2026-18128). Update the Core to 2024 SU8 from ILS."
}

judge_csa() { # where version
  local w=$1 v a b c d
  v=$(trim "$2")
  if ! [[ $v =~ ^([0-9]{1,4})\.([0-9]{1,4})(\.([0-9]{1,4}))?(\.([0-9]{1,4}))?$ ]]; then
    say UNKNOWN "$w" "not a CSA version this pack can judge (use the form 5.0.8.1)."; return
  fi
  a=$((10#${BASH_REMATCH[1]})); b=$((10#${BASH_REMATCH[2]})); c=$((10#${BASH_REMATCH[4]:-0})); d=$((10#${BASH_REMATCH[6]:-0}))
  local k=$(( a*1000000 + b*1000 + c ))
  if [ "$k" -lt 5000008 ]; then say "NOT AFFECTED" "$w" "Ivanti says 5.0.7 and prior is not impacted by CVE-2026-121683."; return; fi
  if [ "$k" -gt 5000008 ]; then say UNKNOWN "$w" "above 5.0.8.x, a version Ivanti's advisory does not list. Confirm with Ivanti."; return; fi
  if [ "$d" -ge 1 ]; then say PATCHED "$w" "at or above the fixed build 5.0.8.1 for CVE-2026-121683."; return; fi
  say VULNERABLE "$w" "5.0.8 is the only affected version for CVE-2026-121683. Update to 5.0.8.1. Until then, Ivanti says a Dual NIC setup limits exposure to the internal interface."
}

judge() { # name product version
  local n=$1 p v w
  p=$(norm_product "$2"); v=$(trim "$3")
  w="$n (${p:-no product}, ${v:-no version})"
  if [ -z "$v" ]; then say UNKNOWN "$w" "no version given."; return; fi
  case $p in
    epm) judge_epm "$w" "$v";;
    csa) judge_csa "$w" "$v";;
    *) say UNKNOWN "$w" "product not recognised (use epm or csa).";;
  esac
}

read_inventory() {
  local f=$1 h line n p v rows=0
  if [ ! -f "$f" ] || [ ! -r "$f" ]; then die "cannot read inventory file: $f"; fi
  IFS= read -r h < "$f" || die "inventory file is empty: $f"
  h=${h%$'\r'}
  [ "$(lc "$(trim "$h")")" = "name,product,version" ] || die "CSV header must be: name,product,version"
  while IFS= read -r line || [ -n "$line" ]; do
    line=${line%$'\r'}
    case $(trim "$line") in ''|\#*) continue;; esac
    IFS=, read -r n p v _ <<< "$line"
    n=$(trim "${n:-}")
    judge "${n:-name not given}" "${p:-}" "${v:-}"
    rows=$((rows+1))
  done < <(tail -n +2 "$f")
  [ "$rows" -gt 0 ] || die "inventory has no data rows: $f"
}

prod="" ver="" name="input" inv="" seen_ver=0 seen_prod=0 seen_name=0
while [ $# -gt 0 ]; do
  [ $# -ge 2 ] || usage
  case $1 in
    --product) prod=$2; seen_prod=1;;
    --version) ver=$2; seen_ver=1;;
    --name) name=$2; seen_name=1;;
    --inventory) inv=$2;;
    *) usage;;
  esac
  shift 2
done
if [ -n "$inv" ] && [ $((seen_ver + seen_prod + seen_name)) -gt 0 ]; then usage; fi
if [ -z "$inv" ] && { [ "$seen_ver" -eq 0 ] || [ "$seen_prod" -eq 0 ]; }; then usage; fi

# Read the inventory fully before printing anything, so read errors leave stdout empty.
if [ -n "$inv" ]; then
  out=$(read_inventory "$inv") || exit 4
  counts=$(printf '%s\n' "$out" | awk -F': ' '{print $1}' | sort | uniq -c)
  NP=$(printf '%s\n' "$counts" | awk '$2=="PATCHED"{print $1}'); NP=${NP:-0}
  NN=$(printf '%s\n' "$counts" | awk '$2=="NOT"{print $1}'); NN=${NN:-0}
  NV=$(printf '%s\n' "$counts" | awk '$2=="VULNERABLE"{print $1}'); NV=${NV:-0}
  NU=$(printf '%s\n' "$counts" | awk '$2=="UNKNOWN"{print $1}'); NU=${NU:-0}
  N=$((NP+NN+NV+NU))
else
  out=$(judge "$name" "$prod" "$ver")
  case $out in PATCHED:*) NP=1;; "NOT AFFECTED:"*) NN=1;; VULNERABLE:*) NV=1;; *) NU=1;; esac
  N=1
fi

echo "check-ivanti-epm-csa: offline version check for Ivanti EPM CVE-2026-121653 and CSA CVE-2026-121683 (advisories read 10 Nov 2026 17:06 GMT)."
echo "KEV: neither CVE listed in CISA KEV catalog 2026.11.09. Patch now regardless."
printf '%s\n' "$out"
if [ "$NV" -gt 0 ]; then res=VULNERABLE; rc=1
elif [ "$NU" -gt 0 ]; then res=UNKNOWN; rc=3
elif [ "$NP" -eq 0 ]; then res="NOT AFFECTED"; rc=0
else res=PATCHED; rc=0; fi
echo "RESULT: $res ($N checked: $NP PATCHED, $NN NOT AFFECTED, $NV VULNERABLE, $NU UNKNOWN)"
exit "$rc"
