diff --git a/lynis/README.md b/lynis/README.md
new file mode 100644
index 0000000..1bd9e34
--- /dev/null
+++ b/lynis/README.md
@@ -0,0 +1,309 @@
+# 🔒 Lynis Security Report Generator
+
+Sistema automatizado para generar y enviar reportes de seguridad de Lynis en múltiples formatos con diseño moderno y profesional.
+
+## 📋 Descripción
+
+Este proyecto automatiza la ejecución de auditorías de seguridad con Lynis y genera reportes en tres formatos:
+
+- **TXT**: Salida original sin formato (referencia técnica completa)
+- **HTML**: Reporte web con diseño moderno, compacto y en columnas paralelas
+- **PDF**: Versión imprimible del reporte HTML (soporta múltiples métodos de generación)
+
+Los reportes se envían automáticamente por correo electrónico con todos los formatos adjuntos.
+
+## ✨ Características
+
+### Diseño HTML Moderno
+- 🎨 Interfaz elegante con gradientes profesionales
+- 📊 Visualización clara de métricas de seguridad en 3 columnas paralelas
+- 🎯 Código de colores según nivel de riesgo
+- 📱 Diseño responsive y optimizado para impresión
+- ⚡ Efectos visuales y animaciones sutiles
+- 📏 Diseño compacto y centrado para mejor aprovechamiento del espacio
+
+### Componentes del Reporte
+- **Hardening Index**: Índice de endurecimiento del sistema (0-100)
+- **Test Results**: Resumen de tests realizados, warnings y sugerencias
+- **Security Components**: Estado de firewall, IDS/IPS y antimalware
+- **Warnings**: Problemas críticos que requieren atención inmediata
+- **Suggestions**: Recomendaciones para mejorar la seguridad
+- **Full Report**: Salida completa de Lynis en formato raw
+
+## 🚀 Instalación
+
+### Requisitos del Sistema
+
+```bash
+# Lynis (herramienta de auditoría)
+sudo apt install lynis
+
+# Python 3.x
+sudo apt install python3 python3-pip python3-venv
+```
+
+### Dependencias Python
+
+```bash
+# Crear entorno virtual (recomendado)
+python3 -m venv venv
+source venv/bin/activate
+
+# Instalar dependencias
+pip install -r requirements.txt
+```
+
+### Herramientas para Generación de PDF
+
+El script intenta varios métodos automáticamente (en orden de prioridad). Instala al menos uno:
+
+**Opción 1: wkhtmltopdf (Recomendado - Mejor calidad y soporte CSS)**
+```bash
+sudo apt install wkhtmltopdf
+```
+
+**Opción 2: WeasyPrint (Alternativa Python con buen soporte CSS)**
+```bash
+pip install weasyprint
+```
+
+**Opción 3: Chromium Headless (Alternativa con motor de navegador)**
+```bash
+sudo apt install chromium-browser
+# O en sistemas basados en Debian/Ubuntu
+sudo apt install chromium
+```
+
+El script intentará usar wkhtmltopdf primero, luego weasyprint, y finalmente chromium/chrome headless. Todas estas opciones generan PDFs con formato visual completo (no texto plano).
+
+## ⚙️ Configuración
+
+Edita el archivo `config.py` con tus parámetros:
+
+```python
+# Comando Lynis
+LYNIS_CMD = ["/usr/bin/lynis", "audit", "system", "--no-colors"]
+
+# Directorio de salida
+BASE_DIR = "/opt/lynis-report"
+
+# Configuración SMTP
+SMTP_HOST = "smtp.example.com"
+SMTP_PORT = 587
+SMTP_USER = "tu-usuario"
+
+# La contraseña se lee desde variable de entorno
+SMTP_PASS = os.environ.get("SENDGRID_API_KEY", "")
+
+# Configuración de correo
+FROM_ADDR = "security-reports@example.com"
+TO_ADDRS = [
+ "admin@example.com",
+]
+
+SUBJECT_PREFIX = "[SECURITY] Lynis report"
+```
+
+### Variables de Entorno
+
+```bash
+# Configura la API key/password del SMTP
+export SENDGRID_API_KEY="tu-api-key-aqui"
+
+# O si usas Gmail/otro proveedor
+export SMTP_PASSWORD="tu-password-aqui"
+```
+
+## 📖 Uso
+
+### Ejecución Manual
+
+```bash
+# Activar entorno virtual
+source venv/bin/activate
+
+# Ejecutar el script
+python3 lynis_report.py
+```
+
+### Ejecución Automática (Cron)
+
+Para ejecutar el reporte semanalmente:
+
+```bash
+# Editar crontab
+crontab -e
+
+# Añadir línea (ejemplo: cada lunes a las 2 AM)
+0 2 * * 1 /home/usuario/Security-Reports/lynis/venv/bin/python3 /home/usuario/Security-Reports/lynis/lynis_report.py
+```
+
+## 📁 Estructura del Proyecto
+
+```
+lynis/
+├── config.py # Configuración del sistema
+├── lynis_report.py # Script principal
+├── requirements.txt # Dependencias Python
+├── templates/
+│ └── report.html.j2 # Plantilla HTML del reporte
+├── venv/ # Entorno virtual (opcional)
+└── README.md # Este archivo
+```
+
+## 📊 Archivos Temporales
+
+Los archivos se generan temporalmente en el directorio configurado (`BASE_DIR`) durante la ejecución:
+
+```
+/opt/lynis-report/
+├── lynis-hostname-20251204-0200.txt # Salida original (temporal)
+├── lynis-hostname-20251204-0200.html # Reporte HTML (temporal)
+└── lynis-hostname-20251204-0200.pdf # Reporte PDF (temporal)
+```
+
+**Nota**: Todos los archivos se eliminan automáticamente después de enviar el correo electrónico. Los reportes solo se conservan en los adjuntos del email.
+
+## 📧 Correo Electrónico
+
+El email incluye:
+- **Cuerpo HTML**: Versión completa del reporte con diseño elegante
+- **Adjunto TXT**: Salida original de Lynis
+- **Adjunto PDF**: Reporte formateado para impresión/distribución
+
+## 🎨 Personalización
+
+### Modificar Diseño
+
+Edita `templates/report.html.j2`:
+
+**Cambiar colores:**
+```css
+/* Fondo principal */
+background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+
+/* Tarjetas */
+background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+```
+
+**Ajustar columnas del resumen:**
+```css
+.summary-grid {
+ grid-template-columns: repeat(3, 1fr); /* 3 columnas iguales */
+ gap: 20px;
+}
+```
+
+### Añadir Secciones
+
+Modifica la plantilla Jinja2 en `templates/report.html.j2` y actualiza la función `generate_html()` en `lynis_report.py`.
+
+## 🔧 Solución de Problemas
+
+### PDF no se genera
+
+```bash
+# Verifica qué herramientas están instaladas
+which wkhtmltopdf
+python3 -c "import weasyprint" 2>/dev/null && echo "weasyprint: installed"
+which chromium || which chromium-browser || which google-chrome
+
+# Instala al menos una opción (que genere PDFs con formato visual)
+sudo apt install wkhtmltopdf # Opción 1 (mejor)
+pip install weasyprint # Opción 2
+sudo apt install chromium-browser # Opción 3
+```
+
+El script intentará las 3 opciones automáticamente. Todas generan PDFs visuales renderizados.
+
+### Error de permisos con Lynis
+
+```bash
+# Lynis requiere permisos de root/sudo
+sudo python3 lynis_report.py
+
+# O configura permisos específicos
+sudo visudo
+# Añade: usuario ALL=(ALL) NOPASSWD: /usr/bin/lynis
+```
+
+### Error de SMTP
+
+```bash
+# Verifica la variable de entorno
+echo $SENDGRID_API_KEY
+
+# Prueba conexión SMTP
+telnet smtp.example.com 587
+```
+
+## 📝 Ejemplo de Salida
+
+### Hardening Index
+```
+76/100 (Medium)
+```
+
+### Componentes de Seguridad
+```
+🔥 Firewall: ✅
+🛡️ IDS/IPS: ✅
+🦠 Malware Scanner: ⚠️
+```
+
+### Tests
+```
+260 Tests realizados
+1 Warning
+31 Suggestions
+```
+
+## 🔐 Seguridad
+
+- **Credenciales**: Nunca incluyas contraseñas en `config.py`, usa variables de entorno
+- **Permisos**: Protege los archivos de configuración con permisos restrictivos
+- **Logs**: Revisa regularmente `/var/log/lynis.log`
+- **SMTP**: Usa conexiones cifradas (STARTTLS/SSL)
+
+```bash
+# Asegurar permisos
+chmod 600 config.py
+chmod 700 lynis_report.py
+```
+
+## 🤝 Contribución
+
+Para contribuir al proyecto:
+
+1. Haz fork del repositorio
+2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`)
+3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`)
+4. Push a la rama (`git push origin feature/AmazingFeature`)
+5. Abre un Pull Request
+
+## 📄 Licencia
+
+Este proyecto está bajo tu propia licencia. Lynis es software libre bajo GPLv3.
+
+## 🔗 Enlaces Útiles
+
+- [Lynis Official](https://cisofy.com/lynis/)
+- [Lynis GitHub](https://github.com/CISOfy/lynis)
+- [Jinja2 Documentation](https://jinja.palletsprojects.com/)
+- [wkhtmltopdf](https://wkhtmltopdf.org/)
+- [WeasyPrint](https://weasyprint.org/)
+
+## 👤 Autor
+
+Eduardo - Sistema de Reportes de Seguridad
+
+## 📞 Soporte
+
+Para problemas o preguntas:
+- Revisa la sección de solución de problemas
+- Consulta los logs en `/var/log/lynis.log`
+- Verifica la configuración SMTP y las credenciales
+
+---
+
+**Nota**: Este script debe ejecutarse con privilegios suficientes para que Lynis pueda auditar el sistema completamente.
diff --git a/lynis/config.py b/lynis/config.py
new file mode 100644
index 0000000..81f2a21
--- /dev/null
+++ b/lynis/config.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+import os
+
+# Comando Lynis (ajusta si en tu sistema está en otra ruta)
+LYNIS_CMD = ["/usr/bin/lynis", "audit", "system", "--no-colors"]
+
+# Carpeta base para guardar informes
+BASE_DIR = "/opt/lynis-report"
+
+# =========================
+# Configuración SMTP genérica
+# =========================
+# Ejemplo para proveedor SMTP cualquiera (SendGrid, Gmail, etc.)
+# - SMTP_HOST: host SMTP de tu proveedor
+# - SMTP_PORT: normalmente 587 (STARTTLS) o 465 (SSL)
+# - SMTP_USER: usuario SMTP (para SendGrid suele ser "apikey")
+# - SMTP_PASS: se lee desde variable de entorno (no lo metas en el código)
+
+SMTP_HOST = "smtp.example.com"
+SMTP_PORT = 587
+SMTP_USER = "apikey" # o tu usuario SMTP real
+
+# Carga la contraseña / API key desde variable de entorno
+# (por ejemplo: SENDGRID_API_KEY, SMTP_PASSWORD, etc.)
+SMTP_PASS = os.environ.get("SENDGRID_API_KEY", "")
+
+# Dirección remitente de los informes
+FROM_ADDR = "security-reports@example.com"
+
+# Destinatarios de los informes (puedes poner varios)
+TO_ADDRS = [
+ "admin@example.com",
+ # "otro-destinatario@example.org",
+]
+
+# Prefijo del asunto del correo
+SUBJECT_PREFIX = "[SECURITY] Lynis report"
+
diff --git a/lynis/lynis_report.py b/lynis/lynis_report.py
new file mode 100644
index 0000000..70ba8ce
--- /dev/null
+++ b/lynis/lynis_report.py
@@ -0,0 +1,384 @@
+#!/usr/bin/env python3
+import os
+import subprocess
+import datetime
+import html
+import shutil
+import smtplib
+import re
+from email.message import EmailMessage
+from typing import Optional
+
+from jinja2 import Environment, FileSystemLoader, select_autoescape
+
+import config
+
+
+# ---------------------- PARSING HELPERS ---------------------- #
+
+def extract_metrics(raw: str) -> dict:
+ """Extrae valores básicos del informe de Lynis."""
+ def extract_int(pattern: str) -> Optional[int]:
+ m = re.search(pattern, raw)
+ if m:
+ try:
+ return int(m.group(1))
+ except ValueError:
+ return None
+ return None
+
+ metrics = {
+ "hardening_index": extract_int(r"Hardening index\s*:\s*([0-9]+)"),
+ "tests_performed": extract_int(r"Tests performed\s*:\s*([0-9]+)"),
+ "warnings": extract_int(r"Warnings\s*\((\d+)\)"),
+ "suggestions": extract_int(r"Suggestions\s*\((\d+)\)"),
+ }
+
+ # Componentes de software (Firewall / IDS / Malware)
+ if "Firewall [V]" in raw:
+ metrics["firewall"] = "enabled"
+ else:
+ metrics["firewall"] = "missing"
+
+ if "Intrusion software [V]" in raw:
+ metrics["ids"] = "enabled"
+ else:
+ metrics["ids"] = "missing"
+
+ if "Malware scanner [V]" in raw:
+ metrics["malware_scanner"] = "enabled"
+ elif "Malware scanner [X]" in raw:
+ metrics["malware_scanner"] = "missing"
+ else:
+ metrics["malware_scanner"] = "unknown"
+
+ return metrics
+
+
+def extract_warning_and_suggestion_blocks(raw: str):
+ """
+ Extrae las secciones de Warnings y Suggestions del informe
+ y las devuelve como listas de líneas.
+ """
+ warnings_lines: list[str] = []
+ suggestions_lines: list[str] = []
+ state: Optional[str] = None
+
+ for line in raw.splitlines():
+ stripped = line.rstrip("\n")
+ s = stripped.lstrip()
+
+ if s.startswith("Warnings ("):
+ state = "warnings"
+ continue
+ if s.startswith("Suggestions ("):
+ state = "suggestions"
+ continue
+ if s.startswith("Follow-up:"):
+ state = None
+ continue
+
+ # Fuera de sección
+ if state is None:
+ continue
+
+ # Saltar líneas vacías y las de guiones -------------------
+ if not s:
+ continue
+ if set(s) <= {"-", " "}:
+ continue
+
+ if state == "warnings":
+ warnings_lines.append(stripped)
+ elif state == "suggestions":
+ suggestions_lines.append(stripped)
+
+ return warnings_lines, suggestions_lines
+
+
+def group_entries(lines: list[str], marker: str) -> list[str]:
+ """
+ Agrupa bloques que empiezan por "marker " (ej: "* " o "! ")
+ en entradas independientes (cada una puede ocupar varias líneas).
+ """
+ entries: list[list[str]] = []
+ current: list[str] = []
+
+ for line in lines:
+ s = line.lstrip()
+ if s.startswith(marker + " "):
+ # Nuevo bloque
+ if current:
+ entries.append(current)
+ # Guardamos la línea sin el marcador inicial
+ current = [s[len(marker) + 1:]]
+ else:
+ if s:
+ current.append(s)
+
+ if current:
+ entries.append(current)
+
+ # Convertimos a texto multi-línea
+ return ["\n".join(entry) for entry in entries]
+
+
+def text_block_to_html(entry: str) -> str:
+ """
+ Convierte un bloque de texto en HTML sencillo:
+ - Escapa caracteres especiales.
+ - Sustituye saltos de línea por
.
+ """
+ escaped = html.escape(entry)
+ return escaped.replace("\n", "
")
+
+
+# ---------------------- LYNIS + REPORT ---------------------- #
+
+def run_lynis(text_path: str) -> None:
+ """Ejecuta Lynis y guarda la salida en un TXT."""
+ os.makedirs(os.path.dirname(text_path), exist_ok=True)
+ print(f"[+] Ejecutando Lynis: {' '.join(config.LYNIS_CMD)}")
+ result = subprocess.run(
+ config.LYNIS_CMD,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ check=False,
+ )
+ with open(text_path, "w", encoding="utf-8") as f:
+ f.write(result.stdout)
+ print(f"[+] Informe TXT generado: {text_path}")
+
+
+def generate_html(text_path: str, html_path: str, hostname: str, now_str: str) -> str:
+ """
+ Genera el HTML usando una plantilla Jinja2 y lo devuelve como string
+ (además de guardarlo en html_path).
+ """
+ with open(text_path, "r", encoding="utf-8") as f:
+ raw = f.read()
+
+ metrics = extract_metrics(raw)
+ warnings_lines, suggestions_lines = extract_warning_and_suggestion_blocks(raw)
+ warning_entries_text = group_entries(warnings_lines, marker="!")
+ suggestion_entries_text = group_entries(suggestions_lines, marker="*")
+
+ warning_entries_html = [text_block_to_html(e) for e in warning_entries_text]
+ suggestion_entries_html = [text_block_to_html(e) for e in suggestion_entries_text]
+
+ hi = metrics.get("hardening_index") or 0
+ if hi >= 80:
+ hi_class = "good"
+ elif hi >= 60:
+ hi_class = "medium"
+ else:
+ hi_class = "bad"
+
+ def safe_int(v: Optional[int]) -> str:
+ return str(v) if v is not None else "N/A"
+
+ def status_to_icon(status: str) -> str:
+ if status == "enabled":
+ return "✅"
+ if status == "missing":
+ return "⚠️"
+ return "❓"
+
+ firewall_icon = status_to_icon(metrics.get("firewall", "unknown"))
+ ids_icon = status_to_icon(metrics.get("ids", "unknown"))
+ malware_icon = status_to_icon(metrics.get("malware_scanner", "unknown"))
+
+ context = {
+ "hostname": hostname,
+ "now_str": now_str,
+ "hardening_index": safe_int(metrics.get("hardening_index")),
+ "hi_class": hi_class,
+ "warnings_count": safe_int(metrics.get("warnings")),
+ "suggestions_count": safe_int(metrics.get("suggestions")),
+ "tests_performed": safe_int(metrics.get("tests_performed")),
+ "firewall_icon": firewall_icon,
+ "ids_icon": ids_icon,
+ "malware_icon": malware_icon,
+ "warning_entries": warning_entries_html,
+ "suggestion_entries": suggestion_entries_html,
+ }
+
+ template_dir = os.path.join(os.path.dirname(__file__), "templates")
+ env = Environment(
+ loader=FileSystemLoader(template_dir),
+ autoescape=select_autoescape(["html", "xml"]),
+ )
+ template = env.get_template("report.html.j2")
+ html_content = template.render(**context)
+
+ os.makedirs(os.path.dirname(html_path), exist_ok=True)
+ with open(html_path, "w", encoding="utf-8") as f:
+ f.write(html_content)
+
+ print(f"[+] Informe HTML generado: {html_path}")
+ return html_content
+
+
+def generate_pdf(html_path: str, pdf_path: str) -> bool:
+ """
+ Genera un PDF visual a partir del HTML renderizado.
+ Requiere wkhtmltopdf o weasyprint para generar PDFs con formato.
+ """
+ # Intentar con wkhtmltopdf primero (mejor calidad y soporte CSS)
+ if shutil.which("wkhtmltopdf"):
+ cmd = [
+ "wkhtmltopdf",
+ "--enable-local-file-access",
+ "--print-media-type",
+ "--no-stop-slow-scripts",
+ "--enable-javascript",
+ "--javascript-delay", "1000",
+ "--margin-top", "5mm",
+ "--margin-bottom", "5mm",
+ "--margin-left", "5mm",
+ "--margin-right", "5mm",
+ "--page-size", "A4",
+ "--encoding", "UTF-8",
+ html_path,
+ pdf_path,
+ ]
+ print(f"[+] Generando PDF con wkhtmltopdf: {pdf_path}")
+ try:
+ subprocess.run(cmd, check=True, capture_output=True)
+ print(f"[+] Informe PDF generado: {pdf_path}")
+ return True
+ except subprocess.CalledProcessError as e:
+ print(f"[!] Error al generar PDF con wkhtmltopdf: {e}")
+
+ # Intentar con weasyprint (segunda mejor opción)
+ try:
+ from weasyprint import HTML
+ print(f"[+] Generando PDF con WeasyPrint: {pdf_path}")
+ HTML(filename=html_path).write_pdf(pdf_path)
+ print(f"[+] Informe PDF generado: {pdf_path}")
+ return True
+ except ImportError:
+ pass
+ except Exception as e:
+ print(f"[!] Error al generar PDF con WeasyPrint: {e}")
+
+ # Intentar con chromium/chrome headless como alternativa
+ for browser in ["chromium", "chromium-browser", "google-chrome", "chrome"]:
+ if shutil.which(browser):
+ cmd = [
+ browser,
+ "--headless",
+ "--disable-gpu",
+ "--print-to-pdf=" + pdf_path,
+ "--no-margins",
+ html_path,
+ ]
+ print(f"[+] Generando PDF con {browser} headless: {pdf_path}")
+ try:
+ subprocess.run(cmd, check=True, capture_output=True, timeout=30)
+ print(f"[+] Informe PDF generado: {pdf_path}")
+ return True
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
+ print(f"[!] Error al generar PDF con {browser}: {e}")
+ continue
+
+ print("[!] No se encontró ninguna herramienta para generar PDFs con formato visual.")
+ print("[!] Instala alguna de las siguientes:")
+ print(" - wkhtmltopdf (recomendado): apt install wkhtmltopdf")
+ print(" - weasyprint: pip install weasyprint")
+ print(" - chromium: apt install chromium-browser")
+ return False
+
+
+def attach_file(msg: EmailMessage, path: str, mime_type: str, subtype: str) -> None:
+ with open(path, "rb") as f:
+ data = f.read()
+ filename = os.path.basename(path)
+ msg.add_attachment(data, maintype=mime_type, subtype=subtype, filename=filename)
+
+
+def send_email(
+ text_path: str,
+ pdf_path: Optional[str],
+ hostname: str,
+ now_str: str,
+ html_body: str,
+) -> None:
+ subject = f"{config.SUBJECT_PREFIX} {hostname} - {now_str}"
+
+ msg = EmailMessage()
+ msg["From"] = config.FROM_ADDR
+ msg["To"] = ", ".join(config.TO_ADDRS)
+ msg["Subject"] = subject
+
+ plain_body = (
+ f"Lynis security report\n\n"
+ f"Host : {hostname}\n"
+ f"Date : {now_str}\n\n"
+ f"This email contains an HTML version of the Lynis report in the body.\n"
+ f"Full report (TXT) and PDF versions are attached.\n"
+ )
+ msg.set_content(plain_body)
+
+ msg.add_alternative(html_body, subtype="html")
+
+ attach_file(msg, text_path, "text", "plain")
+
+ if pdf_path and os.path.exists(pdf_path):
+ attach_file(msg, pdf_path, "application", "pdf")
+
+ print(f"[+] Enviando correo a: {config.TO_ADDRS} via {config.SMTP_HOST}:{config.SMTP_PORT}")
+ with smtplib.SMTP(config.SMTP_HOST, config.SMTP_PORT) as server:
+ server.starttls()
+ if config.SMTP_USER:
+ server.login(config.SMTP_USER, config.SMTP_PASS)
+ server.send_message(msg)
+ print("[+] Correo enviado correctamente.")
+
+
+def main() -> None:
+ now = datetime.datetime.now()
+ now_str = now.strftime("%Y-%m-%d %H:%M:%S")
+ stamp = now.strftime("%Y%m%d-%H%M")
+ hostname = os.uname().nodename
+
+ os.makedirs(config.BASE_DIR, exist_ok=True)
+
+ text_path = os.path.join(config.BASE_DIR, f"lynis-{hostname}-{stamp}.txt")
+ html_path = os.path.join(config.BASE_DIR, f"lynis-{hostname}-{stamp}.html")
+ pdf_path = os.path.join(config.BASE_DIR, f"lynis-{hostname}-{stamp}.pdf")
+
+ # Ejecutar Lynis y guardar salida TXT original
+ run_lynis(text_path)
+
+ # Generar HTML formateado desde el TXT
+ html_body = generate_html(text_path, html_path, hostname, now_str)
+
+ # Generar PDF formateado desde el HTML
+ pdf_ok = generate_pdf(html_path, pdf_path)
+ if not pdf_ok:
+ pdf_path = None
+
+ send_email(text_path, pdf_path, hostname, now_str, html_body)
+
+ # Limpiar archivos después del envío
+ print("[+] Limpiando archivos temporales...")
+ files_to_remove = [text_path, html_path]
+ if pdf_path and os.path.exists(pdf_path):
+ files_to_remove.append(pdf_path)
+
+ for file_path in files_to_remove:
+ try:
+ if os.path.exists(file_path):
+ os.remove(file_path)
+ print(f" - Eliminado: {os.path.basename(file_path)}")
+ except Exception as e:
+ print(f"[!] Error al eliminar {file_path}: {e}")
+
+ print("[+] Proceso completado.")
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/lynis/requirements.txt b/lynis/requirements.txt
new file mode 100644
index 0000000..8fcfec9
--- /dev/null
+++ b/lynis/requirements.txt
@@ -0,0 +1,6 @@
+# Python dependencies
+Jinja2>=3.1.0
+
+# Optional: PDF generation (alternative to wkhtmltopdf)
+# weasyprint>=60.0
+
diff --git a/lynis/templates/report.html.j2 b/lynis/templates/report.html.j2
new file mode 100644
index 0000000..56ff7cc
--- /dev/null
+++ b/lynis/templates/report.html.j2
@@ -0,0 +1,405 @@
+
+
+
+
+ Informe Lynis - {{ hostname }} - {{ now_str }}
+
+
+
+
+
+
+
+
+
+
+
+ Hardening Index
+ {{ hardening_index }}
+ Out of 100 points
+
+ |
+
+
+ Test Results
+ {{ tests_performed }}
+
+ ⚠️ {{ warnings_count }} Warnings
+ 💡 {{ suggestions_count }} Suggestions
+
+
+ |
+
+
+ Security Components
+ System Status
+
+ 🔥 Firewall {{ firewall_icon }}
+ 🛡️ IDS/IPS {{ ids_icon }}
+ 🦠 Malware {{ malware_icon }}
+
+
+ |
+
+
+
+
+
+
+ Critical security issues that require immediate attention.
+
+
+ {% if warning_entries %}
+ {% for w in warning_entries %}
+ -
+
{{ w | safe }}
+
+ {% endfor %}
+ {% else %}
+ - ✅ No warnings detected - Great job!
+ {% endif %}
+
+
+
+
+
+
+ Recommendations to improve your system's security posture.
+
+
+ {% if suggestion_entries %}
+ {% for s in suggestion_entries %}
+ -
+
{{ s | safe }}
+
+ {% endfor %}
+ {% else %}
+ - ✅ No suggestions - Your system is well configured!
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+