Auditoria servidor

1) Conectarte al servidor real por SSH

Necesitas entrar a:

ssh root@TU_IP_DEL_SERVIDOR

o

ssh usuario@TU_IP_DEL_SERVIDOR

Si no sabes la IP:

  • Mírala en:

    • Panel de IONOS

    • Correo de alta del servidor

    • Plesk → Tools & Settings → IP Addresses


2) Cuando estés dentro, lo notarás porque:

El prompt cambiará a algo como:

root@vps123:~#

o

admin@server:~$

Y al ejecutar:

uname -a

debe salir:

Linux ...

3) En ese servidor ejecutas ESTO (no en el Mac)

sudo ss -lntup

y luego:

sudo sshd -T | egrep -i 'permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries'

y:

last -n 20

📌 Conclusión actual

Lo que me has mostrado:

❌ No permite auditar:

  • puertos del servidor

  • accesos root

  • bloqueos

  • firewall

  • Plesk

✅ Solo muestra:

  • apps de tu ordenador personal.


---

sudo bash -lc '
echo "===== SERVER AUDIT ====="
date
echo

echo "## OS"
uname -a
cat /etc/os-release 2>/dev/null | sed -n "1,6p" || true
echo

echo "## LISTEN PORTS (ss)"
ss -lntup
echo

echo "## LISTEN PORTS (lsof)"
command -v lsof >/dev/null && lsof -i -P -n | grep LISTEN || echo "(lsof no instalado)"
echo

echo "## SSH CONFIG (effective)"
command -v sshd >/dev/null && sshd -T 2>/dev/null | egrep -i "permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries|permitempty" || true
echo

echo "## RECENT LOGINS"
last -a | head -n 50
echo

echo "## SSH AUTH EVENTS (last 200)"
if [ -f /var/log/auth.log ]; then
  grep -E "sshd.*(Failed password|Accepted|Invalid user)" /var/log/auth.log | tail -n 200
elif [ -f /var/log/secure ]; then
  grep -E "sshd.*(Failed password|Accepted|Invalid user)" /var/log/secure | tail -n 200
else
  echo "(no auth.log ni secure)"
fi
echo

echo "## FAIL2BAN"
command -v fail2ban-client >/dev/null && fail2ban-client status || echo "(fail2ban no activo)"
echo

echo "## FIREWALL"
ufw status verbose 2>/dev/null || true
firewall-cmd --list-all 2>/dev/null || true
nft list ruleset 2>/dev/null | head -n 120 || true
iptables -S 2>/dev/null || true
echo

echo "## PLESK"
command -v plesk >/dev/null && plesk version || echo "(plesk cli no disponible en PATH)"
'

--------

sudo bash -lc '
set -e

echo "===== SERVER AUDIT (Linux/Plesk) ====="
date
echo

echo "## OS"
uname -a
cat /etc/os-release 2>/dev/null | sed -n "1,12p" || true
echo

echo "## LISTEN PORTS (ss/netstat)"
if command -v ss >/dev/null 2>&1; then
  ss -lntup
else
  netstat -lntup 2>/dev/null || true
fi
echo

echo "## TOP EXPOSED SERVICES (from LISTEN)"
if command -v ss >/dev/null 2>&1; then
  ss -lntup | awk "NR>1{print \$1,\$5,\$7}" | sed "s/users:(//;s/))//" | head -n 80
fi
echo

echo "## SSHD EFFECTIVE CONFIG"
if command -v sshd >/dev/null 2>&1; then
  sshd -T 2>/dev/null | egrep -i "port |permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries|allowusers|allowgroups" || true
fi
echo

echo "## RECENT SSH LOGINS (last)"
last -n 30 || true
echo

echo "## SSH AUTH EVENTS (Accepted/Failed)"
if [ -f /var/log/auth.log ]; then
  tail -n 400 /var/log/auth.log | egrep -i "sshd.*(Accepted|Failed|Invalid user)" | tail -n 120 || true
elif [ -f /var/log/secure ]; then
  tail -n 400 /var/log/secure | egrep -i "sshd.*(Accepted|Failed|Invalid user)" | tail -n 120 || true
else
  journalctl -u ssh -u sshd --since "7 days ago" 2>/dev/null | egrep -i "(Accepted|Failed|Invalid user)" | tail -n 120 || true
fi
echo

echo "## FAIL2BAN"
if command -v fail2ban-client >/dev/null 2>&1; then
  fail2ban-client status || true
  fail2ban-client status sshd 2>/dev/null || true
else
  echo "(fail2ban no instalado)"
fi
echo

echo "## FIREWALL (ufw/firewalld/nft/iptables)"
ufw status verbose 2>/dev/null || true
firewall-cmd --list-all 2>/dev/null || true
nft list ruleset 2>/dev/null | head -n 160 || true
iptables -S 2>/dev/null || true
echo

echo "## PLESK"
if command -v plesk >/dev/null 2>&1; then
  plesk version || true
  # puertos típicos
  echo "Plesk ports hint: 8443 (panel), 8880 (http panel antiguo), 8447 (updates)"
else
  echo "(plesk cli no disponible)"
fi
'

-------

bash -lc '
D="${1:?Uso: externo dominio.com}"

echo "===== VALIDACIÓN EXTERNA: $D ====="

echo; echo "## IP pública"
dig +short A "$D"
dig +short AAAA "$D"

echo; echo "## PTR (reverse)"
for ip in $(dig +short A "$D"); do
  dig +short -x "$ip"
done

echo; echo "## PUERTOS CRÍTICOS (escaneo ligero)"
for p in 21 22 25 53 80 110 143 443 465 587 993 995 3306 3389 8443; do
  nc -z -w 2 "$D" "$p" && echo "PUERTO $p: ABIERTO" || echo "PUERTO $p: cerrado/no responde"
done

echo; echo "## TLS WEB"
echo | openssl s_client -connect "$D:443" -servername "$D" 2>/dev/null | openssl x509 -noout -dates -issuer -subject

echo; echo "## CABECERAS"
curl -sI "https://$D" | egrep -i "server|hsts|x-|content-security|referrer|permissions"

echo; echo "## CORREO MX + TLS"
dig +short MX "$D" | sort -n

for mx in $(dig +short MX "$D" | awk "{print \$2}"); do
  echo "- TLS en $mx:25"
  echo | openssl s_client -starttls smtp -connect "$mx:25" 2>/dev/null | grep -i "Verify return code"
done
' -- psicologiadeapie.com

-----

# Cambia por tu IP
IP=217.160.158.183

# Bing + certificados
curl -s "https://api.hackertarget.com/reverseiplookup/?q=$IP"

