lynis report mail pdf html base
This commit is contained in:
309
lynis/README.md
Normal file
309
lynis/README.md
Normal file
@@ -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.
|
||||||
38
lynis/config.py
Normal file
38
lynis/config.py
Normal file
@@ -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"
|
||||||
|
|
||||||
384
lynis/lynis_report.py
Normal file
384
lynis/lynis_report.py
Normal file
@@ -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 <br>.
|
||||||
|
"""
|
||||||
|
escaped = html.escape(entry)
|
||||||
|
return escaped.replace("\n", "<br>")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------- 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()
|
||||||
|
|
||||||
6
lynis/requirements.txt
Normal file
6
lynis/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Python dependencies
|
||||||
|
Jinja2>=3.1.0
|
||||||
|
|
||||||
|
# Optional: PDF generation (alternative to wkhtmltopdf)
|
||||||
|
# weasyprint>=60.0
|
||||||
|
|
||||||
405
lynis/templates/report.html.j2
Normal file
405
lynis/templates/report.html.j2
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Informe Lynis - {{ hostname }} - {{ now_str }}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 30px 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .subtitle strong {
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 20px 0;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid td {
|
||||||
|
width: 33.333%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 6px 15px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 30px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.good {
|
||||||
|
border-color: #10b981;
|
||||||
|
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.medium {
|
||||||
|
border-color: #f59e0b;
|
||||||
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.bad {
|
||||||
|
border-color: #ef4444;
|
||||||
|
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d3748;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sub {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #718096;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sub div {
|
||||||
|
margin: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 3px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d3748;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .badge {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
color: #718096;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.warnings-list,
|
||||||
|
ul.suggestions-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.warnings-list li,
|
||||||
|
ul.suggestions-list li {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 5px solid;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.warnings-list li:hover,
|
||||||
|
ul.suggestions-list li:hover {
|
||||||
|
transform: translateX(5px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.warnings-list li {
|
||||||
|
border-left-color: #f97316;
|
||||||
|
background: linear-gradient(to right, #fff7ed 0%, #f8fafc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.suggestions-list li {
|
||||||
|
border-left-color: #3b82f6;
|
||||||
|
background: linear-gradient(to right, #eff6ff 0%, #f8fafc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-body {
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #374151;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-body a {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-body a:hover {
|
||||||
|
border-bottom-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.raw {
|
||||||
|
margin-top: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.raw > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d3748;
|
||||||
|
background: #f1f5f9;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.raw > summary:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.raw[open] > summary {
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.raw-output {
|
||||||
|
padding: 25px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1f2937;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: #718096;
|
||||||
|
font-size: 14px;
|
||||||
|
border-top: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.summary-grid td {
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid td {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover,
|
||||||
|
ul.warnings-list li:hover,
|
||||||
|
ul.suggestions-list li:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.raw {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔒 Lynis Security Report</h1>
|
||||||
|
<div class="subtitle">
|
||||||
|
Host: <strong>{{ hostname }}</strong> · Generated: {{ now_str }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<table class="summary-grid">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="card {{ hi_class }}">
|
||||||
|
<div class="card-title">Hardening Index</div>
|
||||||
|
<div class="card-value">{{ hardening_index }}</div>
|
||||||
|
<div class="card-sub">Out of 100 points</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Test Results</div>
|
||||||
|
<div class="card-value">{{ tests_performed }}</div>
|
||||||
|
<div class="card-sub">
|
||||||
|
<div><span>⚠️</span> <span>{{ warnings_count }} Warnings</span></div>
|
||||||
|
<div><span>💡</span> <span>{{ suggestions_count }} Suggestions</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Security Components</div>
|
||||||
|
<div class="card-value" style="font-size: 20px;">System Status</div>
|
||||||
|
<div class="card-sub">
|
||||||
|
<div><span>🔥</span> <span>Firewall {{ firewall_icon }}</span></div>
|
||||||
|
<div><span>🛡️</span> <span>IDS/IPS {{ ids_icon }}</span></div>
|
||||||
|
<div><span>🦠</span> <span>Malware {{ malware_icon }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>⚠️ Warnings</h2>
|
||||||
|
<span class="badge">{{ warnings_count }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-description">
|
||||||
|
Critical security issues that require immediate attention.
|
||||||
|
</div>
|
||||||
|
<ul class="warnings-list">
|
||||||
|
{% if warning_entries %}
|
||||||
|
{% for w in warning_entries %}
|
||||||
|
<li>
|
||||||
|
<div class="entry-body">{{ w | safe }}</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<li class="no-data">✅ No warnings detected - Great job!</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>💡 Suggestions</h2>
|
||||||
|
<span class="badge">{{ suggestions_count }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-description">
|
||||||
|
Recommendations to improve your system's security posture.
|
||||||
|
</div>
|
||||||
|
<ul class="suggestions-list">
|
||||||
|
{% if suggestion_entries %}
|
||||||
|
{% for s in suggestion_entries %}
|
||||||
|
<li>
|
||||||
|
<div class="entry-body">{{ s | safe }}</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<li class="no-data">✅ No suggestions - Your system is well configured!</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Generated by Lynis Security Audit Tool · Report created on {{ now_str }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user