#!/usr/bin/env bash
# check-checkpoint-gaia.sh: strictly read-only, offline check for Check Point Gaia CVE-2026-123039 (sk1000362), local
# privilege escalation to root through the DynamicCLI daemon (dynamic_server) and its world-writable /tmp/dynamic_socket.
# Reads only the values and files you pass. No network, no writes, never runs anything on a gateway or server.
#
# Usage:
#   check-checkpoint-gaia.sh --version VERSION --take N [--product PRODUCT] [--name NAME] [--review FILE ...]
#   check-checkpoint-gaia.sh --cpinfo FILE [--product PRODUCT] [--name NAME] [--review FILE ...]
#   check-checkpoint-gaia.sh --inventory FILE [--review FILE ...]
#   check-checkpoint-gaia.sh --review FILE ...
#   --version    Gaia version as Check Point writes it: R82.20, R82.10, R82, R81.20, R81.10, R81
#   --take       installed Jumbo Hotfix Accumulator Take number (0 if none)
#   --cpinfo     saved output of 'cpinfo -y all' (sk83860); version and Jumbo Take are read from it
#   --product    gateway (default), management, log server, mds, mdls, maestro orchestrator, spark, smart-1 cloud
#   --inventory  CSV with header name,product,version,take (# lines skipped)
#   --review     saved file listing, log export, dynamicCli.log copy or 'ls -l /tmp/dynamic_socket' line to search for
#                the published IOCs (read only; only IOC labels, counts and line numbers are printed)
# Exit: 0 PATCHED or NOT AFFECTED, 1 VULNERABLE, 2 NO FIX, 3 UNKNOWN, 4 usage or read error, 5 IOC REVIEW.
#       Order: 5 > 1 > 2 > 3 > 0. A --review run with nothing to judge and no IOC hits exits 3.
set -u
export LC_ALL=C

# Fixed Jumbo Hotfix Accumulator Takes from sk1000362 (read 12 Nov 2026 17:08 GMT).
FIX_TABLE="R82.20:14 R82.10:52 R82:132 R81.20:174"
NOFIX_VERSIONS="R81.10 R81"
R8120_NOTE="Take 174 was not on Check Point's R81.20 Jumbo resolved issues page when read 12 Nov 2026 17:09 GMT (latest listed: Take 173, 12 Oct 2026); apply the sk1000362 mitigation until you can install it."