Certificados TLS (fuente TOP)
curl -s "https://crt.sh/?q=$IP&output=json" | jq -r '.[].name_value' | sort -u

----

curl -s https://crt.sh/\?q=%25.psicologiadeapie.com\&output=json | \
jq -r '.[].name_value' | sort -u

-------

for d in $(cat lista-dominios.txt); do
  dig +short A $d | grep -q "$IP" && echo "APUNTA → $d"
done

---

plesk bin site --list
plesk bin subscription --list
plesk bin domain --list

---

# Apache
grep -R "ServerName" /etc/apache2/ | grep -v "#"

Nginx
grep -R "server_name" /etc/nginx/ | grep -v "#"

---

https://hackertarget.com/ 

---

1) CAPA DE IDENTIDAD Y REPUTACIÓN

1.1 Reputación de IP

  • Listas negras (Spamhaus, SORBS, Barracuda)

  • historial de envíos

  • PTR alineado

  • vecinos de IP (bad neighborhood)

1.2 Reputación de dominio

  • edad del dominio

  • histórico de malware

  • certificados previos

  • cambios de NS frecuentes

1.3 Antispoofing

  • SPF alineación relaxed/strict

  • DKIM múltiples selectores

  • DMARC política real vs monitor

  • BIMI con VMC

  • MTA-STS enforce

  • TLS-RPT reportes


2) CAPA DNS AVANZADA

  • DNSSEC validación completa

  • coherencia NS vs glue records

  • TTL coherentes

  • split-brain DNS

  • subdominios huérfanos

  • wildcards peligrosos

  • CAA restrictivo

  • zone transfer (AXFR) expuesto

  • enumeración por NSEC/NSEC3


3) CAPA WEB

3.1 Superficie HTTP

  • redirecciones 301/302

  • mixed content

  • HSTS preload

  • OCSP stapling

  • TLS 1.0/1.1 habilitados

  • cipher suites débiles

3.2 Cabeceras

  • CSP

  • X-Frame

  • Permissions-Policy

  • Referrer-Policy

  • X-Content-Type

3.3 Aplicación

  • CMS + versión

  • plugins vulnerables

  • usuarios admin

  • wp-json expuesto

  • xmlrpc

  • REST abierto

  • rutas de backup


4) CAPA DE CORREO

  • open relay

  • STARTTLS real

  • DANE

  • ARC

  • reputación MX

  • límites de rate

  • IMAP/POP expuestos

  • credenciales débiles


5) CAPA SERVIDOR

5.1 Sistema

  • parches

  • kernel

  • servicios innecesarios

  • usuarios privilegiados

  • sudoers

5.2 SSH

  • root

  • password

  • MFA

  • llaves

  • algoritmos

5.3 Aislamiento

  • permisos vhosts

  • PHP open_basedir

  • containers

  • jaulas


6) CAPA DE RED

  • puertos expuestos

  • WAF

  • rate limit

  • geo-bloqueo

  • DDoS

  • fail2ban

  • logs centralizados


7) CAPA PLESK

  • extensiones inseguras

  • backups

  • actualizaciones

  • aislamiento por suscripción

  • mail local vs externo

  • permisos FTP


8) CAPA DE DATOS

  • backups cifrados

  • retención

  • RPO/RTO

  • copias offsite

  • integridad DB


9) CAPA LEGAL / GOBIERNO

  • RGPD

  • cookies

  • logs acceso

  • políticas

  • LSSI


10) CAPA DE CONTINUIDAD

  • monitorización

  • alertas

  • rotación certificados

  • pruebas restauración

  • DRP


🔥 LO MÁS POTENTE QUE PUEDES OFRECER

1) Informe 360 por dominio

  • DNS

  • Web

  • Correo

  • Servidor

  • Reputación

  • Cumplimiento

2) Matriz de riesgo

Área Impacto Probabilidad Acción

3) Plan de remediación

  • P0 hoy

  • P1 semana

  • P2 mejora

-----

cibersoft-audit360.sh

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

# ============================================================
# CIBERSOFT AUDIT 360
# - Mode EXTERNAL: DNS/Web/Mail/Public reputation
# - Mode SERVER: OS/SSH/Network/Plesk/Data
# - Mode FULL: both
#
# Usage:
#   ./cibersoft-audit360.sh dominio.com --mode external
#   ./cibersoft-audit360.sh dominio.com --mode server
#   ./cibersoft-audit360.sh dominio.com --mode full
#
# Optional env:
#   VT_API_KEY=...            (VirusTotal - domain/url checks) [optional]
#   ST_API_KEY=...            (SecurityTrails - DNS history)   [optional]
#   WPSCAN_API_TOKEN=...      (WPScan - vuln plugins/themes)   [optional]
#   SHODAN_API_KEY=...        (Shodan - exposure info)         [optional]
# ============================================================

D="${1:-}"
MODE="external"
OUT_DIR="./audit-${D:-unknown}-$(date +%Y%m%d-%H%M%S)"
QUIET=0

if [[ -z "${D}" ]]; then
  echo "Uso: $0 dominio.com [--mode external|server|full] [--out DIR] [--quiet]"
  exit 1
fi

shift || true
while [[ $# -gt 0 ]]; do
  case "$1" in
    --mode) MODE="${2:-external}"; shift 2;;
    --out) OUT_DIR="${2:-$OUT_DIR}"; shift 2;;
    --quiet) QUIET=1; shift;;
    *) echo "Arg desconocido: $1"; exit 1;;
  esac
done

mkdir -p "$OUT_DIR"
REPORT_MD="${OUT_DIR}/report.md"
REPORT_JSON="${OUT_DIR}/report.json"
RAW_DIR="${OUT_DIR}/raw"
mkdir -p "$RAW_DIR"

log(){ [[ "$QUIET" -eq 1 ]] || echo "$*"; }
md(){ echo "$*" >> "$REPORT_MD"; }
raw_save(){ local name="$1"; shift; printf "%s\n" "$*" > "${RAW_DIR}/${name}"; }

have(){ command -v "$1" >/dev/null 2>&1; }

dig1(){ dig +time=2 +tries=1 +short "$@"; }
digf(){ dig +time=2 +tries=1 "$@"; } # full

# portable TCP connect (macOS/Linux) using bash /dev/tcp if available; else python
tcp_check() {
  local host="$1" port="$2" timeout_s="${3:-2}"
  if (exec 3<>"/dev/tcp/${host}/${port}") >/dev/null 2>&1; then
    exec 3>&- 3<&-
    return 0
  fi
  if have python3; then
    python3 - <<PY >/dev/null 2>&1
import socket
import sys
host="${host}"; port=int("${port}"); t=float("${timeout_s}")
s=socket.socket()
s.settimeout(t)
try:
  s.connect((host,port))
  s.close()
  sys.exit(0)
except Exception:
  sys.exit(1)
PY
    return $?
  fi
  return 2
}

