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

# Dockerized PenTest runner for SASIS.
# Safe-by-default: only non-invasive checks run unless explicitly enabled.
# Usage:
#   ./pruebas/security/run_pentest_tests.sh [PROJECT_PATH] [TARGET_URL]
#
# Defaults:
#   PROJECT_PATH: current working directory
#   TARGET_URL:   http://localhost:8888/sasis/mgmt
#   OUT_DIR:      PROJECT_PATH/pruebas/security/pentest

export LC_ALL=C
export LANG=C

PROJECT_PATH="${1:-$(pwd)}"
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"

if [ -f "$PROJECT_PATH/pruebas/security/pentest.env" ]; then
  # shellcheck disable=SC1091
  source "$PROJECT_PATH/pruebas/security/pentest.env"
fi

TARGET_URL="${2:-${TARGET_URL:-http://localhost:8888/sasis/mgmt}}"
OUT_DIR="${OUT_DIR:-pruebas/security/pentest}"
PENTEST_TIMEOUT="${PENTEST_TIMEOUT:-180}"
RUN_SQLMAP="${RUN_SQLMAP:-0}"
RUN_ZAP_AUTH="${RUN_ZAP_AUTH:-0}"
RUN_SEMGREP="${RUN_SEMGREP:-0}"
SQLMAP_URL="${SQLMAP_URL:-}"
SQLMAP_REQUEST_FILE="${SQLMAP_REQUEST_FILE:-}"

case "$OUT_DIR" in
  /*) ;;
  *) OUT_DIR="$PROJECT_PATH/$OUT_DIR" ;;
esac

RAW_DIR="$OUT_DIR/raw"
RUN_RESULTS="$OUT_DIR/_run-results.json"
mkdir -p "$OUT_DIR" "$RAW_DIR"

RESULTS_COUNT=0

timestamp() { date '+%Y-%m-%d %H:%M:%S'; }

redact() {
  perl -pe 's/((password|passwd|pwd|secret|token|api[_-]?key|authorization|bearer|cookie|session)\s*[:=]\s*)\S+/${1}***REDACTED***/ig'
}

run_with_optional_timeout() {
  if command -v timeout >/dev/null 2>&1; then
    timeout "$PENTEST_TIMEOUT" "$@"
  else
    "$@"
  fi
}

json_escape() {
  python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1])'
}

init_results() {
  printf '[\n' > "$RUN_RESULTS"
  RESULTS_COUNT=0
}

record_result() {
  local file="$1"
  local status="$2"
  local chars="${3:-0}"
  local comma=''
  if [ "$RESULTS_COUNT" -gt 0 ]; then
    comma=','
  fi
  {
    printf '%s  {"file":"%s","status":"%s","chars":%s}\n' \
      "$comma" \
      "$(printf '%s' "$file" | json_escape)" \
      "$(printf '%s' "$status" | json_escape)" \
      "$chars"
  } >> "$RUN_RESULTS"
  RESULTS_COUNT=$((RESULTS_COUNT + 1))
}

finish_results() {
  printf ']\n' >> "$RUN_RESULTS"
}

docker_target_url() {
  python3 - "$TARGET_URL" <<'PY'
from urllib.parse import urlsplit, urlunsplit
import sys
url = sys.argv[1]
p = urlsplit(url)
host = p.hostname or ''
if host in ('localhost', '127.0.0.1'):
    netloc = 'host.docker.internal'
    if p.port:
        netloc += f':{p.port}'
    print(urlunsplit((p.scheme, netloc, p.path, p.query, p.fragment)))
else:
    print(url)
PY
}

DOCKER_TARGET_URL="$(docker_target_url)"

run_and_capture() {
  local file="$1"
  local title="$2"
  local description="$3"
  shift 3
  local path="$OUT_DIR/$file"

  {
    printf '# %s\n\n' "$title"
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '**Target autorizado:** `%s`\n\n' "$TARGET_URL"
    printf '**Target desde Docker:** `%s`\n\n' "$DOCKER_TARGET_URL"
    printf '**Descripción:** %s\n\n' "$description"
    printf '## Comando\n\n```bash\n'
    printf '%q ' "$@"
    printf '\n```\n\n## Salida\n\n'
  } > "$path"

  set +e
  output="$(run_with_optional_timeout "$@" 2>&1 | redact)"
  status=$?
  set -e

  {
    printf '**Estado:** `%s`\n\n' "$status"
    printf '```text\n%s\n```\n' "$output"
  } >> "$path"

  record_result "$file" "$status" "${#output}"
}

