#!/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'
  • 📊 {filename}
  • \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'🏷️ Script v{system_info.get("script_version")}' # 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'''

    🖥️ {system_info.get('hostname', 'Unknown Host')}

    {system_info.get('os_name', 'Unknown OS')}

    {system_info.get('audit_time', '')} {script_version_html}

    📋 Thông tin cơ bản

    {f'' if system_info.get('ip_address') else ''} {f'' if system_info.get('kernel_version') else ''} {f'' if system_info.get('architecture') else ''} {f'' if system_info.get('uptime') else ''} {f'' if system_info.get('timezone') else ''}
    IP Address:{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")}

    ⚙️ Phần cứng

    {f'' if system_info.get('cpu_model') else ''} {f'' if system_info.get('cpu_cores') else ''} {f'' if system_info.get('total_memory') else ''} {f'' if system_info.get('used_memory') else ''} {f'' if system_info.get('free_memory') else ''}
    CPU:{system_info.get("cpu_model", "N/A")}
    CPU Cores:{system_info.get("cpu_cores", "N/A")}
    Total Memory:{system_info.get("total_memory", "N/A")}
    Used Memory:{system_info.get("used_memory", "N/A")}
    Free Memory:{system_info.get("free_memory", "N/A")}

    📊 Tỉ lệ tuân thủ

    {compliance_pct}%
    🔒 Bắt buộc đạt
    {mandatory_passed}/{mandatory_total}
    {f'
    {round((mandatory_passed / mandatory_total) * 100, 1) if mandatory_total > 0 else 0}%
    ' if mandatory_total > 0 else ''}
    📋 Tuỳ chọn đạt
    {optional_passed}/{optional_total}
    Tổng số: {system_info.get('total_checks', 0)} tiêu chí

    🌐 Mạng

    {f'' if system_info.get('primary_interface') else ''} {f'' if system_info.get('mac_address') else ''} {f'' if system_info.get('default_gateway') else ''} {f'' if system_info.get('dns_servers') else ''}
    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")}
    ''' # 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'''
    ❌ Còn {s_mandatory_failed} tiêu chí bắt buộc chưa đạt
    ⚠️ Tỉ lệ đạt chỉ {s_compliance}% (yêu cầu ≥80%)
    ''' elif s_mandatory_failed > 0: note_html = f'''
    ⚠️ Còn {s_mandatory_failed} tiêu chí bắt buộc chưa đạt
    ''' elif s_compliance < 80: note_html = f'''
    ⚠️ Tỉ lệ đạt chỉ {s_compliance}% (yêu cầu ≥80%)
    ''' elif s_mandatory_total > 0: note_html = '''
    ✅ Đạt yêu cầu
    ''' else: note_html = 'Không có dữ liệu' row_bg = "#fafbfc" if idx_s % 2 == 0 else "#ffffff" summary_rows += f''' {idx_s + 1}
    {si.get('hostname', 'N/A')}
    {si.get('os_name', '')}
    {si.get('ip_address', 'N/A')} {s_compliance}%
    {si.get('passed_count', 0)}/{si.get('total_checks', 0)} tiêu chí
    {s_mandatory_passed}/{s_mandatory_total} {f'
    {s_mandatory_pct}%
    ' if s_mandatory_total > 0 else ''} {note_html} ''' # 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'⚠️ Tổng {total_mf} tiêu chí bắt buộc chưa đạt' if total_mf > 0 else '✅ Tất cả đạt' summary_footer = f''' 📈 Tổng hợp ({len(valid_infos)} máy chủ): {avg_pct}% {total_mp}/{total_mt} {footer_note} ''' summary_table_html = f'''

    📋 Bảng tổng hợp kết quả

    {summary_rows} {f"{summary_footer}" if summary_footer else ""}
    STT 🖥️ Tên máy chủ 🌐 Địa chỉ IP 📊 Tỉ lệ đạt 🔒 Bắt buộc đạt 📝 Ghi chú
    ''' # Tạo nội dung HTML với màu tối hơn cho chế độ sáng body_html = 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!
    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}
    {summary_table_html} {'

    🖥️ Thông tin chi tiết hệ thống

    ' + system_info_html if system_info_html else ''}

    📄 Danh sách file kết quả

    📁 Xem tất cả file của tôi

    """ # 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")