json_escape() {
  # minimal JSON string escape
  sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}

# ----------------------------
# Report scaffolding
# ----------------------------
: > "$REPORT_MD"
md "# Auditoría 360 — ${D}"
md ""
md "**Fecha:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')  "
md "**Modo:** ${MODE}  "
md ""

# ----------------------------
# Findings accumulator
# ----------------------------
FINDINGS_TSV="${OUT_DIR}/findings.tsv"
: > "$FINDINGS_TSV"
add_finding(){
  # area, severity(P0/P1/P2/P3/INFO), title, evidence, action
  printf "%s\t%s\t%s\t%s\t%s\n" "$1" "$2" "$3" "$4" "$5" >> "$FINDINGS_TSV"
}
sev_rank(){
  case "$1" in
    P0) echo 0;;
    P1) echo 1;;
    P2) echo 2;;
    P3) echo 3;;
    INFO) echo 4;;
    *) echo 9;;
  esac
}

# ============================================================
# 1) CAPA IDENTIDAD Y REPUTACIÓN (EXTERNAL)
# ============================================================

external_identity_reputation(){
  md "## 1) Identidad y reputación"
  md ""

  # 1.1 IP, PTR, ASN (ASN via whois if possible)
  local A AAAA IPS PTRS
  A="$(dig1 A "$D" || true)"
  AAAA="$(dig1 AAAA "$D" || true)"
  IPS="$(printf "%s\n%s\n" "$A" "$AAAA" | sed '/^$/d' | sort -u)"
  raw_save "ip.txt" "$IPS"

  md "### 1.1 IP / PTR / proveedor"
  md ""
  md "**A/AAAA:**"
  md '```'
  echo "$IPS" >> "$REPORT_MD"
  md '```'
  md ""

  PTRS=""
  if [[ -n "$A" ]]; then
    while read -r ip; do
      [[ -z "$ip" ]] && continue
      local ptr
      ptr="$(dig1 -x "$ip" | head -n 1 || true)"
      PTRS+="$ip -> $ptr"$'\n'
      if [[ -n "$ptr" ]] && [[ "$ptr" != *"$D"* ]]; then
        add_finding "Identidad" "P2" "PTR no alineado con el dominio" "$ip -> $ptr" "Si el servidor envía correo, solicitar PTR alineado (ej: mail.${D}). Si no envía, documentar como aceptado."
      fi
    done <<< "$A"
  fi
  raw_save "ptr.txt" "$PTRS"

  md "**PTR (reverse):**"
  md '```'
  echo "${PTRS:-"(sin PTR o sin A)"}" >> "$REPORT_MD"
  md '```'
  md ""

  # ASN heuristic (whois)
  if have whois && [[ -n "$A" ]]; then
    local asn
    asn="$(whois "$(echo "$A" | head -n 1)" 2>/dev/null | egrep -i 'origin|originas|aut-num|descr|netname' | head -n 20 || true)"
    raw_save "whois_ip.txt" "$asn"
    md "**WHOIS IP (extracto):**"
    md '```'
    echo "$asn" >> "$REPORT_MD"
    md '```'
    md ""
  else
    md "> Nota: WHOIS IP no disponible (falta `whois` o no hay A)."
    md ""
  fi

  # 1.1 Blacklists (DNSBL) - best-effort; some providers block queries.
  md "### 1.1 Listas negras (DNSBL) — best-effort"
  md ""
  md "> Importante: consultas DNSBL pueden estar limitadas/bloqueadas por algunos resolutores; resultados 'NXDOMAIN' no siempre significan limpio."
  md ""
  if [[ -n "$A" ]]; then
    local ip rev lookup
    ip="$(echo "$A" | head -n 1)"
    rev="$(echo "$ip" | awk -F. '{print $4"."$3"."$2"."$1}')"

    # Common DNSBL zones (use responsibly)
    # Spamhaus ZEN has usage policies and may block heavy automated use.
    declare -a bls=("zen.spamhaus.org" "dnsbl.sorbs.net" "b.barracudacentral.org")
    md "**IP evaluada:** $ip"
    md ""
    md "| Lista | Resultado |"
    md "|---|---|"
    for bl in "${bls[@]}"; do
      lookup="$(dig1 A "${rev}.${bl}" || true)"
      if [[ -n "$lookup" ]]; then
        md "| $bl | LISTADA ($lookup) |"
        add_finding "Reputación IP" "P1" "IP listada en DNSBL" "${ip} en ${bl} -> ${lookup}" "Revisar envíos, cerrar servicios de correo locales si no se usan, limpiar malware, solicitar delist según proveedor."
      else
        md "| $bl | no listada (o no comprobable) |"
      fi
    done
    md ""
  else
    md "> Sin A: no se puede evaluar DNSBL de IP."
    md ""
  fi

  # 1.2 Domain age via whois (best-effort)
  md "### 1.2 Reputación de dominio (edad / señales públicas)"
  md ""
  if have whois; then
    local w creation updated registrar
    w="$(whois "$D" 2>/dev/null || true)"
    creation="$(echo "$w" | egrep -i 'Creation Date:|Created On:|created:|Registered On:' | head -n 1 || true)"
    updated="$(echo "$w" | egrep -i 'Updated Date:|Updated On:|changed:' | head -n 1 || true)"
    registrar="$(echo "$w" | egrep -i 'Registrar:|Sponsoring Registrar:' | head -n 1 || true)"
    raw_save "whois_domain.txt" "$creation"$'\n'"$updated"$'\n'"$registrar"
    md '```'
    echo "$creation" >> "$REPORT_MD"
    echo "$updated" >> "$REPORT_MD"
    echo "$registrar" >> "$REPORT_MD"
    md '```'
    md ""
  else
    md "> WHOIS no disponible (instala `whois`)."
    md ""
    add_finding "Dominio" "INFO" "No se pudo calcular edad del dominio" "Falta comando whois" "Instalar whois o usar RDAP API (opcional)."
  fi

  # 1.2 Certificates history via crt.sh (optional jq)
  md "**Certificados previos (crt.sh):**"
  md ""
  if have curl; then
    if have jq; then
      local crt
      crt="$(curl -fsS "https://crt.sh/?q=${D}&output=json" 2>/dev/null | jq -r '.[].not_before + " | " + .[].name_value' 2>/dev/null | head -n 40 || true)"
      # crt.sh JSON can be large; store raw limited
      raw_save "crtsh_sample.txt" "$crt"
      if [[ -n "$crt" ]]; then
        md '```'
        echo "$crt" >> "$REPORT_MD"
        md '```'
      else
        md "> No se pudo obtener muestra crt.sh (vacío o bloqueado)."
      fi
    else
      md "> `jq` no está instalado; crt.sh se omite o quedaría sin parseo."
      add_finding "Dominio" "INFO" "crt.sh no parseado" "Falta jq" "Instalar jq para extraer historial de certificados."
    fi
  else
    md "> Falta `curl`."
  fi
  md ""

  # 1.3 Antispoofing summary
  md "### 1.3 Antispoofing (SPF/DKIM/DMARC/BIMI/MTA-STS/TLS-RPT)"
  md ""

  local SPF DMARC BIMI MTA TLSRPT
  SPF="$(dig1 TXT "$D" | grep -i 'v=spf1' || true)"
  DMARC="$(dig1 TXT "_dmarc.$D" || true)"
  BIMI="$(dig1 TXT "default._bimi.$D" || true)"
  MTA="$(dig1 TXT "_mta-sts.$D" || true)"
  TLSRPT="$(dig1 TXT "_smtp._tls.$D" || true)"

  raw_save "spf.txt" "$SPF"
  raw_save "dmarc.txt" "$DMARC"

  md "**SPF:**"
  md '```'
  echo "${SPF:-"(sin SPF)"}" >> "$REPORT_MD"
  md '```'
  md ""

  if [[ -z "$SPF" ]]; then
    add_finding "Correo" "P0" "SPF ausente" "No hay TXT v=spf1" "Publicar SPF acorde al proveedor real (Google/M365/etc)."
  else
    if echo "$SPF" | grep -q "~all"; then
      add_finding "Correo" "P2" "SPF en softfail (~all)" "$SPF" "Cuando esté estable, endurecer a -all."
    fi
  fi

  md "**DMARC:**"
  md '```'
  echo "${DMARC:-"(sin DMARC)"}" >> "$REPORT_MD"
  md '```'
  md ""
  if [[ -z "$DMARC" ]]; then
    add_finding "Correo" "P1" "DMARC ausente" "No hay _dmarc TXT" "Publicar DMARC p=none (monitor) y subir a quarantine/reject cuando DKIM/SPF estén listos."
  else
    if echo "$DMARC" | grep -qi "p=none"; then
      add_finding "Correo" "P2" "DMARC en monitor (p=none)" "$DMARC" "Plan: p=quarantine → p=reject cuando no haya falsos positivos."
    fi
    if ! echo "$DMARC" | grep -qi "rua="; then
      add_finding "Correo" "P3" "DMARC sin rua" "$DMARC" "Añadir rua=mailto: para reportes agregados."
    fi
  fi

  md "**DKIM (selectores heurísticos):**"
  md ""
  local selectors="google selector1 selector2 default k1 k2"
  md "| Selector | Presente |"
  md "|---|---|"
  local any_dkim=0
  for s in $selectors; do
    local v
    v="$(dig1 TXT "${s}._domainkey.${D}" || true)"
    if [[ -n "$v" ]]; then
      any_dkim=1
      md "| ${s} | sí |"
      raw_save "dkim_${s}.txt" "$v"
    else
      md "| ${s} | no |"
    fi
  done
  md ""
  if [[ "$any_dkim" -eq 0 ]]; then
    add_finding "Correo" "P1" "DKIM no detectado (selectores comunes)" "No se encontró DKIM en selectores típicos" "Generar DKIM en proveedor real y publicar TXT selector._domainkey."
  fi

  md "**BIMI:**"
  md '```'
  echo "${BIMI:-"(no encontrado)"}" >> "$REPORT_MD"
  md '```'
  md ""

  md "**MTA-STS:**"
  md '```'
  echo "${MTA:-"(no encontrado)"}" >> "$REPORT_MD"
  md '```'
  md ""

  md "**TLS-RPT:**"
  md '```'
  echo "${TLSRPT:-"(no encontrado)"}" >> "$REPORT_MD"
  md '```'
  md ""

  if [[ -z "$MTA" ]]; then
    add_finding "Correo" "P3" "MTA-STS no detectado" "No hay _mta-sts TXT" "Opcional: implementar MTA-STS + política en https://mta-sts.dominio/.well-known/mta-sts.txt."
  fi
  if [[ -z "$TLSRPT" ]]; then
    add_finding "Correo" "P3" "TLS-RPT no detectado" "No hay _smtp._tls TXT" "Opcional: habilitar TLS-RPT para visibilidad de fallos TLS."
  fi
}

