import base64 import datetime import json import os import time from os import mkdir from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor from Crypto.Cipher import PKCS1_OAEP, AES from Crypto.PublicKey import RSA from Crypto.Util.Padding import unpad from colorama import Fore from colorama import init from openpyxl import load_workbook from tqdm import tqdm from unidecode import unidecode init() current = datetime.datetime.now() try: os.mkdir("output") except FileExistsError: pass def extract_system_info(lines): """Extract system information from decrypted file content.""" system_info = { 'script_version': '', 'os_name': '', 'kernel_version': '', 'architecture': '', 'hostname': '', 'fqdn': '', 'ip_address': '', 'all_ips': '', 'audit_time': '', 'timezone': '', 'uptime': '', 'cpu_model': '', 'cpu_cores': '', 'total_memory': '', 'used_memory': '', 'free_memory': '', 'swap_total': '', 'primary_interface': '', 'mac_address': '', 'default_gateway': '', 'dns_servers': '', 'selinux_status': '', 'firewall_status': '', 'last_boot': '', 'current_users': '', 'load_average': '', # Compliance statistics 'passed_count': 0, 'failed_count': 0, 'total_checks': 0, 'compliance_percentage': 0.0, # Mandatory/Optional statistics 'mandatory_passed': 0, 'mandatory_failed': 0, 'mandatory_total': 0, 'mandatory_percentage': 0.0, 'optional_passed': 0, 'optional_failed': 0, 'optional_total': 0, 'optional_percentage': 0.0 } passed_count = 0 failed_count = 0 for line in lines: line_stripped = line.strip() # Count PASSED and FAILED checks from JSON format lines if line_stripped.startswith('{') and line_stripped.endswith('}'): if '"PASSED"' in line_stripped: passed_count += 1 elif '"FAILED"' in line_stripped: failed_count += 1 # Parse system information if line_stripped.startswith('Script Version:'): system_info['script_version'] = line_stripped.replace('Script Version:', '').strip() elif line_stripped.startswith('Operating System:'): system_info['os_name'] = line_stripped.replace('Operating System:', '').strip() elif line_stripped.startswith('Kernel Version:'): system_info['kernel_version'] = line_stripped.replace('Kernel Version:', '').strip() elif line_stripped.startswith('Architecture:'): system_info['architecture'] = line_stripped.replace('Architecture:', '').strip() elif line_stripped.startswith('Hostname:'): system_info['hostname'] = line_stripped.replace('Hostname:', '').strip() elif line_stripped.startswith('FQDN:'): system_info['fqdn'] = line_stripped.replace('FQDN:', '').strip() elif line_stripped.startswith('IP Address:'): system_info['ip_address'] = line_stripped.replace('IP Address:', '').strip() elif line_stripped.startswith('All IP Addresses:'): system_info['all_ips'] = line_stripped.replace('All IP Addresses:', '').strip() elif line_stripped.startswith('Audit Time:'): system_info['audit_time'] = line_stripped.replace('Audit Time:', '').strip() elif line_stripped.startswith('Timezone:'): system_info['timezone'] = line_stripped.replace('Timezone:', '').strip() elif line_stripped.startswith('Uptime:'): system_info['uptime'] = line_stripped.replace('Uptime:', '').strip() elif line_stripped.startswith('CPU Model:'): system_info['cpu_model'] = line_stripped.replace('CPU Model:', '').strip() elif line_stripped.startswith('CPU Cores:'): system_info['cpu_cores'] = line_stripped.replace('CPU Cores:', '').strip() elif line_stripped.startswith('Total Memory:'): system_info['total_memory'] = line_stripped.replace('Total Memory:', '').strip() elif line_stripped.startswith('Used Memory:'): system_info['used_memory'] = line_stripped.replace('Used Memory:', '').strip() elif line_stripped.startswith('Free Memory:'): system_info['free_memory'] = line_stripped.replace('Free Memory:', '').strip() elif line_stripped.startswith('Swap Total:'): system_info['swap_total'] = line_stripped.replace('Swap Total:', '').strip() elif line_stripped.startswith('Primary Interface:'): system_info['primary_interface'] = line_stripped.replace('Primary Interface:', '').strip() elif line_stripped.startswith('MAC Address:'): system_info['mac_address'] = line_stripped.replace('MAC Address:', '').strip() elif line_stripped.startswith('Default Gateway:'): system_info['default_gateway'] = line_stripped.replace('Default Gateway:', '').strip() elif line_stripped.startswith('DNS Servers:'): system_info['dns_servers'] = line_stripped.replace('DNS Servers:', '').strip() elif line_stripped.startswith('SELinux Status:'): system_info['selinux_status'] = line_stripped.replace('SELinux Status:', '').strip() elif line_stripped.startswith('Firewall:'): system_info['firewall_status'] = line_stripped.replace('Firewall:', '').strip() elif line_stripped.startswith('Last Boot:'): system_info['last_boot'] = line_stripped.replace('Last Boot:', '').strip() elif line_stripped.startswith('Current Users:'): system_info['current_users'] = line_stripped.replace('Current Users:', '').strip() elif line_stripped.startswith('Load Average:'): system_info['load_average'] = line_stripped.replace('Load Average:', '').strip() # Calculate compliance statistics total_checks = passed_count + failed_count system_info['passed_count'] = passed_count system_info['failed_count'] = failed_count system_info['total_checks'] = total_checks if total_checks > 0: system_info['compliance_percentage'] = round((passed_count / total_checks) * 100, 1) else: system_info['compliance_percentage'] = 0.0 return system_info def detect_os_from_content(lines): """ Detect OS type from decrypted file content. Reads the 'Operating System:' field and maps it to an OS key. Returns the OS key (centos, ubuntu, rhel, oracle, windows) or None if not detected. Example values: 'CentOS Linux 7 (Core)' -> 'centos' 'Red Hat Enterprise ...' -> 'rhel' 'Ubuntu 20.04.x LTS' -> 'ubuntu' 'Oracle Linux 8.x' -> 'oracle' 'Windows Server 2019' -> 'windows' """ os_name_raw = None for line in lines: stripped = line.strip() if stripped.startswith('Operating System:'): os_name_raw = stripped.replace('Operating System:', '').strip() break if not os_name_raw: return None os_lower = os_name_raw.lower() # Order matters: more specific patterns first if 'centos' in os_lower: detected = 'centos' elif 'red hat' in os_lower or 'redhat' in os_lower or 'rhel' in os_lower: detected = 'rhel' elif 'ubuntu' in os_lower: detected = 'ubuntu' elif 'oracle' in os_lower: # Check if version is < 8 # Hỗ trợ cả 'Oracle Linux Server release 7.9' và 'Oracle Linux Server 7.9' import re version_match = re.search(r'(?:release\s+|server\s+|linux\s+)(\d+)', os_lower) if not version_match: # Fallback: tìm số version đầu tiên trong chuỗi (ví dụ 'oracle linux 7.9') version_match = re.search(r'(\d+)\.', os_lower) if version_match and int(version_match.group(1)) < 8: print(Fore.YELLOW + f"[CONTENT-DETECT] Oracle Linux < 8 detected (version {version_match.group(1)}). Routing to CentOS config." + Fore.RESET) detected = 'centos' else: detected = 'oracle' elif 'windows' in os_lower: detected = 'windows' else: detected = None if detected: print(Fore.CYAN + f"[CONTENT-DETECT] 'Operating System: {os_name_raw}' → OS key: '{detected}'" + Fore.RESET) else: print(Fore.YELLOW + f"[CONTENT-DETECT] Unknown OS from content: '{os_name_raw}'" + Fore.RESET) return detected _cached_private_key = None def get_private_key(): """Lấy hoặc nạp RSA private key từ cache trong RAM.""" global _cached_private_key if _cached_private_key is not None: return _cached_private_key script_dir = os.path.dirname(os.path.abspath(__file__)) private_key_paths = [ 'private_key.pem', '../private_key.pem', os.path.join(script_dir, 'private_key.pem'), os.path.join(script_dir, 'keys', 'private_key.pem'), 'keys/private_key.pem' ] for path in private_key_paths: if os.path.exists(path): with open(path, 'rb') as f: _cached_private_key = RSA.import_key(f.read()) return _cached_private_key raise FileNotFoundError("private_key.pem not found! Please ensure it exists in the script directory.") def _decrypt_rsa_block_worker(block: bytes) -> bytes: """Hàm worker cấp module phục vụ cho ProcessPoolExecutor / ThreadPoolExecutor giải mã RSA.""" private_key = get_private_key() cipher = PKCS1_OAEP.new(private_key) return cipher.decrypt(block) def is_hybrid_encrypted(raw_text: str) -> bool: """Kiểm tra xem nội dung file có thuộc chuẩn mã hóa lai MỚI (HYBRID AES-256 + RSA) hay không.""" s = raw_text.strip() return s.startswith("HYBRID_V1:") or s.startswith("HYBRID_AES:") or s.startswith("HYBRID:") def decrypt_hybrid(raw_text: str) -> bytes: """ Giải mã định dạng Mã hóa Lai (Hybrid AES-256 + RSA) siêu tốc (~0.005s). Cấu trúc: HYBRID_V1::: hoặc JSON: HYBRID_V1:{"enc_key": "...", "iv": "...", "data": "..."} """ s = raw_text.strip() # Loại bỏ tiền tố prefix for prefix in ("HYBRID_V1:", "HYBRID_AES:", "HYBRID:"): if s.startswith(prefix): payload = s[len(prefix):].strip() break b64_enc_key = "" b64_iv = "" b64_ciphertext = "" if payload.startswith("{"): # Format JSON data_json = json.loads(payload) b64_enc_key = data_json.get("enc_key", "") b64_iv = data_json.get("iv", "") b64_ciphertext = data_json.get("data", "") else: # Format chuỗi phân cách bởi dấu hai chấm ':' parts = payload.split(":") if len(parts) >= 3: b64_enc_key = parts[0] b64_iv = parts[1] b64_ciphertext = parts[2] if not b64_enc_key or not b64_iv or not b64_ciphertext: raise ValueError("Cấu trúc file Mã hóa Lai (Hybrid Encryption) không hợp lệ!") enc_key = base64.b64decode(b64_enc_key) iv = base64.b64decode(b64_iv) ciphertext = base64.b64decode(b64_ciphertext) # 1. Giải mã khóa AES bằng RSA Private Key (chỉ 1 block RSA duy nhất) private_key = get_private_key() cipher_rsa = PKCS1_OAEP.new(private_key) aes_key = cipher_rsa.decrypt(enc_key) # 2. Giải mã dữ liệu bằng khóa AES-256-CBC (siêu nhanh) cipher_aes = AES.new(aes_key, AES.MODE_CBC, iv) decrypted_padded = cipher_aes.decrypt(ciphertext) # Thử gỡ padding PKCS7 try: plaintext_bytes = unpad(decrypted_padded, AES.block_size) except ValueError: plaintext_bytes = decrypted_padded return plaintext_bytes def decryption_read_header(filename: str, max_blocks: int = 30) -> list[str]: """ Giải mã nhanh phần header của file .enc để đọc thông tin OS. Tự động hỗ trợ cả chuẩn MỚI (Hybrid AES+RSA) và chuẩn CŨ (Pure RSA). """ with open(filename, "r", encoding="utf-8", errors="replace") as f: raw_text = f.read() if is_hybrid_encrypted(raw_text): # Format MỚI: Giải mã siêu tốc bằng AES-256 + RSA (< 0.005s) plaintext_bytes = decrypt_hybrid(raw_text) lines = plaintext_bytes.decode("utf-8", errors="replace").splitlines() return lines[:100] # Format CŨ: Giải mã N block RSA đầu tiên (~5KB) ciphertext = base64.b64decode(raw_text.encode('utf-8')) private_key = get_private_key() block_size = private_key.size_in_bytes() cipher = PKCS1_OAEP.new(private_key) total_blocks = (len(ciphertext) + block_size - 1) // block_size num_blocks = min(max_blocks, total_blocks) blocks = [] for i in range(num_blocks): offset = i * block_size block = ciphertext[offset:offset + block_size] try: blocks.append(cipher.decrypt(block)) except Exception: break header_text = b"".join(blocks).decode('utf-8', errors='replace') return header_text.splitlines() def decryption_write(filename): """ Giải mã toàn bộ file mã hóa. Tự động chuyển đổi giữa chuẩn MỚI (Hybrid AES-256 + RSA) và chuẩn CŨ (Pure RSA Multi-processing). """ with open(filename, "r", encoding="utf-8", errors="replace") as f: raw_text = f.read() start_time = time.time() if is_hybrid_encrypted(raw_text): # ⚡ Chuẩn MỚI (Hybrid AES-256 + RSA): Tốc độ tức thì < 0.01 giây plaintext_bytes = decrypt_hybrid(raw_text) plaintext = plaintext_bytes.decode("utf-8", errors="replace") print(Fore.GREEN + f" [HYBRID AES+RSA] Decryption done in {time.time() - start_time:.3f}s" + Fore.RESET) else: # 🐢 Chuẩn CŨ (Pure RSA): Giải mã đa luồng / đa tiến trình tối ưu CPU Multi-core ciphertext = base64.b64decode(raw_text.encode('utf-8')) private_key = get_private_key() block_size = private_key.size_in_bytes() total_blocks = (len(ciphertext) + block_size - 1) // block_size block_list = [ciphertext[i * block_size:(i + 1) * block_size] for i in range(total_blocks)] max_workers = min(16, (os.cpu_count() or 4) * 2) try: with ThreadPoolExecutor(max_workers=max_workers) as executor: decrypted_blocks = list(executor.map(_decrypt_rsa_block_worker, block_list)) except Exception: cipher = PKCS1_OAEP.new(private_key) decrypted_blocks = [cipher.decrypt(b) for b in block_list] plaintext = b"".join(decrypted_blocks).decode('utf-8', errors='replace') print(Fore.GREEN + f" [LEGACY RSA] Decryption done in {time.time() - start_time:.1f}s ({total_blocks} blocks, {max_workers} workers)" + Fore.RESET) output_path = os.path.basename(filename).replace(".enc", "") with open(output_path, 'w', encoding="utf-8") as f: f.write(plaintext) def check_file_exist(filename): isFile = os.path.isfile(filename) if isFile: return True else: return False def string_normalization(string): return unidecode(str(string)) def extract_failed_logs(lines): """Extract detailed log blocks for each FAILED criteria from decrypted log lines. Returns a dict mapping normalized criteria name -> log detail text. """ failed_logs = {} i = 0 separator = '######################################' dash_separator = '-------------------' stderr_begin = '########## STDERR BEGIN ##########' stderr_end = '########## STDERR END ##########' while i < len(lines): line_stripped = lines[i].strip() # Look for FAILED JSON lines if line_stripped.startswith('{') and line_stripped.endswith('}') and '"FAILED"' in line_stripped: try: parsed = json.loads(line_stripped) criteria_name = str(list(parsed.keys())[0]) normalized_name = string_normalization(criteria_name) # Collect all lines after this FAILED line until the next JSON criteria line log_lines = [] j = i + 1 while j < len(lines): next_line = lines[j].strip() # Stop when we hit the next JSON criteria line (PASSED or FAILED) if next_line.startswith('{') and next_line.endswith('}'): try: next_parsed = json.loads(next_line) next_val = str(list(next_parsed.values())[0]) if next_val in ('PASSED', 'FAILED'): break except (json.JSONDecodeError, IndexError): pass # Skip separator lines and STDERR markers if next_line == separator or next_line == dash_separator: j += 1 continue if next_line == stderr_begin or next_line == stderr_end: j += 1 continue # Skip empty lines at the beginning if not log_lines and not next_line: j += 1 continue log_lines.append(lines[j].rstrip()) j += 1 # Remove trailing empty lines while log_lines and not log_lines[-1].strip(): log_lines.pop() if log_lines: log_text = '\n'.join(log_lines) # Excel cell limit is 32767 characters if len(log_text) > 32767: log_text = log_text[:32764] + '...' failed_logs[normalized_name] = log_text i = j # Skip to where we stopped continue except (json.JSONDecodeError, IndexError): pass i += 1 return failed_logs def run_report(checklist_file, config_file, file_input): with open(file_input, "r", encoding="utf8") as file: line = file.read().splitlines() wb = load_workbook(filename=checklist_file) ws = wb.worksheets[0] with open(config_file, 'r', encoding="utf8") as j: contents = json.loads(j.read()) index_audit = contents['data'] mandatory_rows = set() for row_idx in range(1, ws.max_row + 1): cell_val = ws.cell(row=row_idx, column=5).value if cell_val and str(cell_val).strip().lower() == 'x': mandatory_rows.add(row_idx) mandatory_passed = 0 mandatory_failed = 0 optional_passed = 0 optional_failed = 0 failed_logs = extract_failed_logs(line) audit_results = {} for one_audit in line: try: parsed_data = json.loads(one_audit) result = str(list(parsed_data.keys())[0]) value = str(list(parsed_data.values())[0]) normalized_key = string_normalization(result) audit_results[normalized_key] = (value, one_audit) except Exception: continue for audit_pattern in index_audit: index_a = list(audit_pattern.keys())[0] index_pattern = string_normalization(list(audit_pattern.values())[0]) entry = audit_results.get(index_pattern) if entry is None: continue status, raw_line = entry is_mandatory = int(index_a) in mandatory_rows if index_a.isdigit() else False if "FAILED" in raw_line: ws['D{}'.format(index_a)] = 'x' if index_pattern in failed_logs: ws['F{}'.format(index_a)] = failed_logs[index_pattern] if is_mandatory: mandatory_failed += 1 else: optional_failed += 1 else: ws['C{}'.format(index_a)] = 'x' if is_mandatory: mandatory_passed += 1 else: optional_passed += 1 # Find hostname and time dynamically in the file hostname = "unknown" time_generate = "unknown" for l in line: if l.strip().startswith("Hostname:"): hostname = l.replace("Hostname:", "").strip().split(" ")[0] elif l.strip().startswith("Audit Time:"): time_generate = l.replace("Audit Time:", "").strip().replace("-", "_").replace(":", "_").replace(" ", "-") # Also support old format with "Time:" elif l.strip().startswith("Time:") and time_generate == "unknown": time_generate = l.replace("Time:", "").strip().replace("-", "_").replace(":", "_").replace(" ", "-") # Create file name based strictly on hostname and time file_name = f'{hostname}_{time_generate}.xlsx' wb.save(filename=file_name) if check_file_exist(file_name): print(Fore.GREEN + "\nChecklist Done: {}".format(file_name) + Fore.RESET) else: print(Fore.RED + "Error creating Excel file!" + Fore.RESET) # Extract and return system info system_info = extract_system_info(line) # Add mandatory/optional statistics mandatory_total = mandatory_passed + mandatory_failed optional_total = optional_passed + optional_failed system_info['mandatory_passed'] = mandatory_passed system_info['mandatory_failed'] = mandatory_failed system_info['mandatory_total'] = mandatory_total system_info['mandatory_percentage'] = round((mandatory_passed / mandatory_total) * 100, 1) if mandatory_total > 0 else 0.0 system_info['optional_passed'] = optional_passed system_info['optional_failed'] = optional_failed system_info['optional_total'] = optional_total system_info['optional_percentage'] = round((optional_passed / optional_total) * 100, 1) if optional_total > 0 else 0.0 # Recalculate compliance stats based on config-matched criteria (consistent with Excel) # extract_system_info counts ALL PASSED/FAILED lines in raw text (including criteria # not in the checklist config), which can differ from the Excel result. # Use the actual config-matched counts for accurate web display. config_passed = mandatory_passed + optional_passed config_failed = mandatory_failed + optional_failed config_total = config_passed + config_failed if config_total > 0: system_info['passed_count'] = config_passed system_info['failed_count'] = config_failed system_info['total_checks'] = config_total system_info['compliance_percentage'] = round((config_passed / config_total) * 100, 1) return system_info def run_generate_excel(checklist_file, config_file, filename): global current current = datetime.datetime.now() file_decrypt = os.path.basename(filename).replace(".enc", "") if check_file_exist(file_decrypt): os.remove(file_decrypt) decryption_write(filename) else: decryption_write(filename) if check_file_exist(file_decrypt): system_info = run_report(checklist_file, config_file, file_decrypt) return system_info return None