From a651474dac1ad03d50dd828b94eaaadcc20787c7 Mon Sep 17 00:00:00 2001 From: Eduardo David Paredes Vara Date: Fri, 5 Dec 2025 00:35:56 +0000 Subject: [PATCH] lynis report mail pdf html base --- lynis/README.md | 309 +++++++++++++++++++++++++ lynis/config.py | 38 ++++ lynis/lynis_report.py | 384 +++++++++++++++++++++++++++++++ lynis/requirements.txt | 6 + lynis/templates/report.html.j2 | 405 +++++++++++++++++++++++++++++++++ 5 files changed, 1142 insertions(+) create mode 100644 lynis/README.md create mode 100644 lynis/config.py create mode 100644 lynis/lynis_report.py create mode 100644 lynis/requirements.txt create mode 100644 lynis/templates/report.html.j2 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 }} + + + +
+
+

🔒 Lynis Security Report

+
+ Host: {{ hostname }} · Generated: {{ 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 }}
+
+
+
+ +
+
+

⚠️ Warnings

+ {{ warnings_count }} +
+
+ 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 %} +
+
+ +
+
+

💡 Suggestions

+ {{ suggestions_count }} +
+
+ 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 %} +
+
+ + +
+ + +
+ + +