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

# Reusable Dockerized security test runner for PHP projects.
# Usage:
#   ./run_security_tests.sh [PROJECT_PATH]
#
# Output:
#   PROJECT_PATH/pruebas/security/*.md

PROJECT_PATH="${1:-$(pwd)}"
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
OUT_DIR="$PROJECT_PATH/pruebas/security"
RUN_RESULTS="$OUT_DIR/_run-results.json"
mkdir -p "$OUT_DIR"

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

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

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:-0}" -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"
}

write_log() {
  local file="$1"
  local title="$2"
  local description="$3"
  local command="$4"
  local path="$OUT_DIR/$file"

  {
    printf '# %s\n\n' "$title"
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '**Descripción:** %s\n\n' "$description"
    printf '## Comando\n\n```bash\n%s\n```\n\n' "$command"
    printf '## Salida\n\n'
  } > "$path"

  set +e
  output="$(cd "$PROJECT_PATH" && eval "$command" 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_inventory() {
  local path="$OUT_DIR/00-inventario.md"
  {
    printf '# Inventario del proyecto\n\n'
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Ruta:** `%s`\n\n' "$PROJECT_PATH"
    printf '## Archivos relevantes detectados\n\n'
    cd "$PROJECT_PATH"
    set +o pipefail
    find . \
      -path './.git' -prune -o \
      -path './pruebas/security' -prune -o \
      -path './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' \) \
      -print | sort | sed 's#^./#- `#; s#$#`#' | head -500
    set -o pipefail
  } > "$path"

  record_result '00-inventario.md' 'created' '0'
}

write_missing_log() {
  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 '## Estado\n\n'
    printf 'No ejecutado. %s\n' "$reason"
  } > "$path"

  record_result "$file" 'skipped' '0'
}

run_dast() {
  local path="$OUT_DIR/07-dast-zap-baseline.md"
  local container="security-php-$(date +%s)"
  local docroot=""

  if [ -d "$PROJECT_PATH/api" ]; then
    docroot="/app/api"
  elif [ -d "$PROJECT_PATH/public" ]; then
    docroot="/app/public"
  else
    docroot="/app"
  fi

  {
    printf '# DAST - OWASP ZAP baseline\n\n'
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '**Objetivo:** `http://127.0.0.1:8088`\n\n'
  } > "$path"

  docker rm -f "$container" >/dev/null 2>&1 || true

  {
    printf '## Arranque de servidor PHP\n\n'
    printf '```bash\n'
    printf 'docker run -d --rm --name %s -p 8088:8000 -v "%s:/app" -w /app php:8.2-cli php -S 0.0.0.0:8000 -t %s\n' "$container" "$PROJECT_PATH" "$docroot"
    printf '```\n\n'
  } >> "$path"

  set +e
  start_output="$(docker run -d --rm --name "$container" -p 8088:8000 -v "$PROJECT_PATH:/app" -w /app php:8.2-cli php -S 0.0.0.0:8000 -t "$docroot" 2>&1 | redact)"
  start_status=$?
  set -e

  {
    printf '**Estado arranque:** `%s`\n\n' "$start_status"
    printf '```text\n%s\n```\n\n' "$start_output"
  } >> "$path"

  if [ "$start_status" -ne 0 ]; then
    printf '> DAST bloqueado: no se pudo iniciar el servidor PHP.\n' >> "$path"
    record_result '07-dast-zap-baseline.md' 'created' "$(wc -c < "$path" | tr -d ' ')"
    return 0
  fi

  sleep 5

  write_health_and_zap "$path" "$container"
  record_result '07-dast-zap-baseline.md' 'created' "$(wc -c < "$path" | tr -d ' ')"
}