# ============================================================
# 2) CAPA DNS AVANZADA (EXTERNAL)
# ============================================================

external_dns_advanced(){
  md "## 2) DNS avanzado"
  md ""

  local NS SOA DS DNSKEY NSEC
  NS="$(dig1 NS "$D" | sort || true)"
  SOA="$(dig1 SOA "$D" || true)"
  DS="$(digf +dnssec DS "$D" 2>/dev/null | awk '/IN[[:space:]]+DS/{print}' || true)"
  DNSKEY="$(digf +dnssec DNSKEY "$D" 2>/dev/null | awk '/IN[[:space:]]+DNSKEY/{print}' || true)"

  raw_save "ns.txt" "$NS"
  raw_save "soa.txt" "$SOA"
  raw_save "ds_full.txt" "$DS"
  raw_save "dnskey_full.txt" "$DNSKEY"

  md "**NS:**"
  md '```'
  echo "$NS" >> "$REPORT_MD"
  md '```'
  md ""

  md "**SOA:**"
  md '```'
  echo "$SOA" >> "$REPORT_MD"
  md '```'
  md ""

  md "### 2.1 DNSSEC (DS/DNSKEY)"
  md ""
  if [[ -z "$DS" && -z "$DNSKEY" ]]; then
    md "- Estado: **probablemente sin DNSSEC** (no se detecta DS ni DNSKEY)."
    add_finding "DNS" "P3" "DNSSEC no detectado" "DS y DNSKEY vacíos" "Activar DNSSEC en el proveedor autoritativo y publicar DS en registrador si aplica."
  elif [[ -z "$DS" && -n "$DNSKEY" ]]; then
    md "- Estado: **DNSKEY presente pero DS ausente** (cadena incompleta)."
    add_finding "DNS" "P1" "DNSSEC incompleto (sin DS)" "DNSKEY presente, DS ausente" "Publicar DS en el registrador o activar DNSSEC desde panel del DNS autoritativo."
  elif [[ -n "$DS" && -z "$DNSKEY" ]]; then
    md "- Estado: **DS presente pero DNSKEY ausente** (posible rotura crítica)."
    add_finding "DNS" "P0" "DNSSEC roto (DS sin DNSKEY)" "DS presente, DNSKEY ausente" "Corregir inmediatamente: restaurar DNSKEY correcto o retirar DS del registrador."
  else
    md "- Estado: **DS y DNSKEY presentes** (probablemente activo)."
  fi
  md ""

  md "### 2.2 CAA"
  md ""
  local CAA
  CAA="$(dig1 CAA "$D" || true)"
  md '```'
  echo "${CAA:-"(sin CAA)"}" >> "$REPORT_MD"
  md '```'
  md ""
  if [[ -z "$CAA" ]]; then
    add_finding "DNS" "P3" "CAA ausente" "No hay CAA" "Recomendable: añadir CAA limitando CAs (p.ej. letsencrypt.org)."
  fi

  md "### 2.3 AXFR (transferencia de zona) — prueba controlada"
  md ""
  md "> Se prueba si algún NS permite AXFR. Normalmente debe estar **cerrado**."
  md ""
  if [[ -n "$NS" ]]; then
    md "| NS | AXFR |"
    md "|---|---|"
    while read -r ns; do
      [[ -z "$ns" ]] && continue
      # AXFR can hang; use python timeout if possible
      local axfr_ok="no comprobable"
      if have python3; then
        python3 - <<PY > "${RAW_DIR}/axfr_${ns}.txt" 2>/dev/null
import subprocess, sys
ns="${ns}".strip().rstrip('.')
dom="${D}".strip().rstrip('.')
cmd=["dig","+time=2","+tries=1","AXFR","@"+ns,dom]
try:
  out=subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=4).decode("utf-8","ignore")
  print(out)