usage() { sed -n '6,19p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 4; }
die() { echo "ERROR: $1" >&2; exit 4; }

lines=(); nP=0; nNA=0; nV=0; nF=0; nU=0
add() { # verdict where reason
  case $1 in PATCHED) nP=$((nP+1));; "NOT AFFECTED") nNA=$((nNA+1));; VULNERABLE) nV=$((nV+1));; "NO FIX") nF=$((nF+1));; UNKNOWN) nU=$((nU+1));; esac
  lines+=("$1: $2: $3")
}
trim() { printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'; }
norm_product() {
  local p; p=$(trim "$1" | tr '[:upper:]' '[:lower:]')
  case $p in
    gateway|"security gateway"|firewall|"force firewall"|"check point force firewall"|"cluster member") echo gateway;;
    management|"security management"|"management server"|"security management server") echo management;;
    "log server") echo "log server";;
    mds|"multi-domain security management"|"multi-domain server") echo mds;;
    mdls|"multi-domain log server") echo mdls;;
    "maestro orchestrator"|orchestrator) echo "maestro orchestrator";;
    spark|"spark firewall"|"check point spark firewall"|"quantum spark") echo spark;;
    "smart-1 cloud") echo "smart-1 cloud";;
    *) printf '%s\n' "$p";;
  esac
}
norm_version() { # R82.20 style or empty
  local v; v=$(trim "$1")
  if [[ $v =~ ^[Rr]([0-9]{2})(\.([0-9]{1,2}))?$ ]]; then
    if [ -n "${BASH_REMATCH[3]}" ]; then echo "R${BASH_REMATCH[1]}.${BASH_REMATCH[3]}"; else echo "R${BASH_REMATCH[1]}"; fi
  fi
}
check_item() { # name product version take
  local name=$1 p v raw t w fix e
  p=$(norm_product "$2"); raw=$(trim "$3"); t=$(trim "$4"); v=$(norm_version "$raw")
  w="$name (${p:-no product}, ${raw:-no version}${t:+ Take $t})"
  if [ -z "$p" ]; then add UNKNOWN "$w" "no product given."; return; fi
  case $p in
    spark|"smart-1 cloud")
      add "NOT AFFECTED" "$w" "sk1000362 lists Check Point Spark Firewalls and Smart-1 Cloud as non-affected products. Spark Firewalls are affected by PHP2Shell (sk1000302); check that separately."; return;;
    gateway|management|"log server"|mds|mdls|"maestro orchestrator") ;;
    *) add UNKNOWN "$w" "product not named in sk1000362 (affected: Force Firewalls, Management and Log Servers, Multi-Domain Security Management and Multi-Domain Log Server, Maestro Orchestrator)."; return;;
  esac
  if [ -z "$raw" ]; then add UNKNOWN "$w" "no version given."; return; fi
  if [ -z "$v" ]; then add UNKNOWN "$w" "not a Gaia version this pack can judge (use the form R82.10)."; return; fi
  case " $NOFIX_VERSIONS " in *" $v "*)
    add "NO FIX" "$w" "$v is affected (sk1000362) and has no fixed Jumbo Take (end of support). Apply the sk1000362 mitigation now (stop dynamic_server and prevent restart) and upgrade to a fixed version."; return;;
  esac
  fix=""
  for e in $FIX_TABLE; do [ "${e%%:*}" = "$v" ] && fix=${e#*:}; done
  if [ -z "$fix" ]; then add UNKNOWN "$w" "$v is not listed in sk1000362 (affected: R82.20, R82.10, R82, R81.20, R81.10, R81). Check with Check Point."; return; fi
  if [ -z "$t" ]; then add UNKNOWN "$w" "no Jumbo Take given. $v is fixed from Take $fix (sk1000362)."; return; fi
  if ! [[ $t =~ ^[0-9]{1,4}$ ]]; then add UNKNOWN "$w" "Jumbo Take must be a number (0 if none)."; return; fi
  if [ "$((10#$t))" -ge "$fix" ]; then
    add PATCHED "$w" "Jumbo Take $((10#$t)) is at or above Take $fix, the first $v Take with the fix (sk1000362). Confirm ls -l /tmp/dynamic_socket shows srw-rw---- root config (0660) and still hunt: patching does not remove an implant."
  else
    e="below Jumbo Take $fix, the first $v Take with the fix (sk1000362). Install Take $fix or later; until then apply the sk1000362 mitigation."
    [ "$v" = R81.20 ] && e="$e $R8120_NOTE"
    add VULNERABLE "$w" "$e"
  fi
}
read_cpinfo() { # file name product
  local f=$1 l ver="" takes="" t n=0 u
  if ! [ -f "$f" ] || ! [ -r "$f" ]; then die "cannot read cpinfo file: $f"; fi
  local vre="software version (R[0-9]{2}(\.[0-9]{1,2})?) - Build"
  while IFS= read -r l || [ -n "$l" ]; do
    l=${l%$'\r'}
    if [[ $l =~ $vre ]]; then
      [ -n "$ver" ] && [ "$ver" != "${BASH_REMATCH[1]}" ] && ver="CONFLICT" || ver=${BASH_REMATCH[1]}
    fi
  done < "$f"
  if [ -z "$ver" ] || [ "$ver" = CONFLICT ]; then
    add UNKNOWN "$2 (${3:-gateway}, cpinfo $f)" "no single \"software version Rxx - Build\" line found (format from sk83860). Enter --version and --take by hand."; return
  fi
  u=${ver//./_}
  local tre="HOTFIX_${u}_JUMBO_HF[A-Z0-9_]* Take: ([0-9]{1,4})([^0-9]|$)"
  while IFS= read -r l || [ -n "$l" ]; do
    l=${l%$'\r'}
    if [[ $l =~ $tre ]]; then t=$((10#${BASH_REMATCH[1]})); case " $takes " in *" $t "*) ;; *) takes="$takes $t"; n=$((n+1));; esac; fi
  done < "$f"
  if [ "$n" -gt 1 ]; then add UNKNOWN "$2 (${3:-gateway}, $ver)" "cpinfo shows more than one Jumbo Take ($(trim "$takes")). Confirm by hand."; return; fi
  if [ "$n" -eq 0 ]; then add UNKNOWN "$2 (${3:-gateway}, $ver)" "no HOTFIX_${u}_JUMBO_HF Take line in the cpinfo output (format from sk83860; the _MAIN suffix seen on newer versions is UNVERIFIED). Confirm the Take by hand."; return; fi
  check_item "$2" "${3:-gateway}" "$ver" "$(trim "$takes")"
}

# Published IOCs (sk1000362 and the Check Point Research post, both 12 Nov 2026). label|extended regex (case sensitive)
IOCS=(
  "webshell template zz*.tpl (CPR)|(^|[/[:space:]])zz[A-Za-z0-9._-]*\.tpl([^A-Za-z0-9]|$)"
  "challenge_render.tpl (CPR)|challenge_render\.tpl"
  "/tmp/.up.tpl (CPR)|/tmp/\.up\.tpl"
  "/tmp/CPconfigdaemon (CPR, TSGA)|/tmp/CPconfigdaemon"
  "/tmp/.bd.log (CPR)|/tmp/\.bd\.log"
  "slot_CPconfigdaemon (CPR, TSGA persistence)|slot_CPconfigdaemon"
  "/lib/libcpfwcommon.so (CPR, TSProxy)|/lib/libcpfwcommon\.so"
  "/tmp/.p_sock (CPR)|/tmp/\.p_sock"
  "upload_progress_pwn session marker (CPR)|upload_progress_pwn"
  "destroy_v6 wiper (CPR)|destroy_v6"
  "/tmp/des wiper path (CPR)|/tmp/des([[:space:]]|$)"
  "DREAM_ANNEXATION.txt note (CPR)|DREAM_ANNEXATION\.txt"
  "attacker IP 130.94.19.204 (CPR)|(^|[^0-9.])130\.94\.19\.204([^0-9]|$)"
  "attacker IP 130.94.19.168 (CPR)|(^|[^0-9.])130\.94\.19\.168([^0-9]|$)"
  "TCP 11072 listener (CPR, TSGA; listing format UNVERIFIED)|([Ll][Ii][Ss][Tt][Ee][Nn].*:11072([^0-9]|$))|(:11072[^0-9].*[Ll][Ii][Ss][Tt][Ee][Nn])"
  "dynamicCli.log system() sink, Received = #return_value# (sk1000362)|Received = #return_value#"
  "dynamicCli.log rejected unauthorized uid, blocked attempt (sk1000362)|Rejected connection from unauthorized uid"
)
HASHES="edb3e92eb7bda7413a09edcc4b1ba3cbed4bcdf42808a8abce63b6e4c3fda2b3 4171b521fa6441382aa2fa014a08e18ff9a6e0ab6096c9144072bf46b37a2ec8 d7549671c7189979f0624ec52dad9ea6a707d23146e3585ab8f8c4720eca0c63 3b51080a909023f7d484e2443740b572307ce0054eb9e32d98f0a06bfcd2db90 c7d9477309fac6c638a1128442056a242de086996add8cb49cd6496c36003237 0522937c0c0337be54fe25493539e5f5f8ceb9742c980aaff1cfc9515f3a96f7 ee16e1695711b9d60c62925fcfb2fe1a29c18ae26565bc05410a14e16d2a39d7 fb1a6235eb6bce8c9ec8cd8bb85f17c9c2bde8ecb45b7a243a30ceadf26654da"
WRAPPER="CPprofile.sh >> /dev/null; export SP_DISTRIBUTE=0; set -o noglob;"
SEGV_MIN=10

loglines=(); review=0
fmt_lines() { # "n1 n2 ..." to "k line(s): a,b,c" (first 5 shown)
  local -a a; read -r -a a <<< "$1"
  local k=${#a[@]} s
  s=$(printf '%s,' "${a[@]:0:5}"); s=${s%,}
  [ "$k" -gt 5 ] && s="$s,..."
  if [ "$k" -eq 1 ]; then echo "1 line: $s"; else echo "$k lines: $s"; fi
}
review_file() { # file
  local f=$1 ln=0 l i lc lh h k
  local -a hit=() ; local hh="" unw="" segv="" vpx="" s777="" s660="" sother=""
  for i in "${!IOCS[@]}"; do hit[i]=""; done
  if ! [ -f "$f" ] || ! [ -r "$f" ]; then die "cannot read review file: $f"; fi
  local sre="^s([rwxsStT-]{9})[.+]?[[:space:]]+[0-9]+[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]].*/tmp/dynamic_socket([[:space:]]|$)"
  local hre="[0-9a-f]{64}" recv="Received =" segre="segfault" webre="(httpd|php)" vre="vpxuser" sshre="(sshd|[^a-z]ssh[^a-z])"
  while IFS= read -r l || [ -n "$l" ]; do
    ln=$((ln+1)); l=${l%$'\r'}
    for i in "${!IOCS[@]}"; do
      if [[ $l =~ ${IOCS[i]#*|} ]]; then hit[i]="${hit[i]} $ln"; fi
    done
    lc=${l,,}; lh=$lc
    while [[ $lh =~ $hre ]]; do
      h=${BASH_REMATCH[0]}; case " $HASHES " in *" $h "*) hh="$hh $ln";; esac
      lh=${lh#*"$h"}
    done
    if [[ $l == *"$recv"* ]] && [[ $l != *"$WRAPPER"* ]]; then unw="$unw $ln"; fi
    if [[ $l =~ $segre ]] && [[ $l =~ $webre ]]; then segv="$segv $ln"; fi
    if [[ $lc =~ $vre ]] && [[ $lc =~ $sshre ]]; then vpx="$vpx $ln"; fi
    if [[ $l =~ $sre ]]; then
      if [ "${BASH_REMATCH[1]}" = "rwxrwxrwx" ]; then s777="$s777 $ln"
      elif [ "${BASH_REMATCH[1]}" = "rw-rw----" ] && [ "${BASH_REMATCH[2]}" = root ] && [ "${BASH_REMATCH[3]}" = config ]; then s660="$s660 $ln"
      else sother="$sother $ln"; fi
    fi
  done < "$f"
  loglines+=("REVIEW $f: $ln lines read.")
  for i in "${!IOCS[@]}"; do
    if [ -n "${hit[i]}" ]; then loglines+=("IOC REVIEW: $f: ${IOCS[i]%%|*} ($(fmt_lines "${hit[i]}"))."); review=$((review+1)); fi
  done
  if [ -n "$hh" ]; then loglines+=("IOC REVIEW: $f: SHA-256 from the Check Point Research IOC list ($(fmt_lines "$hh"))."); review=$((review+1)); fi
  if [ -n "$unw" ]; then loglines+=("IOC REVIEW: $f: dynamicCli.log \"Received =\" without the standard Clish wrapper (sk1000362) ($(fmt_lines "$unw"))."); review=$((review+1)); fi
  k=0; [ -n "$segv" ] && { read -r -a _s <<< "$segv"; k=${#_s[@]}; }
  if [ "$k" -ge "$SEGV_MIN" ]; then loglines+=("IOC REVIEW: $f: OUR PATTERN, $k segfault lines naming httpd or php (CPR reports hundreds during exploitation; message format UNVERIFIED) ($(fmt_lines "$segv"))."); review=$((review+1))
  elif [ "$k" -gt 0 ]; then loglines+=("INFO: $f: $k segfault line(s) naming httpd or php, below our threshold of $SEGV_MIN ($(fmt_lines "$segv")).")
  fi
  if [ -n "$vpx" ]; then loglines+=("IOC REVIEW: $f: OUR PATTERN, SSH activity naming vpxuser (CPR: lateral movement to ESXi as vpxuser; ESXi log format UNVERIFIED) ($(fmt_lines "$vpx"))."); review=$((review+1)); fi
  if [ -n "$s777" ]; then add VULNERABLE "$f /tmp/dynamic_socket ($(fmt_lines "$s777"))" "srwxrwxrwx (0777), the affected state described in sk1000362. Patch or apply the sk1000362 mitigation."; fi
  if [ -n "$s660" ]; then loglines+=("INFO: $f: /tmp/dynamic_socket is srw-rw---- root config (0660), the fixed state described in sk1000362 ($(fmt_lines "$s660")).")
  fi
  if [ -n "$sother" ]; then loglines+=("INFO: $f: /tmp/dynamic_socket permissions or owner not described in sk1000362; check by hand ($(fmt_lines "$sother")).")
  fi
}

version=""; hasv=0; take=""; hast=0; product=""; hasp=0; name=""; hasn=0; inv=""; cpi=""; reviews=()
[ $# -gt 0 ] || usage
while [ $# -gt 0 ]; do
  [ $# -ge 2 ] || usage
  case $(printf '%s' "$1" | tr '[:upper:]' '[:lower:]') in
    --version|-version) version=$2; hasv=1;;
    --take|-take) take=$2; hast=1;;
    --product|-product) product=$2; hasp=1;;
    --name|-name) name=$2; hasn=1;;
    --inventory|-inventory) inv=$2;;
    --cpinfo|-cpinfo) cpi=$2;;
    --review|-review) reviews+=("$2");;
    *) usage;;
  esac
  shift 2
done
modes=0; [ $hasv = 1 ] && modes=$((modes+1)); [ -n "$inv" ] && modes=$((modes+1)); [ -n "$cpi" ] && modes=$((modes+1))
[ $modes -le 1 ] || usage
if [ $modes = 0 ] && [ ${#reviews[@]} = 0 ]; then usage; fi
if [ $hast = 1 ] && [ $hasv = 0 ]; then usage; fi
if [ -n "$inv" ] && { [ $hasp = 1 ] || [ $hasn = 1 ]; }; then usage; fi
if [ $modes = 0 ] && { [ $hasp = 1 ] || [ $hasn = 1 ]; }; then usage; fi

read_inventory() {
  local f=$1 h row n rows=0 first=1 a b c d
  if ! [ -f "$f" ] || ! [ -r "$f" ]; then die "cannot read inventory file: $f"; fi
  [ -s "$f" ] || die "inventory file is empty: $f"
  while IFS= read -r row || [ -n "$row" ]; do
    row=${row%$'\r'}
    if [ $first = 1 ]; then
      first=0; h=$(trim "$row" | tr '[:upper:]' '[:lower:]')
      [ "$h" = "name,product,version,take" ] || die "CSV header must be: name,product,version,take"
      continue
    fi
    case $(printf '%s' "$row" | sed 's/^[[:space:]]*//') in ''|\#*) continue;; esac
    IFS=',' read -r a b c d _ <<< "$row"
    n=$(trim "${a:-}"); [ -n "$n" ] || n="name not given"
    check_item "$n" "${b:-}" "${c:-}" "${d:-}"; rows=$((rows+1))
  done < "$f"
  [ "$rows" -gt 0 ] || die "inventory has no data rows: $f"
}

if [ -n "$inv" ]; then read_inventory "$inv"
elif [ -n "$cpi" ]; then read_cpinfo "$cpi" "${name:-input}" "${product:-gateway}"
elif [ $hasv = 1 ]; then check_item "${name:-input}" "${product:-gateway}" "$version" "$take"
fi
for f in "${reviews[@]}"; do review_file "$f"; done

echo "check-checkpoint-gaia: offline check for Check Point Gaia CVE-2026-123039 (sk1000362 read 12 Nov 2026 17:08 GMT)."
echo "KEV: CVE-2026-123039 is not in CISA KEV catalog 2026.11.10 (released 10 Nov 2026 19:03 GMT), which predates the CVE (published 12 Nov 2026 14:02 GMT). Absence from KEV says nothing about exploitation; sk1000362 says Check Point observed an active exploit against a couple of dozen customers."
for out in "${lines[@]}"; do printf '%s\n' "$out"; done
for out in "${loglines[@]}"; do printf '%s\n' "$out"; done
n=$((nP+nNA+nV+nF+nU))
if [ $review -gt 0 ]; then res="IOC REVIEW"; rc=5
elif [ $nV -gt 0 ]; then res=VULNERABLE; rc=1
elif [ $nF -gt 0 ]; then res="NO FIX"; rc=2
elif [ $nU -gt 0 ] || [ $n -eq 0 ]; then res=UNKNOWN; rc=3
elif [ $nP -gt 0 ]; then res=PATCHED; rc=0
else res="NOT AFFECTED"; rc=0; fi
echo "RESULT: $res ($n checked: $nP PATCHED, $nNA NOT AFFECTED, $nV VULNERABLE, $nF NO FIX, $nU UNKNOWN; $review IOC review item(s))"
exit $rc
