Files
2026-08-26 14:11:37 +07:00

1659 lines
56 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<script>
var savedTheme = localStorage.getItem('audit_theme');
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="{{ url_for('icon', path='/icon.png') }}">
<title>Công cụ Kiểm tra Hardening - VNPT-MEDIA</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-main);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
position: relative;
overflow-x: hidden;
overflow-y: auto;
}
body::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background:
linear-gradient(135deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.05) 48%, rgba(33, 150, 243, 0.05) 52%, transparent 52%, transparent 100%),
linear-gradient(45deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.08) 48%, rgba(33, 150, 243, 0.08) 52%, transparent 52%, transparent 100%),
linear-gradient(135deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.03) 48%, rgba(33, 150, 243, 0.03) 52%, transparent 52%, transparent 100%);
background-size: 300px 300px, 400px 400px, 500px 500px;
background-position: 0 0, 100px 100px, 200px 0;
z-index: 0;
pointer-events: none;
}
body::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border-style: solid;
border-width: 0 400px 400px 0;
border-color: transparent rgba(33, 150, 243, 0.03) transparent transparent;
z-index: 0;
pointer-events: none;
}
.container {
background: var(--card-bg);
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
max-width: 800px;
width: 100%;
position: relative;
z-index: 1;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo-text {
font-size: 42px;
font-weight: bold;
color: var(--primary-color);
letter-spacing: 6px;
}
.logo-icon {
display: inline-block;
width: 50px;
height: 50px;
background: #1976d2;
border-radius: 50% 0 50% 50%;
position: relative;
margin-right: 10px;
vertical-align: middle;
}
.logo-icon::after {
content: '';
position: absolute;
width: 25px;
height: 25px;
background: var(--card-bg);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
h1 {
color: var(--text-main);
text-align: center;
margin-bottom: 10px;
font-size: 24px;
}
.subtitle {
text-align: center;
color: var(--text-muted);
margin-bottom: 30px;
font-size: 14px;
}
.alert {
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
font-weight: 500;
}
.alert-success {
background-color: var(--success-bg);
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: var(--danger-bg);
color: var(--danger-color);
border: 1px solid #f5c6cb;
}
.alert-warning {
background-color: var(--warning-bg);
color: #856404;
border: 1px solid #ffeaa7;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
color: var(--text-main);
font-weight: 600;
font-size: 14px;
}
.required {
color: var(--danger-color);
}
input[type="file"],
select {
width: 100%;
padding: 12px;
border: 2px solid #667eea;
border-radius: 10px;
background: var(--table-header-bg);
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
input[type="file"] {
border-style: dashed;
}
input[type="file"]:hover,
select:hover {
border-color: #764ba2;
background: #e9ecef;
}
select:focus {
outline: none;
border-color: #764ba2;
box-shadow: 0 0 0 3px rgba(118, 75, 162, 0.1);
}
.hint {
font-size: 12px;
color: var(--text-muted);
margin-top: 5px;
font-style: italic;
}
.auto-detect-info {
margin-top: 10px;
padding: 10px 15px;
border-radius: 8px;
background-color: var(--info-bg);
border-left: 4px solid #2196f3;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.auto-detect-info.success {
background-color: var(--success-bg);
border-left-color: var(--success-color);
}
.auto-detect-info.warning {
background-color: var(--warning-bg);
border-left-color: var(--warning-color);
}
.auto-detect-info.error {
background-color: var(--danger-bg);
border-left-color: var(--danger-color);
}
.auto-detect-info span:first-child {
font-size: 18px;
}
.btn-submit {
width: 100%;
padding: 15px;
background: #1976d2;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-submit:hover {
background: #1565c0;
}
.btn-submit:active {
transform: translateY(0);
}
.info-box {
background-color: var(--success-bg);
border-left: 4px solid #4caf50;
padding: 15px;
border-radius: 5px;
margin-bottom: 25px;
}
.info-box h3 {
color: var(--success-color);
font-size: 16px;
margin-bottom: 10px;
}
.info-box ol {
margin-left: 20px;
color: var(--text-main);
font-size: 14px;
line-height: 1.8;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
color: var(--text-muted);
font-size: 12px;
}
.link {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
}
.link:hover {
text-decoration: underline;
}
.user-info {
position: absolute;
top: 20px;
right: 20px;
background: var(--card-bg);
padding: 10px 20px;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
font-size: 14px;
color: var(--text-main);
}
.user-info strong {
color: var(--primary-color);
}
/* User Status Bar Styles */
.user-status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
margin-bottom: 20px;
border-radius: 10px;
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border: 1px solid #a5d6a7;
}
.user-status-bar.guest {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 1px solid #ffcc80;
}
.user-status-logged-in {
color: var(--success-color);
font-size: 14px;
}
.user-status-logged-in strong {
color: var(--success-color);
}
.user-role {
color: var(--success-color);
font-size: 12px;
}
.user-status-guest {
color: var(--warning-color);
font-size: 14px;
}
.btn-logout {
background: #ef5350;
color: white;
padding: 8px 16px;
border-radius: 20px;
text-decoration: none;
font-size: 13px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-logout:hover {
background: #e53935;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(229, 57, 53, 0.3);
}
.user-actions {
display: flex;
gap: 10px;
align-items: center;
}
.btn-admin {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
text-decoration: none;
font-size: 13px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-admin:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
}
.btn-login-small {
background: #1976d2;
color: white;
padding: 8px 16px;
border-radius: 20px;
text-decoration: none;
font-size: 13px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-login-small:hover {
background: #1565c0;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(21, 101, 192, 0.3);
}
.link-disabled {
color: var(--text-muted);
cursor: not-allowed;
font-weight: 600;
}
/* Loading Screen Styles */
.loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1976d2 0%, #1565c0 50%, #0d47a1 100%);
background-image:
linear-gradient(135deg, rgba(255, 255, 255, 0.05) 25%, transparent 25%),
linear-gradient(225deg, rgba(255, 255, 255, 0.05) 25%, transparent 25%),
linear-gradient(45deg, rgba(255, 255, 255, 0.05) 25%, transparent 25%),
linear-gradient(315deg, rgba(255, 255, 255, 0.05) 25%, transparent 25%),
linear-gradient(135deg, #1976d2 0%, #1565c0 50%, #0d47a1 100%);
background-size: 100px 100px, 100px 100px, 100px 100px, 100px 100px, 100% 100%;
background-position: 0 0, 50px 0, 50px -50px, 0px 50px, 0 0;
z-index: 9999;
justify-content: center;
align-items: center;
flex-direction: column;
}
.loading-overlay.active {
display: flex;
}
.loading-content {
text-align: center;
color: white;
}
.loading-spinner {
width: 80px;
height: 80px;
margin: 0 auto 30px;
position: relative;
}
.loading-spinner::before,
.loading-spinner::after {
content: '';
position: absolute;
border-radius: 50%;
border: 4px solid transparent;
border-top-color: white;
animation: spin 1.5s linear infinite;
}
.loading-spinner::before {
width: 80px;
height: 80px;
top: 0;
left: 0;
}
.loading-spinner::after {
width: 60px;
height: 60px;
top: 10px;
left: 10px;
border-top-color: rgba(255, 255, 255, 0.5);
animation-duration: 1s;
animation-direction: reverse;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 24px;
font-weight: 600;
margin-bottom: 15px;
animation: pulse 2s ease-in-out infinite;
}
.loading-subtext {
font-size: 16px;
opacity: 0.9;
margin-bottom: 30px;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.loading-progress {
width: 300px;
height: 6px;
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
margin: 20px auto;
overflow: hidden;
}
.loading-progress-bar {
height: 100%;
background: var(--card-bg);
border-radius: 3px;
animation: progress 3s ease-in-out infinite;
}
@keyframes progress {
0% {
width: 0%;
}
50% {
width: 70%;
}
100% {
width: 100%;
}
}
/* Responsive Styles */
@media screen and (max-width: 768px) {
body {
padding: 10px;
}
.container {
padding: 25px;
border-radius: 15px;
}
.logo-text {
font-size: 32px;
letter-spacing: 4px;
}
.logo-icon {
width: 40px;
height: 40px;
}
.logo-icon::after {
width: 20px;
height: 20px;
}
h1 {
font-size: 20px;
}
.info-box ol {
font-size: 13px;
}
.loading-text {
font-size: 20px;
}
.loading-subtext {
font-size: 14px;
}
.loading-progress {
width: 250px;
}
}
@media screen and (max-width: 480px) {
body {
padding: 5px;
}
.container {
padding: 15px;
border-radius: 10px;
}
.logo-text {
font-size: 28px;
letter-spacing: 3px;
}
.logo-icon {
width: 35px;
height: 35px;
}
h1 {
font-size: 16px;
}
.subtitle {
font-size: 12px;
}
label {
font-size: 13px;
}
input[type="file"],
select {
font-size: 13px;
padding: 10px;
}
.hint {
font-size: 11px;
}
.btn-submit {
font-size: 14px;
padding: 12px;
}
.info-box {
padding: 12px;
}
.info-box h3 {
font-size: 14px;
}
.info-box ol {
font-size: 12px;
margin-left: 15px;
}
.footer {
font-size: 10px;
}
.footer .link {
font-size: 11px;
}
.loading-spinner {
width: 60px;
height: 60px;
}
.loading-spinner::before {
width: 60px;
height: 60px;
}
.loading-spinner::after {
width: 45px;
height: 45px;
top: 7.5px;
left: 7.5px;
}
.loading-text {
font-size: 18px;
}
.loading-subtext {
font-size: 12px;
}
.loading-progress {
width: 200px;
}
}
</style>
</head>
<body>
{% include 'header.html' %}
<!-- OS Detection Overlay (Trang Trung Gian 1) -->
<div class="loading-overlay" id="osDetectionOverlay"
style="background: linear-gradient(135deg, #1f1c2c 0%, #928DAB 100%); z-index: 10001;">
<div class="loading-content" id="osDetectionContent">
<!-- Spinner / Scan Animation -->
<div class="os-scan-container" id="osScanContainer">
<div class="scanner-ring"></div>
<div class="loading-text" style="margin-top: 30px;">Đang phân tích Hệ Điều Hành...</div>
<div class="loading-subtext">Hệ thống đang quét nội dung file để tự động xác định OS</div>
</div>
<!-- Result / Fallback container (hidden by default) -->
<div class="os-result-container" id="osResultContainer"
style="display: none; background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(15px); padding: 35px; border-radius: 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.2); border: 1px solid rgba(255, 255, 255, 0.3); max-width: 500px; margin: 0 auto; width: 90%;">
<div id="osResultIcon" style="font-size: 50px; margin-bottom: 20px; animation: bounce 2s infinite;">
</div>
<div class="loading-text" id="osResultTitle" style="font-size: 22px;">Không thể tự động phân tích OS
</div>
<div class="loading-subtext" id="osResultMessage" style="margin-bottom: 25px; font-size: 15px;">Vui lòng
chọn cấu hình Hệ điều hành thủ công để tiếp tục:</div>
<div class="form-group" style="text-align: left; margin-bottom: 25px;">
<select id="manual_os_select"
style="width: 100%; padding: 14px; border-radius: 10px; border: 2px solid rgba(255,255,255,0.8); background: rgba(255, 255, 255, 0.95); font-size: 16px; color: var(--text-main); cursor: pointer; transition: all 0.3s ease; outline: none; box-shadow: 0 4px 10px rgba(0,0,0,0.1);">
<option value="" disabled selected>-- Chọn Hệ Điều Hành --</option>
{% for key, value in os_config.items() %}
<option value="{{ key }}">{{ value.name }}</option>
{% endfor %}
</select>
</div>
<button type="button" class="btn-submit" id="btnContinueUpload"
style="background: linear-gradient(135deg, #00b09b 0%, #96c93d 100%); box-shadow: 0 5px 15px rgba(150, 201, 61, 0.4);">
Xác nhận & Tiếp tục
</button>
<button type="button" class="btn-cancel" id="btnCancelUpload"
style="margin-top: 15px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 12px; width: 100%; border-radius: 10px; cursor: pointer; transition: all 0.3s ease; font-weight: 600; font-size: 15px;">
Hủy bỏ
</button>
</div>
</div>
</div>
<style>
.scanner-ring {
width: 100px;
height: 100px;
border-radius: 50%;
border: 4px solid transparent;
border-top-color: #00e5ff;
border-bottom-color: #b700ff;
margin: 0 auto;
animation: spin 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
position: relative;
}
.scanner-ring::before {
content: '';
position: absolute;
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
border-radius: 50%;
border: 4px solid transparent;
border-left-color: #ff007f;
border-right-color: #00ff88;
animation: spin 2s linear infinite reverse;
}
.scanner-ring::after {
content: 'OS';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 20px;
font-weight: bold;
color: white;
}
#btnCancelUpload:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}
#manual_os_select:hover,
#manual_os_select:focus {
border-color: #00e5ff;
box-shadow: 0 0 15px rgba(0, 229, 255, 0.3);
}
</style>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-content">
<!-- VNPT Logo -->
<div style="margin-bottom: 30px;">
<span class="logo-icon" style="width: 60px; height: 60px; background: var(--card-bg);">
<span
style="position: absolute; width: 30px; height: 30px; background: #1976d2; border-radius: 50%; top: 50%; left: 50%; transform: translate(-50%, -50%);"></span>
</span>
<span
style="font-size: 48px; font-weight: bold; color: white; letter-spacing: 8px; vertical-align: middle; margin-left: 10px;">VNPT-MEDIA</span>
</div>
<div class="loading-spinner"></div>
<div class="loading-text">Đang xử lý file...</div>
<div class="loading-subtext">Vui lòng đợi trong khi hệ thống giải mã và tạo báo cáo</div>
<div style="margin-top: 20px; font-size: 14px; opacity: 0.9;">
<p>⏳ Quá trình này có thể mất vài phút...</p>
<p style="margin-top: 10px; font-size: 12px; opacity: 0.7;">Đang giải mã và tạo báo cáo</p>
</div>
</div>
</div>
<!-- Disclaimer Popup -->
<div class="disclaimer-overlay" id="disclaimerOverlay">
<div class="disclaimer-popup">
<div class="disclaimer-header">
<span class="disclaimer-icon">⚠️</span>
<h2>Thông báo</h2>
</div>
<div class="disclaimer-content">
<p>Trang web phục vụ mục đích để <strong>kiểm tra tuân thủ nhanh</strong>.</p>
<p>Một số các tiêu chí có thể chưa được hoàn thiện, đã và đang được cập nhật.</p>
</div>
<div class="disclaimer-footer">
<button class="btn-disclaimer-confirm" onclick="acceptDisclaimer()">
✓ Tôi đã hiểu
</button>
</div>
</div>
</div>
<style>
/* Disclaimer Popup Styles */
.disclaimer-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
z-index: 10000;
justify-content: center;
align-items: center;
}
.disclaimer-overlay.active {
display: flex;
}
.disclaimer-popup {
background: var(--card-bg);
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
overflow: hidden;
animation: popupSlideIn 0.3s ease-out;
}
@keyframes popupSlideIn {
from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.disclaimer-header {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
color: white;
padding: 20px 25px;
display: flex;
align-items: center;
gap: 12px;
}
.disclaimer-icon {
font-size: 28px;
}
.disclaimer-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.disclaimer-content {
padding: 25px;
color: var(--text-main);
line-height: 1.8;
}
.disclaimer-content p {
margin: 0 0 12px 0;
font-size: 15px;
}
.disclaimer-content p:last-child {
margin-bottom: 0;
}
.disclaimer-content strong {
color: var(--primary-color);
}
.disclaimer-footer {
padding: 15px 25px 25px;
text-align: center;
}
.btn-disclaimer-confirm {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
color: white;
border: none;
padding: 14px 40px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.3);
}
.btn-disclaimer-confirm:hover {
background: linear-gradient(135deg, #1565c0 0%, #0d47a1 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.4);
}
@media screen and (max-width: 480px) {
.disclaimer-popup {
width: 95%;
}
.disclaimer-header {
padding: 15px 20px;
}
.disclaimer-header h2 {
font-size: 18px;
}
.disclaimer-content {
padding: 20px;
}
.disclaimer-content p {
font-size: 14px;
}
.btn-disclaimer-confirm {
padding: 12px 30px;
font-size: 14px;
}
}
</style>
<div class="container">
<!-- Top Navigation Tabs -->
<div class="top-nav-tabs"
style="display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; background: rgba(25, 118, 210, 0.05); padding: 15px; border-radius: 12px; border: 1px solid rgba(25, 118, 210, 0.1);">
<a href="{{ url_for('index') }}" class="nav-tab"
style="padding: 10px 20px; background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; box-shadow: 0 4px 10px rgba(25, 118, 210, 0.3);">🏠 Trang chủ</a>
<a href="{{ url_for('my_files') }}" class="nav-tab"
style="padding: 10px 20px; background: var(--card-bg); color: var(--text-main); text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; border: 1px solid var(--border-color); box-shadow: 0 2px 5px rgba(0,0,0,0.05);">📁 File của tôi</a>
{% if user_info and user_info.role == 'admin' %}
<a href="{{ url_for('admin_tools') }}" class="nav-tab"
style="padding: 10px 20px; background: var(--card-bg); color: var(--text-main); text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; border: 1px solid var(--border-color); box-shadow: 0 2px 5px rgba(0,0,0,0.05);">⚙️ Quản trị</a>
{% endif %}
</div>
<div class="logo">
<img src="{{ url_for('icon', path='/icon.png') }}" alt="VNPT-MEDIA Logo"
style="max-width: 60px; height: auto; vertical-align: middle; margin-right: 10px;">
<span class="logo-text">VNPT-MEDIA</span>
</div>
<h1>🔐 Công cụ Kiểm tra Hardening</h1>
<p class="subtitle">Giải mã & Tạo Báo cáo Tuân thủ</p>
{% if message %}
<div class="alert alert-{{ message.type }}">
{{ message.text }}
</div>
{% endif %}
<div class="info-box">
<h3>📋 Hướng dẫn sử dụng:</h3>
<ol>
<li>Tải <strong>Script đánh giá</strong> phù hợp với hệ điều hành (xem tab Công cụ bên dưới)</li>
<li>Chạy script trên máy chủ cần đánh giá để thu thập dữ liệu</li>
<li>Chọn <strong>File kết quả đã mã hóa</strong> (.txt.enc) bên dưới - Có thể chọn nhiều file</li>
<li>Chọn <strong>Hệ điều hành</strong>: để <em>Tự động phát hiện</em> (hệ thống giải mã và đọc nội dung
file) hoặc chọn thủ công</li>
<li>Nhấn <strong>Xử lý</strong> để giải mã và tạo báo cáo Excel</li>
</ol>
<p style="margin-top: 10px; font-size: 13px; color: var(--text-main);">
ℹ️ Chế độ <strong>Tự động phát hiện</strong> xác định OS từ nội dung file (chính xác) thay vì tên file.
</p>
</div>
<!-- Download Tools Section -->
<div class="tools-section">
<div class="tools-header" onclick="toggleToolsSection()">
<h3>📥 Công cụ Đánh giá</h3>
<span id="toolsToggleIcon" class="toggle-icon"></span>
</div>
<div id="toolsContent" class="tools-content">
{% if tools_list %}
<table class="tools-table">
<thead>
<tr>
<th>Hệ điều hành</th>
<th>Phiên bản</th>
<th>Cập nhật</th>
<th>Tải về</th>
</tr>
</thead>
<tbody>
{% for tool in tools_list %}
{% if tool.available %}
<tr>
<td><span class="os-badge {{ tool.os_key }}">{{ tool.icon }} {{ tool.name }}</span></td>
<td><code>{{ tool.version }}</code></td>
<td>{{ tool.updated }}</td>
<td><a href="{{ url_for('download_tool', version_key=tool.version_key) }}"
class="btn-download">⬇️ Tải</a></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% else %}
<p style="text-align: center; color: var(--text-muted); padding: 20px;">
📭 Chưa có công cụ nào được tải lên.
</p>
{% endif %}
<p class="tools-note">
💡 <strong>Hướng dẫn:</strong> Tải script, chạy trên máy chủ với quyền root/admin, sau đó upload
file kết quả (.txt.enc) ở form bên trên.
</p>
</div>
</div>
<!-- Documentation Guidelines Section -->
<div class="tools-section">
<div class="tools-header" onclick="toggleDocsSection()">
<h3>📚 Tài liệu Cấu hình & Hướng dẫn</h3>
<span id="docsToggleIcon" class="toggle-icon"></span>
</div>
<div id="docsContent" class="tools-content">
{% if docs_list %}
<table class="tools-table">
<thead>
<tr>
<th>Tên tài liệu</th>
<th>Phiên bản</th>
<th>Kích thước</th>
<th>Ngày đăng</th>
<th>Tải về</th>
</tr>
</thead>
<tbody>
{% for doc in docs_list %}
{% if doc.available %}
<tr>
<td>
<strong>📄 {{ doc.name }}</strong>
<span style="font-size: 12px; color: var(--text-muted); margin-left: 6px;">({{ doc.file }})</span>
</td>
<td><code>{{ doc.version }}</code></td>
<td>{{ doc.size }}</td>
<td>{{ doc.updated }}</td>
<td style="white-space: nowrap;">
<a href="{{ url_for('download_doc', doc_key=doc.doc_key) }}" class="btn-download" style="white-space: nowrap;">⬇️ Tải</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% else %}
<p style="text-align: center; color: var(--text-muted); padding: 25px;">
📭 Chưa có tài liệu hướng dẫn cấu hình nào được tải lên.
</p>
{% endif %}
</div>
</div>
<style>
/* Tools Section Styles */
.tools-section {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
margin-bottom: 25px;
overflow: hidden;
box-shadow: var(--shadow);
}
.tools-header {
background: var(--table-header-bg);
padding: 15px 20px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.3s ease;
}
.tools-header:hover {
background: var(--hover-bg);
}
.tools-header h3 {
margin: 0;
color: var(--primary-color);
font-size: 16px;
}
.toggle-icon {
color: var(--primary-color);
font-size: 14px;
transition: transform 0.3s ease;
}
.toggle-icon.collapsed {
transform: rotate(-90deg);
}
.tools-content {
padding: 20px;
display: block;
}
.tools-content.collapsed {
display: none;
}
.tools-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.tools-table th,
.tools-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.tools-table th {
background: var(--bg-main);
font-weight: 600;
color: var(--text-main);
}
.tools-table tr:hover {
background: var(--table-row-hover);
}
.os-badge {
padding: 4px 10px;
border-radius: 15px;
font-size: 13px;
font-weight: 500;
}
.os-badge.ubuntu {
background-color: var(--success-bg);
color: var(--success-color);
}
.os-badge.centos {
background-color: var(--danger-bg);
color: var(--danger-color);
}
.os-badge.rhel {
background-color: var(--danger-bg);
color: var(--danger-color);
}
.os-badge.oracle {
background-color: var(--warning-bg);
color: var(--warning-color);
}
.os-badge.windows {
background-color: var(--info-bg);
color: var(--primary-color);
}
.tools-table code {
background-color: var(--success-bg);
padding: 2px 8px;
border-radius: 4px;
color: var(--success-color);
font-family: 'Consolas', monospace;
}
.btn-download {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
color: white;
padding: 6px 14px;
border-radius: 6px;
text-decoration: none;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
display: inline-block;
}
.btn-download:hover {
background: linear-gradient(135deg, #1565c0 0%, #0d47a1 100%);
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(21, 101, 192, 0.3);
}
.btn-download.disabled {
background: #9e9e9e;
cursor: not-allowed;
opacity: 0.7;
}
.btn-download.disabled:hover {
transform: none;
box-shadow: none;
background: #9e9e9e;
}
.tools-note {
margin: 15px 0 0 0;
padding: 12px 15px;
background-color: var(--warning-bg);
border-left: 4px solid #ffc107;
border-radius: 4px;
font-size: 13px;
color: var(--warning-color);
}
@media screen and (max-width: 600px) {
.tools-table th,
.tools-table td {
padding: 8px 10px;
font-size: 12px;
}
.os-badge {
font-size: 11px;
padding: 3px 8px;
}
.btn-download {
padding: 5px 10px;
font-size: 11px;
}
}
</style>
<!-- Upload Form -->
<form id="uploadForm" action="upload" method="post" enctype="multipart/form-data">
<!-- File selection -->
<div class="form-group">
<label for="encrypted_files">
1. File đã mã hóa <span class="required">*</span>
</label>
<input type="file" id="encrypted_files" name="encrypted_files" accept=".enc,.txt" multiple required>
<p class="hint">Chọn một hoặc nhiều file <code>.txt.enc</code> để giải mã và tạo báo cáo.</p>
<!-- Auto-detect status display -->
<div id="autoDetectInfo" class="auto-detect-info" style="display: none;">
<span id="autoDetectIcon">🔍</span>
<span id="autoDetectMessage">Đang chờ chọn file...</span>
</div>
</div>
<!-- OS selection - hidden by default, shown if auto-detect fails -->
<div class="form-group" id="osSelectionGroup" style="display: none;">
<label for="os_type">
2. Hệ điều hành
</label>
<select id="os_type" name="os_type">
<option value="auto" selected>🔍 Tự động phát hiện (khuyến nghị)</option>
{% for key, value in os_config.items() %}
<option value="{{ key }}">{{ value.name }}</option>
{% endfor %}
</select>
<p class="hint" id="osTypeHint">
Để <strong>Tự động</strong>: hệ thống giải mã file và đọc nội dung để xác định OS (chính xác nhất).
Chọn thủ công nếu muốn override.
</p>
</div>
<button type="button" class="btn-submit" id="submitBtn" onclick="handleUploadClick()">
⚡ Xử lý File
</button>
</form>
<div class="footer"
style="text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: var(--text-muted); font-size: 13px;">
<p>Công cụ Kiểm tra Hardening v2.3 - VNPT-MEDIA</p>
</div>
</div>
<script>
// ROOT_PATH: tiền tố URL từ backend (ví dụ: '/audit' khi deploy Docker,
// hoặc '' khi chạy local). Dùng để xây dựng URL API đúng trong mọi môi trường.
const ROOT_PATH = "{{ root_path }}";
window.onerror = function (msg, url, line, col, error) {
alert("JS Error: " + msg + " \nLine: " + line);
};
// Tools section toggle
function toggleToolsSection() {
const content = document.getElementById('toolsContent');
const icon = document.getElementById('toolsToggleIcon');
content.classList.toggle('collapsed');
icon.classList.toggle('collapsed');
}
// Docs section toggle
function toggleDocsSection() {
const content = document.getElementById('docsContent');
const icon = document.getElementById('docsToggleIcon');
content.classList.toggle('collapsed');
icon.classList.toggle('collapsed');
}
// Disclaimer popup handling
const disclaimerOverlay = document.getElementById('disclaimerOverlay');
function showDisclaimer() {
if (!localStorage.getItem('disclaimerAccepted')) {
disclaimerOverlay.classList.add('active');
}
}
function acceptDisclaimer() {
localStorage.setItem('disclaimerAccepted', 'true');
disclaimerOverlay.classList.remove('active');
}
window.addEventListener('DOMContentLoaded', function () {
// Login bắt buộc — user đã đăng nhập, không cần nhập email nữa.
// Hiển thị disclaimer ngay khi vào trang lần đầu (mỗi phiên trình duyệt)
showDisclaimer();
// Đảm bảo Form luôn bắt đầu ở trạng thái Tự động
document.getElementById('os_type').value = 'auto';
});
// ── Email Registration Logic ───────────────────────────────────────────
const VNPT_EMAIL_RE = /^[a-z0-9._%+\-]+@vnpt\.vn$/i;
function showEmailRegOverlay() {
document.getElementById('emailRegOverlay').style.display = 'flex';
}
function hideEmailRegOverlay() {
document.getElementById('emailRegOverlay').style.display = 'none';
}
async function handleEmailRegSubmit() {
const input = document.getElementById('emailRegInput');
const errorDiv = document.getElementById('emailRegError');
const btn = document.getElementById('emailRegBtn');
const email = input.value.trim().toLowerCase();
// Client-side validation
if (!VNPT_EMAIL_RE.test(email)) {
errorDiv.textContent = '⚠️ Email không hợp lệ. Vui lòng nhập email có đuôi @vnpt.vn';
errorDiv.style.display = 'block';
input.style.borderColor = '#e53935';
input.focus();
return;
}
// Reset UI
errorDiv.style.display = 'none';
input.style.borderColor = '#90caf9';
btn.disabled = true;
btn.textContent = 'Đang xử lý...';
try {
const fd = new FormData();
fd.append('email', email);
const resp = await fetch(ROOT_PATH + '/api/register_email', {
method: 'POST',
body: fd
});
const data = await resp.json();
if (data.success) {
localStorage.setItem('vnpt_user_email', email);
hideEmailRegOverlay();
// Hiển thị disclaimer sau khi xác nhận email
showDisclaimer();
} else {
errorDiv.textContent = data.message || 'Lỗi không xác định. Vui lòng thử lại.';
errorDiv.style.display = 'block';
input.style.borderColor = '#e53935';
}
} catch (err) {
errorDiv.textContent = 'Lỗi kết nối. Vui lòng thử lại sau.';
errorDiv.style.display = 'block';
} finally {
btn.disabled = false;
btn.textContent = 'Xác nhận & Tiếp tục';
}
}
async function restoreEmailToSession(email) {
// Đưa email từ localStorage lên session backend, không hỏi lại user
try {
const fd = new FormData();
fd.append('email', email);
const resp = await fetch(ROOT_PATH + '/api/register_email', {
method: 'POST',
body: fd
});
const data = await resp.json();
if (!data.success) {
// Email trong localStorage không hợp lệ → xóa và hỏi lại
localStorage.removeItem('vnpt_user_email');
showEmailRegOverlay();
return;
}
} catch (_) {
// Lỗi mạng → tiếp tục bình thường, không block user
}
showDisclaimer();
}
// ── Upload form logic ──────────────────────────────────────────────────
const form = document.getElementById('uploadForm');
const loadingOverlay = document.getElementById('loadingOverlay');
const osSelect = document.getElementById('os_type');
const fileInput = document.getElementById('encrypted_files');
const autoDetectInfo = document.getElementById('autoDetectInfo');
const autoDetectIcon = document.getElementById('autoDetectIcon');
const autoDetectMessage = document.getElementById('autoDetectMessage');
// OS name display map (mirrors OS_CONFIG)
const osNames = {
'centos': 'CentOS',
'rhel': 'RHEL (Red Hat Enterprise Linux)',
'ubuntu': 'Ubuntu',
'oracle': 'Oracle Linux',
'windows': 'Windows'
};
// Client-side OS patterns for guessing from filename (hint only)
const osPatterns = {
'centos': ['centos', 'centos_linux'],
'rhel': ['rhel', 'red_hat', 'red-hat', 'redhat'],
'ubuntu': ['ubuntu'],
'oracle': ['oracle', 'oracle_linux', 'ol_'],
'windows': ['windows', 'win_server', 'winserver']
};
function detectOSFromFilename(filename) {
if (!filename) return null;
const lower = filename.toLowerCase();
for (const [key, patterns] of Object.entries(osPatterns)) {
for (const p of patterns) {
if (lower.includes(p)) return key;
}
}
return null;
}
function updateAutoDetectInfo(status, message) {
autoDetectInfo.style.display = 'flex';
const iconMap = { success: '✅', warning: '⚠️', error: '❌', info: '🔍' };
const classMap = { success: 'success', warning: 'warning', error: 'error', info: '' };
autoDetectIcon.textContent = iconMap[status] || '🔍';
autoDetectMessage.textContent = message;
autoDetectInfo.className = 'auto-detect-info ' + (classMap[status] || '');
}
// Removed auto OS detect on file change to prevent overlap
// Update hint text when user manually changes OS
osSelect.addEventListener('change', function () {
const hintEl = document.getElementById('osTypeHint');
if (this.value === 'auto') {
hintEl.innerHTML = 'Để <strong>Tự động</strong>: hệ thống giải mã file và đọc nội dung để xác định OS (chính xác nhất). Chọn thủ công nếu muốn override.';
} else {
hintEl.innerHTML = `<strong>Đã chọn thủ công: ${osNames[this.value] || this.value}.</strong> Hệ thống sẽ dùng cấu hình này khi xử lý.`;
}
});
// OS Detection Overlay Logic
const osOverlay = document.getElementById('osDetectionOverlay');
const scanContainer = document.getElementById('osScanContainer');
const resultContainer = document.getElementById('osResultContainer');
const defaultScanHtml = scanContainer.innerHTML; // Cache default HTML
async function handleUploadClick() {
const files = fileInput.files;
if (files.length === 0) {
alert("Vui lòng chọn ít nhất một file để Xử lý.");
return;
}
// Nếu người dùng đã chọn thủ công một OS trên form chính (không phải auto)
if (osSelect.value !== 'auto') {
// Trang trung gian 2 luôn (Xử lý ngay)
startGeneratingReport();
return;
}
// Trang Trung Gian 1: Detect OS
scanContainer.innerHTML = defaultScanHtml; // Reset vể trạng thái đang scan
// Xoá dòng mô tả chi tiết theo yêu cầu
const subtextEl = scanContainer.querySelector('.loading-subtext');
if (subtextEl) {
subtextEl.remove();
}
scanContainer.style.display = 'block';
resultContainer.style.display = 'none';
osOverlay.classList.add('active');
try {
let allDetected = true;
let detectedResults = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Cập nhật giao diện: Đang xử lý file nào
const textEl = scanContainer.querySelector('.loading-text');
if (textEl) {
textEl.innerHTML = `Đang phân tích file (${i + 1}/${files.length}):<br><span style="font-size: 16px; color: #00e5ff; font-weight: normal; margin-top: 10px; display: inline-block;">${file.name}</span>`;
}
const formData = new FormData();
formData.append('files', file);
// Gửi từng file lên API
// Dùng ROOT_PATH để URL đúng trong môi trường Docker (/audit/api/...)
const response = await fetch(ROOT_PATH + '/api/detect_os_from_content', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success && data.detected_os) {
detectedResults.push({ name: file.name, os: data.os_name, os_key: data.detected_os });
} else {
// Nếu có bất kì file nào thất bại thì ngắt và chuyển sang chế độ chọn thủ công
allDetected = false;
break;
}
}
if (allDetected) {
// Thành công: Đã tìm thấy OS cho tất cả các file
let resultHtml = `
<div style="font-size: 60px; margin-bottom: 15px; animation: bounce 1s;">✅</div>
<div class="loading-text" style="font-size: 24px; color: #00ff88;">Hoàn tất phân tích!</div>
<div style="max-height: 150px; overflow-y: auto; margin-top: 15px; text-align: left; border-radius: 10px; background: rgba(0,0,0,0.3); padding: 15px;">
`;
for (let r of detectedResults) {
resultHtml += `<div style="font-size: 14px; margin-bottom: 8px; color: white;">📄 ${r.name} <br> ➔ <strong style="color: #00e5ff;">${r.os}</strong></div>`;
}
resultHtml += `</div>`;
scanContainer.innerHTML = resultHtml;
// Kiểm tra xem tất cả file có cùng OS không
const uniqueOsKeys = [...new Set(detectedResults.map(r => r.os_key))];
if (uniqueOsKeys.length === 1) {
// Tất cả file cùng OS → gửi OS key cụ thể xuống backend
// để backend không cần detect lại (chính xác hơn vì đã phân tích content)
osSelect.value = uniqueOsKeys[0];
console.log('[OS-DETECT] All files same OS, setting os_type=' + uniqueOsKeys[0]);
} else {
// Mixed OS → gửi 'auto' để backend xử lý từng file
osSelect.value = 'auto';
console.log('[OS-DETECT] Mixed OS detected, keeping auto mode');
}
// Delay 2s để user kịp nhìn thấy danh sách kết quả trước khi sang Trang 2
setTimeout(() => {
osOverlay.classList.remove('active');
startGeneratingReport();
}, 2000);
} else {
// Thất bại: Không xác định được ít nhất 1 file
scanContainer.style.display = 'none';
resultContainer.style.display = 'block';
}
} catch (error) {
console.error("Lỗi khi detect OS:", error);
// Fallback to manual selection on error
scanContainer.style.display = 'none';
resultContainer.style.display = 'block';
}
}
// Hàm bắt đầu xử lý (Trang Trung Gian 2)
function startGeneratingReport() {
loadingOverlay.classList.add('active');
// Sử dụng setTimeout để đảm bảo trình duyệt (Chrome/Firefox)
// kịp render giao diện Trang Trung Gian 2 ra màn hình
// trước khi luồng tải bị khoá cứng bởi lệnh 'form.submit()'
setTimeout(() => {
form.submit();
}, 100);
}
// Xử lý khi user bấm Xác nhận & Tiếp tục trong màn hình chọn thủ công
document.getElementById('btnContinueUpload').addEventListener('click', function () {
const manualOs = document.getElementById('manual_os_select').value;
if (!manualOs) {
alert("Vui lòng chọn một Hệ Điều Hành trong danh sách!");
return;
}
// Cập nhật thẻ select bị ẩn trong form chính
osSelect.value = manualOs;
// Ẩn lớp phủ trang 1 và gọi trang 2
osOverlay.classList.remove('active');
startGeneratingReport();
});
// Xử lý khi user bấm Hủy
document.getElementById('btnCancelUpload').addEventListener('click', function () {
osOverlay.classList.remove('active');
// User huỷ upload, form không submit.
});
</script>
</body>
</html>