write_health_and_zap() {
  local path="$1"
  local container="$2"

  set +e
  health_output="$(curl -i --max-time 10 http://127.0.0.1:8088/ 2>&1 | redact)"
  health_status=$?
  set -e

  {
    printf '## Health check local\n\n'
    printf '```bash\ncurl -i --max-time 10 http://127.0.0.1:8088/\n```\n\n'
    printf '**Estado:** `%s`\n\n' "$health_status"
    printf '```text\n%s\n```\n\n' "$health_output"
  } >> "$path"

  if [ "$health_status" -eq 0 ]; then
    local zap_cmd='docker run --rm --network host ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://127.0.0.1:8088 -I'
    {
      printf '## ZAP baseline\n\n'
      printf '```bash\n%s\n```\n\n' "$zap_cmd"
    } >> "$path"
    set +e
    zap_output="$(cd "$PROJECT_PATH" && eval "$zap_cmd" 2>&1 | redact)"
    zap_status=$?
    set -e
    {
      printf '**Estado:** `%s`\n\n' "$zap_status"
      printf '```text\n%s\n```\n\n' "$zap_output"
    } >> "$path"

    if [ "$zap_status" -ne 0 ] && [ "$zap_status" -ne 1 ] && [ "$zap_status" -ne 2 ]; then
      local zap_cmd2='docker run --rm ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:8088 -I'
      {
        printf '## ZAP baseline fallback Docker Desktop\n\n'
        printf '```bash\n%s\n```\n\n' "$zap_cmd2"
      } >> "$path"
      set +e
      zap_output2="$(cd "$PROJECT_PATH" && eval "$zap_cmd2" 2>&1 | redact)"
      zap_status2=$?
      set -e
      {
        printf '**Estado:** `%s`\n\n' "$zap_status2"
        printf '```text\n%s\n```\n\n' "$zap_output2"
      } >> "$path"
    fi
  else
    printf '> DAST bloqueado: el servidor PHP no respondió al health check.\n\n' >> "$path"
  fi

  {
    printf '## Logs del servidor PHP\n\n```text\n'
    docker logs "$container" 2>&1 | redact || true
    printf '\n```\n'
  } >> "$path"
  docker rm -f "$container" >/dev/null 2>&1 || true
}