write_skip() {
  local file="$1"
  local title="$2"
  local reason="$3"
  local path="$OUT_DIR/$file"
  {
    printf '# %s\n\n' "$title"
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '**Target autorizado:** `%s`\n\n' "$TARGET_URL"
    printf '## Estado\n\nNo ejecutado. %s\n' "$reason"
  } > "$path"
  record_result "$file" 'skipped' '0'
}

write_scope() {
  local path="$OUT_DIR/00-alcance.md"
  {
    printf '# Alcance de PenTest Docker\n\n'
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '**Target autorizado:** `%s`\n\n' "$TARGET_URL"
    printf '**Target usado desde contenedores:** `%s`\n\n' "$DOCKER_TARGET_URL"
    printf '## Reglas de seguridad\n\n'
    printf -- '- El código se monta en solo lectura cuando las herramientas inspeccionan archivos.\n'
    printf -- '- Las pruebas DAST son no destructivas por defecto.\n'
    printf -- '- SQLMap queda bloqueado salvo `RUN_SQLMAP=1` y URL/request explícito.\n'
    printf -- '- ZAP autenticado queda bloqueado hasta definir credenciales, contexto y manejo seguro de sesión.\n'
    printf -- '- Los reportes se escriben en `%s`.\n\n' "$OUT_DIR"
    printf '## Variables relevantes\n\n'
    printf -- '- `TARGET_URL=%s`\n' "$TARGET_URL"
    printf -- '- `PENTEST_TIMEOUT=%s`\n' "$PENTEST_TIMEOUT"
    printf -- '- `RUN_SEMGREP=%s`\n' "$RUN_SEMGREP"
    printf -- '- `RUN_SQLMAP=%s`\n' "$RUN_SQLMAP"
    printf -- '- `RUN_ZAP_AUTH=%s`\n' "$RUN_ZAP_AUTH"
  } > "$path"
  record_result '00-alcance.md' 'created' "$(wc -c < "$path" | tr -d ' ')"
}

write_inventory() {
  local path="$OUT_DIR/01-inventario.md"
  {
    printf '# Inventario para PenTest\n\n'
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '## Archivos y carpetas relevantes\n\n'
    cd "$PROJECT_PATH"
    set +o pipefail
    find . \
      -path './.git' -prune -o \
      -path './pruebas/security/pentest' -prune -o \
      -path './mgmt/vendor' -prune -o \
      -path './api/vendor' -prune -o \
      \( -name 'composer.json' -o -name 'composer.lock' -o -name 'package.json' -o -name 'package-lock.json' -o -name 'Dockerfile' -o -name 'docker-compose.yml' -o -name 'docker-compose.yaml' -o -name '*.php' -o -name '.env' \) \
      -print | sort | sed 's#^./#- `#; s#$#`#' | head -500
    set -o pipefail
  } > "$path"
  record_result '01-inventario.md' 'created' "$(wc -c < "$path" | tr -d ' ')"
}

run_composer_audit() {
  local app_dir="$1"
  local file="$2"
  if [ -f "$PROJECT_PATH/$app_dir/composer.lock" ]; then
    run_and_capture "$file" "Composer audit - $app_dir" "Revisión de vulnerabilidades Composer para $app_dir." \
      docker run --rm -v "$PROJECT_PATH/$app_dir:/app:ro" -w /app composer:2 audit --format=plain
  else
    write_skip "$file" "Composer audit - $app_dir" "No existe $app_dir/composer.lock."
  fi
}

run_trivy() {
  run_and_capture '04-trivy-fs.md' 'Trivy filesystem scan' 'Revisión de vulnerabilidades, secretos y misconfiguraciones sobre el filesystem del proyecto.' \
    docker run --rm -v "$PROJECT_PATH:/workspace:ro" -v "$RAW_DIR:/reports" aquasec/trivy:latest fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --timeout "${PENTEST_TIMEOUT}s" --format table /workspace
}

