v2.3
This commit is contained in:
@@ -0,0 +1,805 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Email Notification Module for Audit Hardening Tool
|
||||
Gửi email thông báo khi xử lý file hoàn tất
|
||||
Hỗ trợ 2 phương thức: SMTP trực tiếp hoặc HTTP API
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
import imaplib
|
||||
import ssl
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import requests
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.base import MIMEBase
|
||||
from email import encoders
|
||||
from email.utils import formatdate
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
from colorama import Fore
|
||||
|
||||
|
||||
class EmailNotifier:
|
||||
"""Class xử lý gửi email thông báo"""
|
||||
|
||||
def __init__(self, config_path: str = "email_config.json"):
|
||||
"""
|
||||
Khởi tạo EmailNotifier với cấu hình từ file JSON
|
||||
|
||||
Args:
|
||||
config_path: Đường dẫn tới file cấu hình email
|
||||
"""
|
||||
self.config = self._load_config(config_path)
|
||||
self.enabled = self.config.get("enabled", False)
|
||||
self.email_method = self.config.get("email_method", "smtp") # "smtp" hoặc "api"
|
||||
|
||||
def _load_config(self, config_path: str) -> dict:
|
||||
"""Load cấu hình email từ file JSON"""
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
print(Fore.GREEN + f"[EMAIL] Configuration loaded. Enabled: {config.get('enabled', False)}" + Fore.RESET)
|
||||
return config
|
||||
except FileNotFoundError:
|
||||
print(Fore.YELLOW + f"[EMAIL] {config_path} not found. Email notifications disabled." + Fore.RESET)
|
||||
return {"enabled": False}
|
||||
except Exception as e:
|
||||
print(Fore.RED + f"[EMAIL] Error loading config: {e}" + Fore.RESET)
|
||||
return {"enabled": False}
|
||||
|
||||
def _create_email(
|
||||
self,
|
||||
to_emails: List[str],
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str = None,
|
||||
attachments: List[str] = None
|
||||
) -> MIMEMultipart:
|
||||
"""
|
||||
Tạo đối tượng email
|
||||
|
||||
Args:
|
||||
to_emails: Danh sách email nhận
|
||||
subject: Tiêu đề email
|
||||
body_html: Nội dung HTML
|
||||
body_text: Nội dung text thuần (fallback)
|
||||
attachments: Danh sách file đính kèm
|
||||
|
||||
Returns:
|
||||
MIMEMultipart object
|
||||
"""
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['From'] = self.config.get('sender_email', '')
|
||||
msg['To'] = ', '.join(to_emails)
|
||||
msg['Subject'] = subject
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
|
||||
# Thêm phần text và HTML
|
||||
if body_text:
|
||||
msg.attach(MIMEText(body_text, 'plain', 'utf-8'))
|
||||
msg.attach(MIMEText(body_html, 'html', 'utf-8'))
|
||||
|
||||
# Thêm file đính kèm nếu có
|
||||
if attachments and self.config.get('send_attachment', False):
|
||||
for file_path in attachments:
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
part = MIMEBase('application', 'octet-stream')
|
||||
part.set_payload(f.read())
|
||||
encoders.encode_base64(part)
|
||||
part.add_header(
|
||||
'Content-Disposition',
|
||||
f'attachment; filename="{os.path.basename(file_path)}"'
|
||||
)
|
||||
msg.attach(part)
|
||||
except Exception as e:
|
||||
print(Fore.YELLOW + f"[EMAIL] Cannot attach file {file_path}: {e}" + Fore.RESET)
|
||||
|
||||
return msg
|
||||
|
||||
def _file_to_base64(self, file_path: str) -> dict:
|
||||
"""
|
||||
Convert file thành base64 để gửi qua API
|
||||
|
||||
Args:
|
||||
file_path: Đường dẫn tới file
|
||||
|
||||
Returns:
|
||||
Dict với filename và content_base64
|
||||
"""
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
|
||||
p = Path(file_path)
|
||||
size = p.stat().st_size
|
||||
if size > MAX_FILE_SIZE:
|
||||
print(Fore.YELLOW + f"[EMAIL] File too large for API attachment: {p.name} ({size} bytes)" + Fore.RESET)
|
||||
raise ValueError(f"{p.name} too large ({size} bytes > {MAX_FILE_SIZE} bytes)")
|
||||
|
||||
with p.open("rb") as f:
|
||||
return {
|
||||
"filename": p.name,
|
||||
"content_base64": base64.b64encode(f.read()).decode()
|
||||
}
|
||||
|
||||
def _send_via_api(
|
||||
self,
|
||||
to_emails: List[str],
|
||||
subject: str,
|
||||
html_content: str,
|
||||
attachments: List[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Gửi email qua HTTP API (giống send_mail.py)
|
||||
|
||||
Args:
|
||||
to_emails: Danh sách email nhận
|
||||
subject: Tiêu đề email
|
||||
html_content: Nội dung HTML
|
||||
attachments: Danh sách đường dẫn file đính kèm
|
||||
|
||||
Returns:
|
||||
True nếu gửi thành công
|
||||
"""
|
||||
api_url = self.config.get('email_api_url')
|
||||
api_key = self.config.get('email_api_key')
|
||||
|
||||
if not api_url or not api_key:
|
||||
print(Fore.RED + "[EMAIL] ✗ API URL or API Key not configured in email_config.json" + Fore.RESET)
|
||||
return False
|
||||
|
||||
# Chuẩn bị attachments base64
|
||||
att_list = []
|
||||
if attachments and self.config.get('send_attachment', False):
|
||||
for file_path in attachments:
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
att_list.append(self._file_to_base64(file_path))
|
||||
except Exception as e:
|
||||
print(Fore.YELLOW + f"[EMAIL] Cannot attach file {file_path}: {e}" + Fore.RESET)
|
||||
|
||||
payload = {
|
||||
"api_key": api_key,
|
||||
"to_emails": to_emails,
|
||||
"cc_emails": [],
|
||||
"subject": subject,
|
||||
"html_content": html_content,
|
||||
"attachments": att_list
|
||||
}
|
||||
|
||||
try:
|
||||
print(Fore.CYAN + f"[EMAIL] Sending via API: {api_url}" + Fore.RESET)
|
||||
r = requests.post(
|
||||
api_url,
|
||||
json=payload,
|
||||
timeout=120,
|
||||
verify=False
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
print(Fore.RED + f"[EMAIL] ✗ API Error: {r.status_code} - {r.text}" + Fore.RESET)
|
||||
return False
|
||||
|
||||
print(Fore.GREEN + f"[EMAIL] ✓ API Email sent successfully to: {', '.join(to_emails)}" + Fore.RESET)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(Fore.RED + f"[EMAIL] ✗ API Request failed: {e}" + Fore.RESET)
|
||||
return False
|
||||
|
||||
def _save_to_sent_folder(self, msg: MIMEMultipart) -> bool:
|
||||
"""Lưu email vào thư mục Sent qua IMAP"""
|
||||
if not self.config.get('save_to_sent', False):
|
||||
return True
|
||||
|
||||
try:
|
||||
ssl_context = ssl.create_default_context()
|
||||
imap = imaplib.IMAP4_SSL(
|
||||
host=self.config.get('imap_server', 'email.vnpt.vn'),
|
||||
port=self.config.get('imap_port', 993),
|
||||
ssl_context=ssl_context
|
||||
)
|
||||
|
||||
imap.login(
|
||||
self.config.get('sender_email', ''),
|
||||
self.config.get('sender_password', '')
|
||||
)
|
||||
|
||||
# Thử các tên thư mục Sent phổ biến
|
||||
sent_folders = ['Sent', 'INBOX.Sent', 'Sent Items', 'Sent Messages']
|
||||
|
||||
for folder in sent_folders:
|
||||
try:
|
||||
status, _ = imap.select(folder)
|
||||
if status == 'OK':
|
||||
imap.append(folder, '\\Seen', None, msg.as_bytes())
|
||||
print(Fore.CYAN + f"[EMAIL] Saved to {folder} folder" + Fore.RESET)
|
||||
imap.logout()
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
|
||||
imap.logout()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(Fore.YELLOW + f"[EMAIL] Could not save to Sent: {e}" + Fore.RESET)
|
||||
return False
|
||||
|
||||
def send_processing_notification(
|
||||
self,
|
||||
output_files: List[str],
|
||||
os_type: str,
|
||||
processing_time: float = None,
|
||||
recipients: List[str] = None,
|
||||
base_url: str = "http://localhost:8000",
|
||||
system_info_list: List[dict] = None,
|
||||
attachments: List[str] = None,
|
||||
is_admin: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Gửi email thông báo khi xử lý file hoàn tất
|
||||
|
||||
Args:
|
||||
output_files: Danh sách tên file output đã tạo
|
||||
os_type: Loại hệ điều hành đã xử lý
|
||||
processing_time: Thời gian xử lý (giây)
|
||||
recipients: Danh sách email nhận (mặc định từ config)
|
||||
base_url: URL base của ứng dụng
|
||||
system_info_list: Danh sách thông tin hệ thống từ các file đã xử lý
|
||||
attachments: Danh sách file đính kèm (đầy đủ đường dẫn)
|
||||
is_admin: True nếu admin đăng nhập, sẽ gửi kèm file
|
||||
|
||||
Returns:
|
||||
True nếu gửi thành công, False nếu lỗi
|
||||
"""
|
||||
if not self.enabled:
|
||||
print(Fore.YELLOW + "[EMAIL] Email notifications are disabled" + Fore.RESET)
|
||||
return False
|
||||
|
||||
# Sử dụng recipients mặc định nếu không được chỉ định
|
||||
to_emails = recipients or self.config.get('default_recipients', [])
|
||||
|
||||
if not to_emails:
|
||||
print(Fore.YELLOW + "[EMAIL] No recipients specified" + Fore.RESET)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Tạo tiêu đề
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
subject = f"[Audit Tool] Xử lý hoàn tất - {os_type.upper()} - {len(output_files)} file(s)"
|
||||
|
||||
# Tạo danh sách file với link download
|
||||
files_html = ""
|
||||
files_text = ""
|
||||
for filename in output_files:
|
||||
download_url = f"{base_url}/download/{filename}"
|
||||
files_html += f'<li style="margin: 8px 0;"><a href="{download_url}" style="color: #1976d2; text-decoration: none;">📊 {filename}</a></li>\n'
|
||||
files_text += f" - {filename}: {download_url}\n"
|
||||
|
||||
# Tạo nội dung thông tin hệ thống
|
||||
system_info_html = ""
|
||||
system_info_text = ""
|
||||
if system_info_list:
|
||||
for idx, system_info in enumerate(system_info_list):
|
||||
if not system_info:
|
||||
continue
|
||||
|
||||
# Tính màu compliance
|
||||
compliance_pct = system_info.get('compliance_percentage', 0)
|
||||
if compliance_pct >= 80:
|
||||
compliance_color = "#4caf50"
|
||||
elif compliance_pct >= 50:
|
||||
compliance_color = "#ff9800"
|
||||
else:
|
||||
compliance_color = "#f44336"
|
||||
|
||||
# Script version badge
|
||||
script_version_html = ''
|
||||
if system_info.get('script_version'):
|
||||
script_version_html = f'<span style="background: #e8f5e9; color: #2e7d32; padding: 4px 10px; border-radius: 15px; font-size: 11px; font-weight: 600; margin-left: 5px;">🏷️ Script v{system_info.get("script_version")}</span>'
|
||||
|
||||
# Mandatory/Optional stats
|
||||
mandatory_passed = system_info.get('mandatory_passed', 0)
|
||||
mandatory_total = system_info.get('mandatory_total', 0)
|
||||
optional_passed = system_info.get('optional_passed', 0)
|
||||
optional_total = system_info.get('optional_total', 0)
|
||||
|
||||
# Mandatory color
|
||||
if mandatory_total > 0 and mandatory_passed == mandatory_total:
|
||||
mandatory_color = "#4caf50"
|
||||
elif mandatory_total > 0:
|
||||
mandatory_color = "#ff9800"
|
||||
else:
|
||||
mandatory_color = "#666"
|
||||
|
||||
# Optional color
|
||||
if optional_total > 0 and optional_passed == optional_total:
|
||||
optional_color = "#4caf50"
|
||||
elif optional_total > 0:
|
||||
optional_color = "#ff9800"
|
||||
else:
|
||||
optional_color = "#666"
|
||||
|
||||
# Progress bar width
|
||||
progress_width = int(compliance_pct * 2) # max 200px
|
||||
|
||||
system_info_html += f'''
|
||||
<div style="background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); border: 1px solid #e0e0e0; border-radius: 15px; padding: 25px; margin-bottom: 20px;">
|
||||
<!-- Header with hostname and OS -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #e3f2fd;">
|
||||
<div>
|
||||
<h3 style="color: #1565c0; margin: 0; font-size: 22px;">🖥️ {system_info.get('hostname', 'Unknown Host')}</h3>
|
||||
<p style="color: #666; margin: 5px 0 0 0; font-size: 14px;">{system_info.get('os_name', 'Unknown OS')}</p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span style="background: #e3f2fd; color: #1565c0; padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">
|
||||
{system_info.get('audit_time', '')}
|
||||
</span>
|
||||
{script_version_html}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Grid - 2 columns -->
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<!-- Left Column: Basic Info + Hardware -->
|
||||
<td style="vertical-align: top; width: 50%; padding-right: 10px;">
|
||||
<!-- Basic Info -->
|
||||
<div style="background: #fff; border-radius: 10px; padding: 15px; border: 1px solid #e8e8e8; margin-bottom: 10px;">
|
||||
<h4 style="color: #1976d2; margin: 0 0 12px 0; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">📋 Thông tin cơ bản</h4>
|
||||
<table style="width: 100%; font-size: 12px;">
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">IP Address:</td><td style="text-align: right;"><strong>{system_info.get("ip_address", "N/A")}</strong></td></tr>' if system_info.get('ip_address') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Kernel:</td><td style="text-align: right; font-size: 10px;"><strong>{system_info.get("kernel_version", "N/A")}</strong></td></tr>' if system_info.get('kernel_version') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Architecture:</td><td style="text-align: right;"><strong>{system_info.get("architecture", "N/A")}</strong></td></tr>' if system_info.get('architecture') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Uptime:</td><td style="text-align: right;"><strong>{system_info.get("uptime", "N/A")}</strong></td></tr>' if system_info.get('uptime') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Timezone:</td><td style="text-align: right;"><strong>{system_info.get("timezone", "N/A")}</strong></td></tr>' if system_info.get('timezone') else ''}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Hardware Info -->
|
||||
<div style="background: #fff; border-radius: 10px; padding: 15px; border: 1px solid #e8e8e8;">
|
||||
<h4 style="color: #4caf50; margin: 0 0 12px 0; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">⚙️ Phần cứng</h4>
|
||||
<table style="width: 100%; font-size: 12px;">
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">CPU:</td><td style="text-align: right; font-size: 10px;"><strong>{system_info.get("cpu_model", "N/A")}</strong></td></tr>' if system_info.get('cpu_model') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">CPU Cores:</td><td style="text-align: right;"><strong>{system_info.get("cpu_cores", "N/A")}</strong></td></tr>' if system_info.get('cpu_cores') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Total Memory:</td><td style="text-align: right;"><strong>{system_info.get("total_memory", "N/A")}</strong></td></tr>' if system_info.get('total_memory') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Used Memory:</td><td style="text-align: right;"><strong>{system_info.get("used_memory", "N/A")}</strong></td></tr>' if system_info.get('used_memory') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Free Memory:</td><td style="text-align: right;"><strong>{system_info.get("free_memory", "N/A")}</strong></td></tr>' if system_info.get('free_memory') else ''}
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Right Column: Compliance + Network -->
|
||||
<td style="vertical-align: top; width: 50%; padding-left: 10px;">
|
||||
<!-- Compliance Stats -->
|
||||
<div style="background: #fff; border-radius: 10px; padding: 15px; border: 1px solid #e8e8e8; text-align: center;">
|
||||
<h4 style="color: #e91e63; margin: 0 0 15px 0; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">📊 Tỉ lệ tuân thủ</h4>
|
||||
|
||||
<!-- Compliance Percentage with progress bar -->
|
||||
<div style="font-size: 36px; font-weight: bold; color: {compliance_color}; margin: 10px 0;">
|
||||
{compliance_pct}%
|
||||
</div>
|
||||
<div style="background: #e0e0e0; border-radius: 10px; height: 10px; width: 100%; margin: 8px 0 15px 0;">
|
||||
<div style="background: {compliance_color}; border-radius: 10px; height: 10px; width: {compliance_pct}%;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Mandatory / Non-Mandatory Stats -->
|
||||
<table style="width: 100%; margin-top: 10px;">
|
||||
<tr>
|
||||
<td style="text-align: center; padding: 10px 8px; background: #e3f2fd; border-radius: 8px; width: 45%; border-left: 3px solid #1565c0;">
|
||||
<div style="font-size: 10px; color: #1565c0; font-weight: 600; text-transform: uppercase; margin-bottom: 5px;">🔒 Bắt buộc đạt</div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: {mandatory_color};">{mandatory_passed}/{mandatory_total}</div>
|
||||
{f'<div style="font-size: 11px; color: #999; margin-top: 4px;">{round((mandatory_passed / mandatory_total) * 100, 1) if mandatory_total > 0 else 0}%</div>' if mandatory_total > 0 else ''}
|
||||
</td>
|
||||
<td style="width: 10%;"></td>
|
||||
<td style="text-align: center; padding: 10px 8px; background: #f3e5f5; border-radius: 8px; width: 45%; border-left: 3px solid #7b1fa2;">
|
||||
<div style="font-size: 10px; color: #7b1fa2; font-weight: 600; text-transform: uppercase; margin-bottom: 5px;">📋 Tuỳ chọn đạt</div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: {optional_color};">{optional_passed}/{optional_total}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Total checks -->
|
||||
<div style="margin-top: 10px; color: #666; font-size: 12px;">
|
||||
Tổng số: <strong>{system_info.get('total_checks', 0)}</strong> tiêu chí
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Info -->
|
||||
<div style="background: #fff; border-radius: 10px; padding: 15px; border: 1px solid #e8e8e8; margin-top: 10px;">
|
||||
<h4 style="color: #ff9800; margin: 0 0 12px 0; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">🌐 Mạng</h4>
|
||||
<table style="width: 100%; font-size: 12px;">
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Interface:</td><td style="text-align: right;"><strong>{system_info.get("primary_interface", "N/A")}</strong></td></tr>' if system_info.get('primary_interface') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">MAC:</td><td style="text-align: right; font-size: 10px;"><strong>{system_info.get("mac_address", "N/A")}</strong></td></tr>' if system_info.get('mac_address') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">Gateway:</td><td style="text-align: right;"><strong>{system_info.get("default_gateway", "N/A")}</strong></td></tr>' if system_info.get('default_gateway') else ''}
|
||||
{f'<tr><td style="color: #666; padding: 4px 0;">DNS:</td><td style="text-align: right;"><strong>{system_info.get("dns_servers", "N/A")}</strong></td></tr>' if system_info.get('dns_servers') else ''}
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
# Text version
|
||||
script_ver_text = f" (Script v{system_info.get('script_version')})" if system_info.get('script_version') else ''
|
||||
mandatory_pct_text = round((mandatory_passed / mandatory_total) * 100, 1) if mandatory_total > 0 else 0
|
||||
system_info_text += f'''
|
||||
--- Hệ thống {idx + 1}: {system_info.get('hostname', 'Unknown')}{script_ver_text} ---
|
||||
OS: {system_info.get('os_name', 'N/A')}
|
||||
IP: {system_info.get('ip_address', 'N/A')}
|
||||
Kernel: {system_info.get('kernel_version', 'N/A')}
|
||||
Architecture: {system_info.get('architecture', 'N/A')}
|
||||
Uptime: {system_info.get('uptime', 'N/A')}
|
||||
Timezone: {system_info.get('timezone', 'N/A')}
|
||||
CPU: {system_info.get('cpu_model', 'N/A')} ({system_info.get('cpu_cores', 'N/A')} cores)
|
||||
Memory: {system_info.get('total_memory', 'N/A')} (Used: {system_info.get('used_memory', 'N/A')}, Free: {system_info.get('free_memory', 'N/A')})
|
||||
Interface: {system_info.get('primary_interface', 'N/A')} | MAC: {system_info.get('mac_address', 'N/A')}
|
||||
Gateway: {system_info.get('default_gateway', 'N/A')} | DNS: {system_info.get('dns_servers', 'N/A')}
|
||||
|
||||
📊 Tỉ lệ tuân thủ: {compliance_pct}%
|
||||
- 🔒 Bắt buộc đạt: {mandatory_passed}/{mandatory_total} ({mandatory_pct_text}%)
|
||||
- 📋 Tuỳ chọn đạt: {optional_passed}/{optional_total}
|
||||
- Tổng số: {system_info.get('total_checks', 0)} tiêu chí
|
||||
'''
|
||||
|
||||
# ===== Tạo BẢNG TỔNG HỢP KẾT QUẢ (giống web) =====
|
||||
summary_table_html = ""
|
||||
if system_info_list and len([s for s in system_info_list if s]) > 0:
|
||||
valid_infos = [s for s in system_info_list if s]
|
||||
|
||||
# Build summary table rows
|
||||
summary_rows = ""
|
||||
for idx_s, si in enumerate(valid_infos):
|
||||
s_compliance = si.get('compliance_percentage', 0)
|
||||
s_mandatory_passed = si.get('mandatory_passed', 0)
|
||||
s_mandatory_total = si.get('mandatory_total', 0)
|
||||
s_mandatory_failed = si.get('mandatory_failed', 0)
|
||||
s_mandatory_pct = round((s_mandatory_passed / s_mandatory_total) * 100, 1) if s_mandatory_total > 0 else 0
|
||||
|
||||
# Compliance color
|
||||
if s_compliance >= 80:
|
||||
s_pct_bg = "#e8f5e9"; s_pct_color = "#2e7d32"
|
||||
elif s_compliance >= 50:
|
||||
s_pct_bg = "#fff3e0"; s_pct_color = "#e65100"
|
||||
else:
|
||||
s_pct_bg = "#ffebee"; s_pct_color = "#c62828"
|
||||
|
||||
# Mandatory color
|
||||
if s_mandatory_failed == 0 and s_mandatory_total > 0:
|
||||
s_mand_bg = "#e8f5e9"; s_mand_color = "#2e7d32"
|
||||
elif s_mandatory_total > 0:
|
||||
s_mand_bg = "#ffebee"; s_mand_color = "#c62828"
|
||||
else:
|
||||
s_mand_bg = "#f5f5f5"; s_mand_color = "#999"
|
||||
|
||||
# Note
|
||||
if s_mandatory_failed > 0 and s_compliance < 80:
|
||||
note_html = f'''
|
||||
<div style="padding: 6px 10px; background: #ffebee; border-left: 3px solid #c62828; border-radius: 4px; color: #c62828; font-size: 11px; margin-bottom: 4px;">
|
||||
❌ Còn <strong>{s_mandatory_failed}</strong> tiêu chí bắt buộc chưa đạt
|
||||
</div>
|
||||
<div style="padding: 6px 10px; background: #fff8e1; border-left: 3px solid #ffa000; border-radius: 4px; color: #e65100; font-size: 11px;">
|
||||
⚠️ Tỉ lệ đạt chỉ {s_compliance}% (yêu cầu ≥80%)
|
||||
</div>'''
|
||||
elif s_mandatory_failed > 0:
|
||||
note_html = f'''
|
||||
<div style="padding: 6px 10px; background: #fff8e1; border-left: 3px solid #ffa000; border-radius: 4px; color: #e65100; font-size: 11px;">
|
||||
⚠️ Còn <strong style="color: #c62828;">{s_mandatory_failed}</strong> tiêu chí bắt buộc chưa đạt
|
||||
</div>'''
|
||||
elif s_compliance < 80:
|
||||
note_html = f'''
|
||||
<div style="padding: 6px 10px; background: #fff8e1; border-left: 3px solid #ffa000; border-radius: 4px; color: #e65100; font-size: 11px;">
|
||||
⚠️ Tỉ lệ đạt chỉ {s_compliance}% (yêu cầu ≥80%)
|
||||
</div>'''
|
||||
elif s_mandatory_total > 0:
|
||||
note_html = '''
|
||||
<div style="padding: 6px 10px; background: #e8f5e9; border-left: 3px solid #4caf50; border-radius: 4px; color: #2e7d32; font-size: 11px;">
|
||||
✅ Đạt yêu cầu
|
||||
</div>'''
|
||||
else:
|
||||
note_html = '<span style="color: #bbb; font-style: italic; font-size: 11px;">Không có dữ liệu</span>'
|
||||
|
||||
row_bg = "#fafbfc" if idx_s % 2 == 0 else "#ffffff"
|
||||
|
||||
summary_rows += f'''
|
||||
<tr style="background: {row_bg};">
|
||||
<td style="padding: 12px 14px; text-align: center; color: #666; font-weight: 500; border-bottom: 1px solid #f0f0f0;">{idx_s + 1}</td>
|
||||
<td style="padding: 12px 14px; border-bottom: 1px solid #f0f0f0;">
|
||||
<div style="font-weight: 600; color: #1565c0;">{si.get('hostname', 'N/A')}</div>
|
||||
<div style="font-size: 11px; color: #999; margin-top: 2px;">{si.get('os_name', '')}</div>
|
||||
</td>
|
||||
<td style="padding: 12px 14px; color: #333; font-family: Consolas, monospace; font-size: 13px; border-bottom: 1px solid #f0f0f0;">{si.get('ip_address', 'N/A')}</td>
|
||||
<td style="padding: 12px 14px; text-align: center; border-bottom: 1px solid #f0f0f0;">
|
||||
<span style="display: inline-block; padding: 4px 12px; border-radius: 15px; font-weight: 700; font-size: 13px; background: {s_pct_bg}; color: {s_pct_color};">{s_compliance}%</span>
|
||||
<div style="font-size: 10px; color: #999; margin-top: 3px;">{si.get('passed_count', 0)}/{si.get('total_checks', 0)} tiêu chí</div>
|
||||
</td>
|
||||
<td style="padding: 12px 14px; text-align: center; border-bottom: 1px solid #f0f0f0;">
|
||||
<span style="display: inline-block; padding: 4px 12px; border-radius: 15px; font-weight: 700; font-size: 13px; background: {s_mand_bg}; color: {s_mand_color};">{s_mandatory_passed}/{s_mandatory_total}</span>
|
||||
{f'<div style="font-size: 10px; color: #999; margin-top: 3px;">{s_mandatory_pct}%</div>' if s_mandatory_total > 0 else ''}
|
||||
</td>
|
||||
<td style="padding: 12px 14px; border-bottom: 1px solid #f0f0f0;">{note_html}</td>
|
||||
</tr>'''
|
||||
|
||||
# Summary footer row (if multiple servers)
|
||||
summary_footer = ""
|
||||
if len(valid_infos) > 1:
|
||||
total_passed_all = sum(s.get('passed_count', 0) for s in valid_infos)
|
||||
total_checks_all = sum(s.get('total_checks', 0) for s in valid_infos)
|
||||
avg_pct = round((total_passed_all / total_checks_all) * 100, 1) if total_checks_all > 0 else 0
|
||||
total_mp = sum(s.get('mandatory_passed', 0) for s in valid_infos)
|
||||
total_mt = sum(s.get('mandatory_total', 0) for s in valid_infos)
|
||||
total_mf = sum(s.get('mandatory_failed', 0) for s in valid_infos)
|
||||
|
||||
if avg_pct >= 80:
|
||||
avg_bg = "#e8f5e9"; avg_color = "#2e7d32"
|
||||
elif avg_pct >= 50:
|
||||
avg_bg = "#fff3e0"; avg_color = "#e65100"
|
||||
else:
|
||||
avg_bg = "#ffebee"; avg_color = "#c62828"
|
||||
|
||||
footer_note = f'<span style="color: #c62828; font-weight: 600;">⚠️ Tổng {total_mf} tiêu chí bắt buộc chưa đạt</span>' if total_mf > 0 else '<span style="color: #2e7d32; font-weight: 600;">✅ Tất cả đạt</span>'
|
||||
|
||||
summary_footer = f'''
|
||||
<tr style="background: linear-gradient(135deg, #f5f5f5, #eeeeee); font-weight: 600;">
|
||||
<td colspan="3" style="padding: 14px 16px; text-align: right; color: #555; font-size: 13px; border-top: 2px solid #e0e0e0;">
|
||||
📈 Tổng hợp ({len(valid_infos)} máy chủ):
|
||||
</td>
|
||||
<td style="padding: 14px 16px; text-align: center; border-top: 2px solid #e0e0e0;">
|
||||
<span style="display: inline-block; padding: 4px 12px; border-radius: 15px; font-weight: 700; font-size: 13px; background: {avg_bg}; color: {avg_color};">{avg_pct}%</span>
|
||||
</td>
|
||||
<td style="padding: 14px 16px; text-align: center; border-top: 2px solid #e0e0e0;">
|
||||
<span style="font-size: 13px; color: #333;">{total_mp}/{total_mt}</span>
|
||||
</td>
|
||||
<td style="padding: 14px 16px; border-top: 2px solid #e0e0e0;">{footer_note}</td>
|
||||
</tr>'''
|
||||
|
||||
summary_table_html = f'''
|
||||
<h3 style="color: #1976d2; margin: 25px 0 15px 0; font-size: 18px;">📋 Bảng tổng hợp kết quả</h3>
|
||||
<div style="overflow-x: auto; border-radius: 10px; border: 1px solid #e0e0e0;">
|
||||
<table style="width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; border-radius: 10px; overflow: hidden;">
|
||||
<thead>
|
||||
<tr style="background: linear-gradient(135deg, #1565c0 0%, #1976d2 100%);">
|
||||
<th style="padding: 12px 14px; color: white; font-weight: 600; text-align: center; font-size: 12px; letter-spacing: 0.5px; white-space: nowrap;">STT</th>
|
||||
<th style="padding: 12px 14px; color: white; font-weight: 600; text-align: left; font-size: 12px; letter-spacing: 0.5px; white-space: nowrap;">🖥️ Tên máy chủ</th>
|
||||
<th style="padding: 12px 14px; color: white; font-weight: 600; text-align: left; font-size: 12px; letter-spacing: 0.5px; white-space: nowrap;">🌐 Địa chỉ IP</th>
|
||||
<th style="padding: 12px 14px; color: white; font-weight: 600; text-align: center; font-size: 12px; letter-spacing: 0.5px; white-space: nowrap;">📊 Tỉ lệ đạt</th>
|
||||
<th style="padding: 12px 14px; color: white; font-weight: 600; text-align: center; font-size: 12px; letter-spacing: 0.5px; white-space: nowrap;">🔒 Bắt buộc đạt</th>
|
||||
<th style="padding: 12px 14px; color: white; font-weight: 600; text-align: left; font-size: 12px; letter-spacing: 0.5px; min-width: 180px;">📝 Ghi chú</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summary_rows}
|
||||
</tbody>
|
||||
{f"<tfoot>{summary_footer}</tfoot>" if summary_footer else ""}
|
||||
</table>
|
||||
</div>'''
|
||||
|
||||
# Tạo nội dung HTML với màu tối hơn cho chế độ sáng
|
||||
body_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {{ font-family: 'Segoe UI', Arial, sans-serif; line-height: 1.6; color: #333333; margin: 0; padding: 0; background: #f5f5f5; }}
|
||||
.container {{ max-width: 800px; margin: 0 auto; padding: 20px; }}
|
||||
.header {{ background-color: #1565c0; color: #ffffff; padding: 25px; border-radius: 12px 12px 0 0; text-align: center; }}
|
||||
.content {{ background: #ffffff; padding: 25px; border: 1px solid #e0e0e0; color: #333333; }}
|
||||
.footer {{ background: #f8f9fa; padding: 15px; text-align: center; font-size: 12px; color: #555555; border-radius: 0 0 12px 12px; border: 1px solid #e0e0e0; border-top: none; }}
|
||||
.success {{ color: #1b5e20; background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745; margin-bottom: 20px; }}
|
||||
.info-table {{ width: 100%; border-collapse: collapse; margin: 15px 0; }}
|
||||
.info-table td {{ padding: 10px 12px; border-bottom: 1px solid #eee; color: #333333; }}
|
||||
.info-table td:first-child {{ font-weight: bold; width: 40%; color: #444444; }}
|
||||
ul {{ margin: 10px 0; padding-left: 20px; list-style: none; }}
|
||||
a {{ color: #1565c0; text-decoration: none; }}
|
||||
a:hover {{ text-decoration: underline; }}
|
||||
.note {{ background: #e7f3ff; border-left: 4px solid #2196F3; padding: 12px 15px; margin: 20px 0; border-radius: 5px; font-size: 13px; color: #333333; }}
|
||||
h3 {{ color: #1565c0; }}
|
||||
p {{ color: #333333; }}
|
||||
</style>
|
||||
</head>
|
||||
<body style="color: #333333;">
|
||||
<div class="container">
|
||||
<div class="header" style="background-color: #1565c0;">
|
||||
<h2 style="margin: 0; font-size: 24px; color: #ffffff !important;">🔒 Audit Hardening Tool</h2>
|
||||
<p style="margin: 8px 0 0 0; font-size: 14px; color: #ffffff !important;">Thông báo xử lý hoàn tất</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="success">
|
||||
<strong>✅ Xử lý file audit đã hoàn tất thành công!</strong>
|
||||
</div>
|
||||
|
||||
<table class="info-table">
|
||||
<tr><td>Hệ điều hành:</td><td>{os_type.upper()}</td></tr>
|
||||
<tr><td>Số file đã tạo:</td><td>{len(output_files)} file(s)</td></tr>
|
||||
<tr><td>Thời gian xử lý:</td><td>{processing_time:.2f} giây</td></tr>
|
||||
<tr><td>Thời điểm:</td><td>{timestamp}</td></tr>
|
||||
</table>
|
||||
|
||||
{summary_table_html}
|
||||
|
||||
{'<h3 style="color: #1976d2; margin: 25px 0 15px 0; font-size: 18px;">🖥️ Thông tin chi tiết hệ thống</h3>' + system_info_html if system_info_html else ''}
|
||||
|
||||
<h3 style="color: #1976d2; margin: 25px 0 15px 0; font-size: 18px;">📄 Danh sách file kết quả</h3>
|
||||
<ul>
|
||||
{files_html}
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
<p style="margin-top: 25px; text-align: center;">
|
||||
<a href="{base_url}/my_files" style="background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%); color: white; padding: 12px 25px; border-radius: 8px; display: inline-block; font-weight: 600; text-decoration: none;">
|
||||
📁 Xem tất cả file của tôi
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p style="margin: 0;">Email này được gửi tự động từ Audit Hardening Tool</p>
|
||||
<p style="margin: 5px 0 0 0;">VNPT-MEDIA - An Toàn Thông Tin</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Tạo nội dung text thuần
|
||||
body_text = f"""
|
||||
Audit Hardening Tool - Thông báo xử lý hoàn tất
|
||||
===============================================
|
||||
|
||||
✅ Xử lý file audit đã hoàn tất thành công!
|
||||
|
||||
Thông tin:
|
||||
- Hệ điều hành: {os_type.upper()}
|
||||
- Số file đã tạo: {len(output_files)} file(s)
|
||||
- Thời gian xử lý: {processing_time:.2f} giây
|
||||
- Thời điểm: {timestamp}
|
||||
{system_info_text if system_info_text else ''}
|
||||
Danh sách file kết quả:
|
||||
{files_text}
|
||||
|
||||
|
||||
Truy cập {base_url}/my_files để xem tất cả file của bạn.
|
||||
|
||||
---
|
||||
Email này được gửi tự động từ Audit Hardening Tool
|
||||
VNPT-MEDIA - An Toàn Thông Tin
|
||||
"""
|
||||
|
||||
# Tạo email với file đính kèm nếu là admin
|
||||
email_attachments = None
|
||||
if is_admin and attachments:
|
||||
email_attachments = attachments
|
||||
print(Fore.CYAN + f"[EMAIL] Admin user - attaching {len(attachments)} file(s)" + Fore.RESET)
|
||||
|
||||
# ========== Chọn phương thức gửi email ==========
|
||||
if self.email_method == 'api':
|
||||
# Gửi qua HTTP API
|
||||
return self._send_via_api(
|
||||
to_emails=to_emails,
|
||||
subject=subject,
|
||||
html_content=body_html,
|
||||
attachments=email_attachments
|
||||
)
|
||||
else:
|
||||
# Gửi qua SMTP (phương thức cũ)
|
||||
msg = self._create_email(
|
||||
to_emails=to_emails,
|
||||
subject=subject,
|
||||
body_html=body_html,
|
||||
body_text=body_text,
|
||||
attachments=email_attachments
|
||||
)
|
||||
|
||||
print(Fore.CYAN + f"[EMAIL] Connecting to SMTP server: {self.config.get('smtp_server')}:{self.config.get('smtp_port')}..." + Fore.RESET)
|
||||
|
||||
smtp = smtplib.SMTP(
|
||||
self.config.get('smtp_server', 'email.vnpt.vn'),
|
||||
self.config.get('smtp_port', 587),
|
||||
timeout=self.config.get('timeout', 30)
|
||||
)
|
||||
smtp.ehlo()
|
||||
smtp.starttls()
|
||||
smtp.ehlo()
|
||||
|
||||
smtp.login(
|
||||
self.config.get('sender_email', ''),
|
||||
self.config.get('sender_password', '')
|
||||
)
|
||||
|
||||
smtp.sendmail(
|
||||
self.config.get('sender_email', ''),
|
||||
to_emails,
|
||||
msg.as_string()
|
||||
)
|
||||
|
||||
smtp.quit()
|
||||
|
||||
print(Fore.GREEN + f"[EMAIL] ✓ Notification sent to: {', '.join(to_emails)}" + Fore.RESET)
|
||||
|
||||
# Lưu vào thư mục Sent
|
||||
self._save_to_sent_folder(msg)
|
||||
|
||||
return True
|
||||
|
||||
except smtplib.SMTPException as e:
|
||||
print(Fore.RED + f"[EMAIL] ✗ SMTP Error: {e}" + Fore.RESET)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(Fore.RED + f"[EMAIL] ✗ Error sending email: {e}" + Fore.RESET)
|
||||
return False
|
||||
|
||||
|
||||
# Global instance
|
||||
_email_notifier = None
|
||||
|
||||
|
||||
def get_email_notifier() -> EmailNotifier:
|
||||
"""Lấy instance EmailNotifier (singleton pattern)"""
|
||||
global _email_notifier
|
||||
if _email_notifier is None:
|
||||
_email_notifier = EmailNotifier()
|
||||
return _email_notifier
|
||||
|
||||
|
||||
def send_processing_complete_email(
|
||||
output_files: List[str],
|
||||
os_type: str,
|
||||
processing_time: float = 0,
|
||||
recipients: List[str] = None,
|
||||
base_url: str = "http://localhost:8000",
|
||||
system_info_list: List[dict] = None,
|
||||
attachments: List[str] = None,
|
||||
is_admin: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Hàm tiện ích để gửi email thông báo xử lý hoàn tất
|
||||
|
||||
Args:
|
||||
output_files: Danh sách tên file output
|
||||
os_type: Loại OS đã xử lý
|
||||
processing_time: Thời gian xử lý (giây)
|
||||
recipients: Email nhận (None = dùng mặc định)
|
||||
base_url: URL của ứng dụng
|
||||
system_info_list: Danh sách thông tin hệ thống từ các file đã xử lý
|
||||
attachments: Danh sách đường dẫn file đính kèm
|
||||
is_admin: True nếu admin đăng nhập
|
||||
|
||||
Returns:
|
||||
True nếu thành công
|
||||
"""
|
||||
notifier = get_email_notifier()
|
||||
return notifier.send_processing_notification(
|
||||
output_files=output_files,
|
||||
os_type=os_type,
|
||||
processing_time=processing_time,
|
||||
recipients=recipients,
|
||||
base_url=base_url,
|
||||
system_info_list=system_info_list,
|
||||
attachments=attachments,
|
||||
is_admin=is_admin
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test gửi email
|
||||
print("=" * 50)
|
||||
print(" Test Email Notification")
|
||||
print("=" * 50)
|
||||
|
||||
success = send_processing_complete_email(
|
||||
output_files=["Test_Result_2025-02-03.xlsx"],
|
||||
os_type="centos",
|
||||
processing_time=5.23,
|
||||
base_url="http://localhost:8000"
|
||||
)
|
||||
|
||||
if success:
|
||||
print("\n✅ Email sent successfully!")
|
||||
else:
|
||||
print("\n❌ Failed to send email")
|
||||
Reference in New Issue
Block a user