This commit is contained in:
Luftmensch
2026-08-26 14:11:37 +07:00
commit e528419f9a
135 changed files with 413970 additions and 0 deletions
+805
View File
@@ -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")