except Exception as e:
  print(str(e))
PY
        if grep -q "Transfer failed\|refused\|connection timed out\|no servers could be reached" "${RAW_DIR}/axfr_${ns}.txt" 2>/dev/null; then
          axfr_ok="cerrado"
        elif grep -q "IN[[:space:]]\+SOA" "${RAW_DIR}/axfr_${ns}.txt" 2>/dev/null; then
          axfr_ok="ABIERTO"
          add_finding "DNS" "P0" "AXFR abierto (exposición total de zona)" "NS ${ns} permite AXFR" "Cerrar AXFR en el DNS autoritativo (solo allowlist de secundarios autorizados)."
        else
          axfr_ok="cerrado/no concluyente"
        fi
      else
        axfr_ok="no (sin python3)"
      fi
      md "| ${ns} | ${axfr_ok} |"
    done <<< "$NS"
    md ""
  else
    md "> Sin NS, no se prueba AXFR."
    md ""
  fi

  md "### 2.4 NSEC/NSEC3 (enumeración)"
  md ""
  # NSEC check: query DNSKEY with +dnssec and see if NSEC returned for NXDOMAIN on random label
  local rnd="zz$(date +%s)$(printf %04d $RANDOM)"
  local nsec_out
  nsec_out="$(digf +dnssec "${rnd}.${D}" A 2>/dev/null | egrep -i 'NSEC3|NSEC' || true)"
  raw_save "nsec_check.txt" "$nsec_out"
  if echo "$nsec_out" | grep -qi "NSEC3"; then
    md "- Se observa **NSEC3** (reduce enumeración trivial, pero no la elimina)."
  elif echo "$nsec_out" | grep -qi "NSEC"; then
    md "- Se observa **NSEC** (posible enumeración de zona si DNSSEC está activo)."
    add_finding "DNS" "P2" "NSEC visible (posible enumeración)" "$nsec_out" "Evaluar NSEC3 con parámetros adecuados si es requisito de seguridad."
  else
    md "- No se detecta NSEC/NSEC3 en esta prueba (no concluyente)."
  fi
  md ""

  md "### 2.5 Wildcard DNS (riesgo de subdominios fantasma)"
  md ""
  local wc
  wc="$(dig1 A "${rnd}.${D}" || true)"
  if [[ -n "$wc" ]]; then
    md "- Parece haber **wildcard A** para subdominios inexistentes → $wc"
    add_finding "DNS" "P2" "Wildcard DNS activo" "${rnd}.${D} -> ${wc}" "Revisar si es intencional. Puede facilitar phishing y confundir auditorías."
  else
    md "- No se detecta wildcard A en esta prueba."
  fi
  md ""
}

# ============================================================
# 3) CAPA WEB (EXTERNAL)
# ============================================================