run_gitleaks() {
  run_and_capture '05-gitleaks.md' 'Gitleaks secrets scan' 'Búsqueda de secretos en el repositorio.' \
    docker run --rm -v "$PROJECT_PATH:/workspace:ro" zricethezav/gitleaks:latest detect --source /workspace --no-git --redact --verbose
}

run_semgrep() {
  if [ "$RUN_SEMGREP" != '1' ]; then
    write_skip '06-semgrep-php.md' 'Semgrep PHP security scan' 'Bloqueado por defecto en este runner porque el escaneo sobre código PHPMaker generado puede ser largo. Ejecutar con RUN_SEMGREP=1 si se quiere SAST aquí, o usar el runner SAST dedicado existente.'
    return 0
  fi

  run_and_capture '06-semgrep-php.md' 'Semgrep PHP security scan' 'SAST acotado a rutas PHP sensibles para evitar escaneos largos sobre código generado/vendor.' \
    docker run --rm -v "$PROJECT_PATH:/src:ro" semgrep/semgrep:latest sh -c 'semgrep scan --config p/php --config p/owasp-top-ten --config p/security-audit --timeout 20 --timeout-threshold 3 --jobs 1 --exclude vendor --exclude node_modules --exclude pruebas --exclude "*.min.js" /src/api/classes /src/api/*.php /src/mgmt/src /src/mgmt/*.php'
}

run_zap_baseline() {
  run_and_capture '07-zap-baseline.md' 'OWASP ZAP baseline' 'DAST no autenticado y no destructivo contra el target autorizado.' \
    docker run --rm -v "$RAW_DIR:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t "$DOCKER_TARGET_URL" -I -m 5 -r zap-baseline.html
}

run_zap_auth() {
  if [ "$RUN_ZAP_AUTH" = '1' ]; then
    write_skip '08-zap-auth.md' 'OWASP ZAP authenticated scan' 'RUN_ZAP_AUTH=1 fue solicitado, pero todavía falta definir contexto, credenciales de prueba y manejo seguro de sesión. Pedir autorización/configuración antes de implementar esta fase.'
  else
    write_skip '08-zap-auth.md' 'OWASP ZAP authenticated scan' 'Fase bloqueada por defecto. Requiere credenciales de prueba, contexto ZAP y autorización explícita.'
  fi
}

run_nuclei() {
  run_and_capture '09-nuclei-basic.md' 'Nuclei basic scan' 'Templates básicos no invasivos para exposición y misconfiguración común.' \
    docker run --rm projectdiscovery/nuclei:latest -u "$DOCKER_TARGET_URL" -tags exposure,misconfig -severity critical,high,medium -rl 3 -timeout 5 -retries 0 -exclude-tags fuzz,bruteforce,dos
}

run_nikto() {
  run_and_capture '10-nikto-basic.md' 'Nikto basic scan' 'Revisión básica no destructiva del servidor web.' \
    docker run --rm ghcr.io/sullo/nikto:latest -host "$DOCKER_TARGET_URL" -nointeractive -Tuning x
}

run_ffuf() {
  local wordlist="$RAW_DIR/ffuf-wordlist.txt"
  cat > "$wordlist" <<'EOF'
backup
old
test
phpinfo.php
.env
vendor
uploads
admin
api
logs
tmp
debug
EOF
  run_and_capture '11-ffuf-discovery.md' 'Discovery básico de rutas' 'Discovery acotado de rutas comunes con wordlist pequeña generada localmente. Usa curl en Docker como fallback reproducible si la imagen de ffuf no está disponible.' \
    docker run --rm --entrypoint sh -v "$RAW_DIR:/data:ro" curlimages/curl:latest -c 'target="$1"; while IFS= read -r word; do url="$target/$word"; result=$(curl -k -L -s -o /dev/null -w "%{http_code} %{size_download}" --max-time 10 "$url"); case "$result" in 404\ *) ;; *) printf "%s %s\n" "$result" "$url" ;; esac; done < /data/ffuf-wordlist.txt' sh "$DOCKER_TARGET_URL"
}

