feat: add versioned multi-component assessment registry

This commit is contained in:
Luftmensch
2026-09-04 10:33:36 +07:00
parent 4f4c7df4ed
commit 9f34cf8afd
9 changed files with 906 additions and 38 deletions
+69 -7
View File
@@ -428,6 +428,54 @@
</div>
{% if user_info and user_info.role == 'admin' %}
<div class="card" id="profile-registry">
<div class="card-header"><h2 class="card-title">🧩 Registry Category → Component → Profile → Version</h2></div>
<p style="color: var(--text-muted);">Các phiên bản là bất biến; chỉ phiên bản active được dùng khi audit. Mọi thao tác bên dưới chỉ dành cho admin.</p>
<div class="form-row">
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_category_v2') }}">
<div class="form-group"><label>Category ID</label><input name="id" placeholder="database-platforms" required></div>
<div class="form-group"><label>Tên category</label><input name="name" required></div>
<button class="btn btn-primary" type="submit">Thêm category</button>
</form>
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_component_v2') }}">
<div class="form-group"><label>Category ID</label><input name="category_id" required></div>
<div class="form-group"><label>Component ID</label><input name="id" required></div>
<div class="form-group"><label>Tên component</label><input name="name" required></div>
<button class="btn btn-primary" type="submit">Thêm component</button>
</form>
<form class="registry-json-form" data-endpoint="{{ url_for('registry_create_profile_v2') }}">
<div class="form-group"><label>Component ID</label><input name="component_id" required></div>
<div class="form-group"><label>Profile ID</label><input name="id" required></div>
<div class="form-group"><label>Tên profile</label><input name="name" required></div>
<div class="form-group"><label>Aliases (phân cách dấu phẩy)</label><input name="aliases"></div>
<button class="btn btn-primary" type="submit">Thêm profile</button>
</form>
</div>
{% for category in registry.categories %}
<h3>{{ category.name }} <code>{{ category.id }}</code></h3>
{% for component in category.components %}
<h4>{{ component.name }} <code>{{ component.id }}</code></h4>
{% for profile in component.profiles %}
<div style="border:1px solid var(--border-color);padding:14px;margin:10px 0;border-radius:8px;">
<strong>{{ profile.name }}</strong> <code>{{ profile.id }}</code>
<form action="{{ url_for('registry_upload_version_v2', profile_id=profile.id) }}" method="post" enctype="multipart/form-data" class="upload-form registry-action-form">
<div class="form-row">
<div class="form-group"><label>Version</label><input name="version" required></div>
<div class="form-group"><label>Checklist XLSX</label><input type="file" name="checklist_file" accept=".xlsx" required></div>
<div class="form-group"><label>Rule JSON</label><input type="file" name="config_file" accept=".json" required></div>
<label><input type="checkbox" name="activate" value="true"> Kích hoạt ngay</label>
</div>
<button class="btn btn-primary" type="submit">Upload version</button>
</form>
{% for version in profile.versions %}
<span style="display:inline-block;margin:5px;"><code>{{ version.version }}</code>{% if version.active %} ✅ active{% else %}
<form class="registry-action-form" style="display:inline" action="{{ url_for('registry_activate_v2', profile_id=profile.id, version=version.version) }}" method="post"><button class="btn" type="submit">Activate</button></form>{% endif %}</span>
{% endfor %}
<form class="registry-action-form" style="display:inline" action="{{ url_for('registry_rollback_v2', profile_id=profile.id) }}" method="post"><button class="btn" type="submit">Rollback</button></form>
</div>
{% endfor %}{% endfor %}{% endfor %}
</div>
<!-- Upload Checklist and Rule Configuration -->
<div class="card">
<div class="card-header">
@@ -681,14 +729,28 @@
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadForm.addEventListener(eventName, () => uploadForm.classList.add('dragover'), false);
});
if (uploadForm) {
['dragenter', 'dragover'].forEach(eventName => uploadForm.addEventListener(eventName, () => uploadForm.classList.add('dragover'), false));
['dragleave', 'drop'].forEach(eventName => uploadForm.addEventListener(eventName, () => uploadForm.classList.remove('dragover'), false));
}
['dragleave', 'drop'].forEach(eventName => {
uploadForm.addEventListener(eventName, () => uploadForm.classList.remove('dragover'), false);
});
document.querySelectorAll('.registry-json-form').forEach(form => form.addEventListener('submit', async event => {
event.preventDefault();
const body = Object.fromEntries(new FormData(form).entries());
if (body.aliases !== undefined) body.aliases = body.aliases.split(',').map(x => x.trim()).filter(Boolean);
const response = await fetch(form.dataset.endpoint, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)});
const result = await response.json();
if (!response.ok) return alert(result.detail || 'Registry update failed');
location.reload();
}));
document.querySelectorAll('.registry-action-form').forEach(form => form.addEventListener('submit', async event => {
event.preventDefault();
const response = await fetch(form.action, {method: 'POST', body: new FormData(form)});
const result = await response.json();
if (!response.ok) return alert(result.detail || 'Registry action failed');
location.reload();
}));
</script>
</body>
</html>
</html>
+59 -2
View File
@@ -1309,6 +1309,29 @@
</p>
</div>
<div class="form-group">
<label for="profile_category">2. Loại thành phần đánh giá</label>
<select id="profile_category">
<option value="">Tất cả loại thành phần</option>
</select>
</div>
<div class="form-group">
<label for="profile_component">3. Thành phần</label>
<select id="profile_component">
<option value="">Tất cả thành phần</option>
</select>
</div>
<div class="form-group">
<label for="profile_id">4. Assessment Profile (tùy chọn)</label>
<select id="profile_id" name="profile_id">
<option value="">Dùng profile đang active theo OS tự động</option>
{% for profile in active_profiles %}
<option value="{{ profile.id }}" data-category="{{ profile.category_id }}" data-component="{{ profile.component_id }}">{{ profile.category }} → {{ profile.component }} → {{ profile.name }} ({{ profile.version }})</option>
{% endfor %}
</select>
<p class="hint">Chọn profile cho OS, Database, Web Server, Network Device...; để trống vẫn giữ auto-detect OS tương thích cũ.</p>
</div>
<button type="button" class="btn-submit" id="submitBtn" onclick="handleUploadClick()">
⚡ Xử lý File
</button>
@@ -1453,11 +1476,45 @@
const form = document.getElementById('uploadForm');
const loadingOverlay = document.getElementById('loadingOverlay');
const osSelect = document.getElementById('os_type');
const profileSelect = document.getElementById('profile_id');
const categorySelect = document.getElementById('profile_category');
const componentSelect = document.getElementById('profile_component');
const fileInput = document.getElementById('encrypted_files');
const autoDetectInfo = document.getElementById('autoDetectInfo');
const autoDetectIcon = document.getElementById('autoDetectIcon');
const autoDetectMessage = document.getElementById('autoDetectMessage');
const profileOptions = [...profileSelect.options].filter(option => option.value).map(option => ({
value: option.value,
label: option.textContent,
category: option.dataset.category,
component: option.dataset.component
}));
const categories = [...new Set(profileOptions.map(option => option.category))];
categories.forEach(category => categorySelect.add(new Option(category, category)));
function refreshComponents() {
componentSelect.innerHTML = '<option value="">Tất cả thành phần</option>';
const components = [...new Set(profileOptions
.filter(option => !categorySelect.value || option.category === categorySelect.value)
.map(option => option.component))];
components.forEach(component => componentSelect.add(new Option(component, component)));
refreshProfiles();
}
function refreshProfiles() {
const selected = profileSelect.value;
profileSelect.innerHTML = '<option value="">Dùng profile đang active theo OS tự động</option>';
profileOptions.filter(option =>
(!categorySelect.value || option.category === categorySelect.value) &&
(!componentSelect.value || option.component === componentSelect.value)
).forEach(option => profileSelect.add(new Option(option.label, option.value)));
if ([...profileSelect.options].some(option => option.value === selected)) profileSelect.value = selected;
}
categorySelect.addEventListener('change', refreshComponents);
componentSelect.addEventListener('change', refreshProfiles);
refreshComponents();
// OS name display map (mirrors OS_CONFIG)
const osNames = {
'centos': 'CentOS',
@@ -1522,7 +1579,7 @@
}
// 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') {
if (profileSelect.value || osSelect.value !== 'auto') {
// Trang trung gian 2 luôn (Xử lý ngay)
startGeneratingReport();
return;
@@ -1656,4 +1713,4 @@
</script>
</body>
</html>
</html>