594 lines
23 KiB
Python
594 lines
23 KiB
Python
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:<b64_enc_key>:<b64_iv>:<b64_ciphertext>
|
|
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
|