external_web(){
  md "## 3) Web"
  md ""

  if ! have curl; then
    md "> Falta `curl`. Web checks omitidos."
    add_finding "Web" "INFO" "Sin curl" "No se pudo revisar HTTP/HTTPS" "Instalar curl o ejecutar en entorno que lo tenga."
    md ""
    return
  fi

  md "### 3.1 Superficie HTTP/TLS"
  md ""

  # redirects
  local http_hdr https_hdr
  http_hdr="$(curl -sI "http://${D}" 2>/dev/null || true)"
  https_hdr="$(curl -sI "https://${D}" 2>/dev/null || true)"
  raw_save "http_headers.txt" "$http_hdr"
  raw_save "https_headers.txt" "$https_hdr"

  md "**HTTP (primeras líneas):**"
  md '```'
  echo "$http_hdr" | head -n 15 >> "$REPORT_MD"
  md '```'
  md ""

  md "**HTTPS (primeras líneas):**"
  md '```'
  echo "$https_hdr" | head -n 25 >> "$REPORT_MD"
  md '```'
  md ""

  # HSTS
  if echo "$https_hdr" | grep -qi '^strict-transport-security:'; then
    md "- HSTS: **presente**"
  else
    md "- HSTS: **ausente**"
    add_finding "Web" "P2" "HSTS ausente" "No header Strict-Transport-Security" "Añadir HSTS (con cuidado) tras validar HTTPS estable."
  fi

  # TLS versions quick check
  if have openssl; then
    md ""
    md "**TLS (cert + fechas):**"
    md '```'
    echo | openssl s_client -connect "${D}:443" -servername "${D}" 2>/dev/null | openssl x509 -noout -dates -issuer -subject 2>/dev/null >> "$REPORT_MD" || true
    md '```'
    md ""

    md "**TLS versiones (best-effort):**"
    md ""
    md "| Versión | Resultado |"
    md "|---|---|"
    for v in -tls1 -tls1_1 -tls1_2 -tls1_3; do
      if echo | openssl s_client $v -connect "${D}:443" -servername "${D}" 2>/dev/null | grep -q "BEGIN CERTIFICATE"; then
        md "| ${v#-} | OK |"
        if [[ "$v" == "-tls1" || "$v" == "-tls1_1" ]]; then
          add_finding "Web" "P1" "TLS legado habilitado" "Acepta ${v#-}" "Deshabilitar TLS1.0/1.1; dejar TLS1.2/1.3."
        fi
      else
        md "| ${v#-} | no |"
      fi
    done
    md ""

    # OCSP stapling (best-effort)
    md "**OCSP stapling (best-effort):**"
    local ocsp
    ocsp="$(echo | openssl s_client -connect "${D}:443" -servername "${D}" -status 2>/dev/null | egrep -i 'OCSP response|No OCSP response' | head -n 5 || true)"
    md '```'
    echo "${ocsp:-"(no concluyente)"}" >> "$REPORT_MD"
    md '```'
    md ""
  else
    md "> openssl no disponible; TLS checks omitidos."
    md ""
  fi

  # Mixed content (simple HTML scan)
  md "### 3.1 Mixed content (heurístico)"
  md ""
  local html
  html="$(curl -fsSL "https://${D}" 2>/dev/null | head -n 400 || true)"
  raw_save "homepage_head.txt" "$html"
  if echo "$html" | grep -qi 'http://'; then
    md "- Se detecta **referencia http://** en HTML (posible mixed content)."
    add_finding "Web" "P2" "Posible mixed content" "HTML contiene http://" "Revisar recursos (scripts/css/img) y forzar https."
  else
    md "- No se detecta http:// en el primer bloque HTML (no concluyente)."
  fi
  md ""

  md "### 3.2 Cabeceras de seguridad"
  md ""
  local headers
  headers="$(echo "$https_hdr" | egrep -i '^(strict-transport-security:|content-security-policy:|x-frame-options:|x-content-type-options:|referrer-policy:|permissions-policy:|server:|x-powered-by:)' || true)"
  md '```'
  echo "${headers:-"(no se detectaron cabeceras relevantes)"}" >> "$REPORT_MD"
  md '```'
  md ""

  # Actionable header gaps
  if ! echo "$https_hdr" | grep -qi '^content-security-policy:'; then
    add_finding "Web" "P2" "CSP ausente" "No Content-Security-Policy" "Definir CSP (al menos report-only) para mitigar XSS."
  fi
  if ! echo "$https_hdr" | grep -qi '^x-frame-options:'; then
    add_finding "Web" "P3" "X-Frame-Options ausente" "No X-Frame-Options" "Añadir X-Frame-Options o frame-ancestors en CSP."
  fi
  if ! echo "$https_hdr" | grep -qi '^x-content-type-options:'; then
    add_finding "Web" "P3" "X-Content-Type-Options ausente" "No X-Content-Type-Options" "Añadir X-Content-Type-Options: nosniff."
  fi

  md "### 3.3 Aplicación (detección básica)"
  md ""
  # Basic WP detection
  local wp=0
  if curl -fsS "https://${D}/wp-json/" >/dev/null 2>&1; then wp=1; fi
  if curl -fsS "https://${D}/xmlrpc.php" >/dev/null 2>&1; then
    # xmlrpc often returns 405 or message; treat as present if status != 404
    local code
    code="$(curl -s -o /dev/null -w "%{http_code}" "https://${D}/xmlrpc.php" || true)"
    if [[ "$code" != "404" ]]; then
      md "- xmlrpc.php: **presente** (HTTP $code)"
      add_finding "WordPress" "P2" "XML-RPC expuesto" "xmlrpc.php responde (HTTP $code)" "Si no se usa, bloquear xmlrpc. Si se usa, rate limit + WAF."
    fi
  fi

  if [[ "$wp" -eq 1 ]]; then
    md "- CMS: **WordPress probable** (wp-json accesible)"
    add_finding "WordPress" "INFO" "WordPress detectado" "wp-json accesible" "Aplicar auditoría WP (usuarios, plugins, hardening)."
  else
    md "- CMS: no se identifica como WordPress (heurístico)."
  fi
  md ""

  # Backup artifacts (common)
  md "**Rutas de backup comunes (heurístico):**"
  md ""
  local paths=("backup.zip" "backup.tar.gz" "site.zip" "wp-content.zip" ".env" ".git/config" "wp-config.php~" "wp-config.php.bak")
  local hit=0
  for p in "${paths[@]}"; do
    local c
    c="$(curl -s -o /dev/null -w "%{http_code}" "https://${D}/${p}" || true)"
    if [[ "$c" == "200" || "$c" == "206" ]]; then
      hit=1
      md "- **ENCONTRADO:** /${p} (HTTP ${c})"
      add_finding "Web" "P0" "Artefacto sensible expuesto" "/${p} HTTP ${c}" "Retirar inmediatamente del webroot, rotar credenciales, revisar fuga de datos."
    fi
  done
  if [[ "$hit" -eq 0 ]]; then
    md "- No se detectaron artefactos en la lista corta (no concluyente)."
  fi
  md ""
}

# ============================================================
# 4) CAPA CORREO (EXTERNAL + limited)
# ============================================================

external_mail(){
  md "## 4) Correo"
  md ""

  local MX
  MX="$(dig1 MX "$D" | sort -n || true)"
  raw_save "mx.txt" "$MX"

  md "**MX:**"
  md '```'
  echo "${MX:-"(sin MX)"}" >> "$REPORT_MD"
  md '```'
  md ""

  # STARTTLS check for MX:25 (safe handshake only)
  if have openssl && [[ -n "$MX" ]]; then
    md "### 4.1 STARTTLS (MX:25) — handshake"
    md ""
    md "| MX | STARTTLS |"
    md "|---|---|"
    while read -r prio host; do
      [[ -z "$host" ]] && continue
      local v
      v="$(echo | openssl s_client -starttls smtp -connect "${host}:25" 2>/dev/null | egrep -i 'Verify return code|TLSv|Cipher' | head -n 2 || true)"
      if [[ -n "$v" ]]; then
        md "| ${host} | OK |"
      else
        md "| ${host} | no concluyente |"
        add_finding "Correo" "P2" "STARTTLS no verificable en MX" "$host:25" "Revisar si proveedor bloquea handshake desde tu IP o si SMTP no ofrece TLS."
      fi
    done <<< "$MX"
    md ""
  else
    md "> STARTTLS no comprobado (falta openssl o no hay MX)."
    md ""
  fi

  # DANE (TLSA records) check
  md "### 4.2 DANE (TLSA) — opcional"
  md ""
  local tlsa
  tlsa="$(dig1 TLSA "_25._tcp.${D}" || true)"
  if [[ -n "$tlsa" ]]; then
    md "- TLSA detectado: **sí**"
    md '```'
    echo "$tlsa" >> "$REPORT_MD"
    md '```'
  else
    md "- TLSA detectado: **no**"
  fi
  md ""

  # Open relay: do NOT attempt relay. We only flag if MX is local IP and SMTP ports open at domain.
  md "### 4.3 Open relay — evaluación segura (sin envío)"
  md ""
  # If MX points to local domain and port 25 open on D, warn.
  if echo "$MX" | awk '{print $2}' | grep -qiE "(mail\.${D}\.|${D}\.)"; then
    add_finding "Correo" "P2" "SMTP local posible" "MX apunta a host bajo el dominio" "Si gestionas correo local, auditar configuración (relay, auth, rate limits). Si no, migrar a proveedor (Google/M365) y cerrar puertos."
    md "- MX parece gestionado localmente: **recomendado** auditar relay y rate limits en el servidor."
  else
    md "- MX parece de proveedor externo (p.ej. Google/M365): no se prueba relay."
  fi
  md ""
}