run_testssl() {
  if [[ "$TARGET_URL" == https://* ]]; then
    local host
    host="$(python3 - "$TARGET_URL" <<'PY'
from urllib.parse import urlsplit
import sys
p=urlsplit(sys.argv[1])
print(p.netloc)
PY
)"
    run_and_capture '12-testssl.md' 'testssl.sh' 'Revisión TLS/HTTPS del host autorizado.' \
      docker run --rm drwetter/testssl.sh:latest --fast --warnings batch "$host"
  else
    write_skip '12-testssl.md' 'testssl.sh' 'El target no usa HTTPS; testssl.sh no aplica.'
  fi
}

run_sqlmap() {
  if [ "$RUN_SQLMAP" != '1' ]; then
    write_skip '13-sqlmap.md' 'SQLMap' 'Bloqueado por defecto. Requiere RUN_SQLMAP=1 y SQLMAP_URL o SQLMAP_REQUEST_FILE con autorización explícita.'
    return 0
  fi

  if [ -n "$SQLMAP_URL" ]; then
    run_and_capture '13-sqlmap.md' 'SQLMap' 'Prueba controlada de SQL injection sobre URL explícitamente autorizada.' \
      docker run --rm sqlmapproject/sqlmap:latest -u "$SQLMAP_URL" --batch --risk=1 --level=1 --threads=1 --smart
  elif [ -n "$SQLMAP_REQUEST_FILE" ]; then
    if [ ! -f "$SQLMAP_REQUEST_FILE" ]; then
      write_skip '13-sqlmap.md' 'SQLMap' "SQLMAP_REQUEST_FILE no existe: $SQLMAP_REQUEST_FILE."
    else
      run_and_capture '13-sqlmap.md' 'SQLMap' 'Prueba controlada de SQL injection usando request file autorizado.' \
        docker run --rm -v "$SQLMAP_REQUEST_FILE:/request.txt:ro" sqlmapproject/sqlmap:latest -r /request.txt --batch --risk=1 --level=1 --threads=1 --smart
    fi
  else
    write_skip '13-sqlmap.md' 'SQLMap' 'RUN_SQLMAP=1 está activo, pero falta SQLMAP_URL o SQLMAP_REQUEST_FILE.'
  fi
}

write_summary() {
  local path="$OUT_DIR/14-resumen-ejecutivo.md"
  {
    printf '# Resumen ejecutivo de PenTest Docker\n\n'
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '**Target autorizado:** `%s`\n\n' "$TARGET_URL"
    printf '## Archivos generados\n\n'
    for f in "$OUT_DIR"/*.md; do
      printf -- '- `%s`\n' "$(basename "$f")"
    done
    printf '\n## Lectura recomendada\n\n'
    printf '1. Revisar primero hallazgos críticos/altos en Composer, Trivy, Gitleaks y Semgrep.\n'
    printf '2. Validar manualmente cualquier hallazgo de ZAP, Nuclei, Nikto o ffuf antes de reportarlo al cliente.\n'
    printf '3. Ejecutar SQLMap solo sobre endpoints específicos y autorizados.\n'
    printf '4. Configurar ZAP autenticado cuando existan usuarios de prueba y autorización formal.\n\n'
    printf '## Comando para repetir\n\n```bash\nTARGET_URL=%q %q %q\n```\n' "$TARGET_URL" "$PROJECT_PATH/pruebas/security/run_pentest_tests.sh" "$PROJECT_PATH"
  } > "$path"
  record_result '14-resumen-ejecutivo.md' 'created' "$(wc -c < "$path" | tr -d ' ')"
}

main() {
  init_results
  write_scope
  write_inventory
  run_composer_audit 'api' '02-composer-audit-api.md'
  run_composer_audit 'mgmt' '03-composer-audit-mgmt.md'
  run_trivy
  run_gitleaks
  run_semgrep
  run_zap_baseline
  run_zap_auth
  run_nuclei
  run_nikto
  run_ffuf
  run_testssl
  run_sqlmap
  write_summary
  finish_results
  printf 'PenTest reports written to: %s\n' "$OUT_DIR"
}

main "$@"