write_summary() {
  local path="$OUT_DIR/08-resumen-completo.md"
  {
    printf '# Resumen completo de pruebas de seguridad\n\n'
    printf '**Fecha:** %s\n\n' "$(timestamp)"
    printf '**Proyecto:** `%s`\n\n' "$PROJECT_PATH"
    printf '## Alcance\n\n'
    printf -- '- SAST: Semgrep con reglas PHP y security-audit.\n'
    printf -- '- SCA: Composer audit, npm audit si existe package-lock, y Trivy FS.\n'
    printf -- '- DAST: OWASP ZAP baseline contra servidor PHP dockerizado cuando el proyecto puede exponerse localmente.\n\n'
    printf '## Archivos de evidencia\n\n'
    for f in "$OUT_DIR"/*.md; do
      printf -- '- `%s`\n' "$(basename "$f")"
    done
    printf '\n## Hallazgos principales detectados automáticamente\n\n'
    printf '### SAST\n\n'
    grep -E 'Code Findings|echoed-request|printed-request|timeout error|Blocking' "$OUT_DIR/02-sast-semgrep.md" 2>/dev/null | head -80 || true
    printf '\n\n### SCA / dependencias\n\n'
    grep -E 'Found [0-9]+ security|Total:|CRITICAL|HIGH|CVE-' "$OUT_DIR"/03-*.md "$OUT_DIR"/04-*.md "$OUT_DIR/06-sca-trivy-fs.md" 2>/dev/null | head -120 || true
    printf '\n\n### DAST\n\n'
    grep -E 'FAIL-NEW|WARN-NEW|PASS:|Missing|Header|Policy|X-Powered-By' "$OUT_DIR/07-dast-zap-baseline.md" 2>/dev/null | head -80 || true
    printf '\n\n## Recomendaciones prioritarias\n\n'
    printf '1. Actualizar dependencias Composer con CVE críticas/altas, especialmente `firebase/php-jwt`, `phpoffice/phpspreadsheet`, `symfony/mime` y paquetes marcados por Composer/Trivy.\n'
    printf '2. Revisar los hallazgos SAST de salida directa de datos y aplicar codificación contextual (`htmlentities`, JSON seguro y headers correctos) según el tipo de respuesta.\n'
    printf '3. Agregar headers HTTP de seguridad: `Content-Security-Policy`, `X-Content-Type-Options`, `X-Frame-Options` o `frame-ancestors`, `Permissions-Policy` y ocultar `X-Powered-By`.\n'
    printf '4. Repetir DAST contra el ambiente real autenticado/staging; esta corrida usa servidor PHP embebido y no reemplaza una prueba funcional completa.\n'
    printf '5. Revisar paquetes abandonados reportados por Composer y definir reemplazo o mitigación.\n\n'
    printf '## Comando para repetir\n\n'
    printf '```bash\n%s/run_security_tests.sh "%s"\n```\n' "$OUT_DIR" "$PROJECT_PATH"
  } > "$path"
}

main() {
  if ! docker version >/dev/null 2>&1; then
    echo 'Docker no está disponible. Abortando.' >&2
    exit 1
  fi

  init_results
  write_inventory
  write_log '01-docker.md' 'Verificación Docker' 'Estado de Docker y contenedores activos.' "docker version && docker ps --format 'table {{.Names}}\\t{{.Image}}\\t{{.Ports}}'"
  write_log '02-sast-semgrep.md' 'SAST - Semgrep' 'Análisis estático con Semgrep, excluyendo vendor y reportes.' 'docker run --rm -v "$PWD:/src" semgrep/semgrep semgrep scan --config p/php --config p/security-audit --timeout 60 --timeout-threshold 10 --exclude vendor --exclude pruebas/security /src'

  if [ -f "$PROJECT_PATH/mgmt/composer.lock" ]; then
    write_log '03-sca-composer-audit-mgmt.md' 'SCA - Composer audit mgmt' 'Auditoría de dependencias Composer del módulo mgmt.' 'docker run --rm -v "$PWD:/app" -w /app/mgmt composer:2 composer audit --locked --format=plain'
  else
    write_missing_log '03-sca-composer-audit-mgmt.md' 'SCA - Composer audit mgmt' 'No se encontró `mgmt/composer.lock`.'
  fi
  if [ -f "$PROJECT_PATH/api/composer.lock" ]; then
    write_log '04-sca-composer-audit-api.md' 'SCA - Composer audit api' 'Auditoría de dependencias Composer del módulo api.' 'docker run --rm -v "$PWD:/app" -w /app/api composer:2 composer audit --locked --format=plain'
  else
    write_missing_log '04-sca-composer-audit-api.md' 'SCA - Composer audit api' 'No se encontró `api/composer.lock`.'
  fi
  if [ -f "$PROJECT_PATH/mgmt/package-lock.json" ]; then
    write_log '05-sca-npm-audit-mgmt.md' 'SCA - npm audit mgmt' 'Auditoría npm del módulo mgmt.' 'docker run --rm -v "$PWD:/app" -w /app/mgmt node:20 npm audit --omit=dev'
  else
    write_missing_log '05-sca-npm-audit-mgmt.md' 'SCA - npm audit mgmt' 'No se encontró `mgmt/package-lock.json`.'
  fi

  write_log '06-sca-trivy-fs.md' 'SCA/Secret/Misconfig - Trivy FS' 'Escaneo de vulnerabilidades, secretos y misconfiguraciones con Trivy.' 'docker run --rm -v "$PWD:/src" aquasec/trivy:latest fs --scanners vuln,secret,misconfig --skip-dirs /src/mgmt/vendor --skip-dirs /src/api/phpseclib --skip-dirs /src/pruebas/security /src'
  run_dast
  finish_results
  write_summary

  echo "Reportes generados en: $OUT_DIR"
}

main "$@"