# ============================================================
# 5) CAPA SERVIDOR / 6) RED / 7) PLESK / 8) DATOS (SERVER MODE)
# ============================================================

server_audit(){
  md "## 5) Servidor (modo SERVER)"
  md ""
  md "> Esta sección solo es válida si el script se ejecuta **dentro del servidor** (Linux)."
  md ""

  local os
  os="$(uname -s || true)"
  if [[ "$os" != "Linux" ]]; then
    add_finding "Servidor" "INFO" "No es Linux" "uname -s = ${os}" "Ejecutar en el servidor Linux para auditar sistema/SSH/Plesk."
    md "- Detectado: **${os}** → se omite auditoría de servidor Linux."
    md ""
    return
  fi

  md "### 5.1 Sistema"
  md ""
  md "**Kernel/OS:**"
  md '```'
  uname -a >> "$REPORT_MD" || true
  cat /etc/os-release 2>/dev/null | head -n 20 >> "$REPORT_MD" || true
  md '```'
  md ""

  # patches: detect package manager
  if have apt-get; then
    md "**Actualizaciones pendientes (apt):**"
    md '```'
    (apt-get -s upgrade 2>/dev/null | egrep -i '^Inst|^Conf|^[0-9]+ upgraded|security' | head -n 80) || true >> "$REPORT_MD"
    md '```'
    md ""
  elif have dnf; then
    md "**Actualizaciones pendientes (dnf):**"
    md '```'
    (dnf -q check-update 2>/dev/null | head -n 80) || true >> "$REPORT_MD"
    md '```'
    md ""
  elif have yum; then
    md "**Actualizaciones pendientes (yum):**"
    md '```'
    (yum -q check-update 2>/dev/null | head -n 80) || true >> "$REPORT_MD"
    md '```'
    md ""
  else
    md "> No se detecta gestor de paquetes (apt/dnf/yum)."
    md ""
  fi

  md "### 5.2 SSH"
  md ""
  if have sshd; then
    md "**sshd -T (claves relevantes):**"
    md '```'
    sshd -T 2>/dev/null | egrep -i 'port |permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries|kexalgorithms|macs|ciphers|allowusers|allowgroups' >> "$REPORT_MD" || true
    md '```'
    md ""

    local prl pwa
    prl="$(sshd -T 2>/dev/null | awk '/permitrootlogin/{print $2}' | head -n 1 || true)"
    pwa="$(sshd -T 2>/dev/null | awk '/passwordauthentication/{print $2}' | head -n 1 || true)"

    if [[ "${prl:-}" != "no" ]]; then
      add_finding "SSH" "P0" "PermitRootLogin no deshabilitado" "permitrootlogin=${prl:-unknown}" "Configurar PermitRootLogin no; crear usuario admin con sudo; usar llaves."
    fi
    if [[ "${pwa:-}" != "no" ]]; then
      add_finding "SSH" "P0" "PasswordAuthentication no deshabilitado" "passwordauthentication=${pwa:-unknown}" "Deshabilitar PasswordAuthentication; usar llaves; habilitar Fail2Ban."
    fi
  else
    md "> sshd no disponible o no accesible."
    md ""
  fi

  md "### 5.3 Servicios y puertos"
  md ""
  if have ss; then
    md '```'
    ss -lntup >> "$REPORT_MD" || true
    md '```'
    md ""
  else
    md "> Falta `ss` (instalar iproute2)."
    add_finding "Servidor" "P2" "Falta ss/iproute2" "No se pudo listar puertos con ss" "Instalar iproute2 para auditoría y troubleshooting."
    md ""
  fi

  # Users privileged
  md "### 5.4 Usuarios privilegiados / sudoers"
  md ""
  md "**Usuarios con UID 0:**"
  md '```'
  awk -F: '($3==0){print $1":"$3":"$7}' /etc/passwd 2>/dev/null >> "$REPORT_MD" || true
  md '```'
  md ""
  if [[ -f /etc/sudoers ]]; then
    md "**sudoers (extracto no sensible):**"
    md '```'
    egrep -v '^\s*#|^\s*$' /etc/sudoers 2>/dev/null | head -n 80 >> "$REPORT_MD" || true
    md '```'
    md ""
  fi

  # 6) Network / firewall / fail2ban
  md "## 6) Red (modo SERVER)"
  md ""
  md "### 6.1 Firewall / Fail2Ban"
  md ""
  md "**Fail2Ban:**"
  md '```'
  if have fail2ban-client; then
    fail2ban-client status >> "$REPORT_MD" 2>/dev/null || true
  else
    echo "(fail2ban no instalado)" >> "$REPORT_MD"
    add_finding "Red" "P1" "Fail2Ban no instalado" "fail2ban-client no encontrado" "Instalar y activar jail sshd + plesk (si aplica)."
  fi
  md '```'
  md ""

  md "**Firewall (best-effort):**"
  md '```'
  if have ufw; then ufw status verbose >> "$REPORT_MD" 2>/dev/null || true; fi
  if have firewall-cmd; then firewall-cmd --list-all >> "$REPORT_MD" 2>/dev/null || true; fi
  if have nft; then nft list ruleset >> "$REPORT_MD" 2>/dev/null | head -n 200 || true; fi
  if have iptables; then iptables -S >> "$REPORT_MD" 2>/dev/null || true; fi
  md '```'
  md ""

  # 7) Plesk
  md "## 7) Plesk (modo SERVER)"
  md ""
  if have plesk; then
    md "**Versión Plesk:**"
    md '```'
    plesk version >> "$REPORT_MD" 2>/dev/null || true
    md '```'
    md ""

    md "**Dominios (plesk):**"
    md '```'
    plesk bin domain --list >> "$REPORT_MD" 2>/dev/null || true
    md '```'
    md ""

    md "**Suscripciones:**"
    md '```'
    plesk bin subscription --list >> "$REPORT_MD" 2>/dev/null || true
    md '```'
    md ""

    # Mail local vs externo (heurístico)
    md "**Correo local vs externo (heurístico):**"
    md "- Revisar en Plesk si Mail service está habilitado por dominio."
    md ""
  else
    md "- Plesk CLI no disponible."
    add_finding "Plesk" "INFO" "Plesk CLI no disponible" "plesk no encontrado" "Ejecutar en el servidor con Plesk o añadir PATH."
    md ""
  fi

  # 8) Datos (backups)
  md "## 8) Datos (modo SERVER)"
  md ""
  md "- Evaluar backups: cifrado, retención, offsite, pruebas de restauración."
  md "- Este script no puede inferir RPO/RTO sin tu política; sí puede listar configuraciones si existen rutas."
  md ""
}

