1) Conectarte al servidor real por SSH
Necesitas entrar a:
o
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:
o
Y al ejecutar:
debe salir:
3) En ese servidor ejecutas ESTO (no en el Mac)
y luego:
y:
📌 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/"