# ============================================================
# 9) LEGAL / 10) CONTINUIDAD (EXTERNAL heuristics)
# ============================================================

external_legal_continuity(){
  md "## 9) Legal / gobierno (heurístico)"
  md ""
  if have curl; then
    local home
    home="$(curl -fsSL "https://${D}" 2>/dev/null | head -n 2000 || true)"
    if echo "$home" | grep -qiE 'cookie|cookies|consent|gdpr|rgpd'; then
      md "- Señal: se detectan menciones a **cookies/RGPD** en HTML (no concluyente)."
    else
      md "- No se detectan menciones obvias a cookies/RGPD en HTML inicial (no concluyente)."
      add_finding "Legal" "P3" "Cookies/RGPD no verificable" "No match en HTML inicial" "Revisar banner de cookies, política de privacidad, aviso legal (LSSI) manualmente."
    fi
  else
    md "- Sin curl, no se revisa legal."
  fi
  md ""

  md "## 10) Continuidad (heurístico)"
  md ""
  md "- Verificar: monitorización, alertas, rotación SSL, pruebas de restore, DRP."
  md "- Recomendación: integrar checks con UptimeRobot/BetterUptime y backups offsite con verificación."
  md ""
}

# ============================================================
# Exposure scan (EXTERNAL) - ports
# ============================================================

external_ports(){
  md "## 6) Red (exposición externa)"
  md ""
  md "### 6.1 Puertos expuestos (ligero)"
  md ""
  local ports=(21 22 25 53 80 110 143 443 465 587 993 995 3306 3389 8443)
  md "| Puerto | Estado |"
  md "|---:|---|"
  for p in "${ports[@]}"; do
    if tcp_check "$D" "$p" 2; then
      md "| $p | ABIERTO |"
      case "$p" in
        21) add_finding "Red" "P0" "FTP (21) expuesto" "Puerto 21 abierto" "Desactivar FTP; usar SFTP; o restringir por IP y forzar FTPS.";;
        22) add_finding "Red" "P1" "SSH (22) expuesto" "Puerto 22 abierto" "Hardening SSH: root off, password off, llaves, Fail2Ban, allowlist IP.";;
        8443) add_finding "Red" "P0" "Panel Plesk (8443) expuesto" "Puerto 8443 abierto" "Restringir por IP + 2FA + Fail2Ban; considerar VPN.";;
        25|465|587|993|995)
          add_finding "Red" "P2" "Servicios de correo expuestos en servidor" "Puerto $p abierto" "Si usas Google/M365, desactivar mail local y cerrar puertos. Si usas mail local, auditar relay/rate/TLS."
          ;;
      esac
    else
      md "| $p | cerrado/no concluyente |"
    fi
  done
  md ""
}

# ============================================================
# Risk matrix + Action plan
# ============================================================

render_risk_matrix_and_plan(){
  md "## 🔥 Informe 360 (resumen ejecutivo)"
  md ""

  md "### Matriz de riesgos (hallazgos)"
  md ""
  md "| Área | Severidad | Hallazgo | Evidencia | Acción |"
  md "|---|---|---|---|---|"

  # sort by severity rank
  sort -t$'\t' -k2,2 "${FINDINGS_TSV}" | while IFS=$'\t' read -r area sev title evidence action; do
    # sanitize pipes
    evidence="${evidence//|/\\|}"
    action="${action//|/\\|}"
    md "| ${area} | ${sev} | ${title} | ${evidence} | ${action} |"
  done
  md ""

  # Plan P0/P1/P2
  md "### Plan de remediación"
  md ""
  for s in P0 P1 P2 P3 INFO; do
    md "#### ${s}"
    md ""
    awk -F'\t' -v S="$s" '$2==S{print "- ["$1"] **"$3"** — "$5" (Evidencia: "$4")"}' "$FINDINGS_TSV" >> "$REPORT_MD" || true
    md ""
  done
}

# ============================================================
# JSON export (simple)
# ============================================================

export_json(){
  {
    echo '{'
    echo "  \"domain\": \"${D}\","
    echo "  \"mode\": \"${MODE}\","
    echo "  \"generated_utc\": \"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\","
    echo '  "findings": ['
    local first=1
    while IFS=$'\t' read -r area sev title evidence action; do
      [[ -z "$area" ]] && continue
      if [[ $first -eq 0 ]]; then echo ','; fi
      first=0
      printf '    {"area":"%s","severity":"%s","title":"%s","evidence":"%s","action":"%s"}' \
        "$(printf "%s" "$area" | json_escape)" \
        "$(printf "%s" "$sev" | json_escape)" \
        "$(printf "%s" "$title" | json_escape)" \
        "$(printf "%s" "$evidence" | json_escape)" \
        "$(printf "%s" "$action" | json_escape)"
    done < "$FINDINGS_TSV"
    echo
    echo '  ]'
    echo '}'
  } > "$REPORT_JSON"
}

# ============================================================
# RUN
# ============================================================

case "$MODE" in
  external)
    external_identity_reputation
    external_dns_advanced
    external_ports
    external_web
    external_mail
    external_legal_continuity
    ;;
  server)
    server_audit
    ;;
  full)
    external_identity_reputation
    external_dns_advanced
    external_ports
    external_web
    external_mail
    external_legal_continuity
    server_audit
    ;;
  *)
    echo "Modo inválido: $MODE"
    exit 1
    ;;
esac

render_risk_matrix_and_plan
export_json

md ""
md "---"
md "**Archivos generados:**"
md "- Reporte Markdown: \`${REPORT_MD}\`"
md "- Reporte JSON: \`${REPORT_JSON}\`"
md "- Evidencias raw: \`${RAW_DIR}/\`"
md ""

log "OK. Reporte generado en:"
log "  $REPORT_MD"
log "  $REPORT_JSON"
log "  $RAW_DIR/"

¿Le ha resultado útil este artículo?