Files
fw_rules_builder/app/templates/index.html
T
2026-04-20 08:59:59 +03:00

553 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firewall Rules Builder</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="/static/style.css" rel="stylesheet">
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-dark bg-dark px-3 py-2">
<span class="navbar-brand fw-bold"><i class="bi bi-shield-lock-fill me-2 text-info"></i>Firewall Rules Builder</span>
<div class="d-flex gap-2 align-items-center">
<label class="btn btn-sm btn-outline-warning mb-0" title="Загрузить fw_settings.py">
<i class="bi bi-upload me-1"></i>Загрузить fw_settings.py
<input type="file" id="upload-input" accept=".py" style="display:none" onchange="App.uploadFile(this)">
</label>
<a class="btn btn-sm btn-outline-success" href="/api/export" download="fw_settings.py">
<i class="bi bi-download me-1"></i>Экспорт fw_settings.py
</a>
<button class="btn btn-sm btn-outline-danger" onclick="App.resetAll()">
<i class="bi bi-trash3 me-1"></i>Сбросить всё
</button>
</div>
</nav>
<!-- Tabs -->
<div class="container-fluid mt-3">
<ul class="nav nav-tabs" id="mainTabs">
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-objects"><i class="bi bi-hdd-network me-1"></i>Объекты</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-groups"><i class="bi bi-collection me-1"></i>Группы объектов</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-services"><i class="bi bi-gear me-1"></i>Сервисы</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-svcgroups"><i class="bi bi-layers me-1"></i>Группы сервисов</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-rules"><i class="bi bi-list-check me-1"></i>Правила</a></li>
</ul>
<div class="tab-content mt-3">
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- TAB: Объекты -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="tab-pane fade show active" id="tab-objects">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Объекты (хосты и сети)</h5>
<button class="btn btn-primary btn-sm" onclick="Objects.openModal()">
<i class="bi bi-plus-lg me-1"></i>Добавить объект
</button>
</div>
<div class="mb-2">
<input type="text" class="form-control form-control-sm w-auto d-inline-block" id="obj-search" placeholder="Поиск..." oninput="Objects.filter(this.value)">
<select class="form-select form-select-sm w-auto d-inline-block ms-2" id="obj-type-filter" onchange="Objects.filter()">
<option value="">Все типы</option>
<option value="host">host</option>
<option value="network">network</option>
</select>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover table-bordered align-middle" id="objects-table">
<thead class="table-dark">
<tr>
<th>Ключ</th><th>Тип</th><th>IP</th><th>Префикс</th><th>Шлюз</th><th>Домен</th><th>Описание</th><th>Affinity</th><th style="width:90px"></th>
</tr>
</thead>
<tbody id="objects-tbody"></tbody>
</table>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- TAB: Группы объектов -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="tab-pane fade" id="tab-groups">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Группы объектов</h5>
<button class="btn btn-primary btn-sm" onclick="Groups.openModal()">
<i class="bi bi-plus-lg me-1"></i>Добавить группу
</button>
</div>
<div class="mb-2">
<input type="text" class="form-control form-control-sm w-auto d-inline-block" id="grp-search" placeholder="Поиск..." oninput="Groups.filter(this.value)">
</div>
<div class="table-responsive">
<table class="table table-sm table-hover table-bordered align-middle" id="groups-table">
<thead class="table-dark">
<tr><th>Ключ</th><th>Имя</th><th>Элементы</th><th style="width:90px"></th></tr>
</thead>
<tbody id="groups-tbody"></tbody>
</table>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- TAB: Сервисы -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="tab-pane fade" id="tab-services">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Сервисы</h5>
<button class="btn btn-primary btn-sm" onclick="Services.openModal()">
<i class="bi bi-plus-lg me-1"></i>Добавить сервис
</button>
</div>
<div class="mb-2">
<input type="text" class="form-control form-control-sm w-auto d-inline-block" id="svc-search" placeholder="Поиск..." oninput="Services.filter(this.value)">
<select class="form-select form-select-sm w-auto d-inline-block ms-2" id="svc-proto-filter" onchange="Services.filter()">
<option value="">Все протоколы</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
<option value="icmp-request">icmp</option>
</select>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover table-bordered align-middle">
<thead class="table-dark">
<tr><th>Ключ</th><th>Имя</th><th>Протокол</th><th>Порт источника</th><th>Порт назначения</th><th style="width:90px"></th></tr>
</thead>
<tbody id="services-tbody"></tbody>
</table>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- TAB: Группы сервисов -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="tab-pane fade" id="tab-svcgroups">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Группы сервисов</h5>
<button class="btn btn-primary btn-sm" onclick="SvcGroups.openModal()">
<i class="bi bi-plus-lg me-1"></i>Добавить группу сервисов
</button>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover table-bordered align-middle">
<thead class="table-dark">
<tr><th>Ключ</th><th>Имя</th><th>Сервисы</th><th style="width:90px"></th></tr>
</thead>
<tbody id="svcgroups-tbody"></tbody>
</table>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- TAB: Правила -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="tab-pane fade" id="tab-rules">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Правила Firewall</h5>
<div class="d-flex gap-2">
<button class="btn btn-secondary btn-sm" onclick="Rules.openSpanModal()">
<i class="bi bi-dash-lg me-1"></i>Добавить разделитель
</button>
<button class="btn btn-primary btn-sm" onclick="Rules.openModal()">
<i class="bi bi-plus-lg me-1"></i>Добавить правило
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover table-bordered align-middle" id="rules-table">
<thead class="table-dark">
<tr>
<th style="width:40px">#</th>
<th>Порядок</th>
<th>Имя</th>
<th>Описание</th>
<th>Источник</th>
<th>Назначение</th>
<th>Сервисы</th>
<th>Действие</th>
<th>Лог</th>
<th>IDP</th>
<th>Affinity</th>
<th style="width:100px"></th>
</tr>
</thead>
<tbody id="rules-tbody"></tbody>
</table>
</div>
</div>
</div><!-- /tab-content -->
</div><!-- /container -->
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<!-- MODAL: Объект -->
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<div class="modal fade" id="modal-object" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="modal-object-title">Объект</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="form-object">
<input type="hidden" id="obj-edit-key">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Ключ <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="obj-key" required placeholder="cr">
</div>
<div class="col-md-4">
<label class="form-label">Hostname</label>
<input type="text" class="form-control" id="obj-hostname" placeholder="cr">
</div>
<div class="col-md-4">
<label class="form-label">Тип <span class="text-danger">*</span></label>
<select class="form-select" id="obj-type">
<option value="host">host</option>
<option value="network">network</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">IP-адрес</label>
<input type="text" class="form-control" id="obj-ip" placeholder="172.19.20.2">
</div>
<div class="col-md-2">
<label class="form-label">Префикс</label>
<input type="text" class="form-control" id="obj-prefix" placeholder="24">
</div>
<div class="col-md-4">
<label class="form-label">Шлюз</label>
<input type="text" class="form-control" id="obj-gw" placeholder="172.19.20.1">
</div>
<div class="col-md-2">
<label class="form-label">Домен</label>
<input type="text" class="form-control" id="obj-domain" placeholder="avndr.ru">
</div>
<div class="col-12">
<label class="form-label">Описание</label>
<input type="text" class="form-control" id="obj-description" placeholder="Описание объекта">
</div>
<div class="col-12">
<label class="form-label">Affinity (через запятую)</label>
<input type="text" class="form-control" id="obj-affinity" placeholder="fw_cr, fw_cr_ca">
</div>
</div>
<div id="obj-error" class="alert alert-danger mt-2 d-none"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-primary" onclick="Objects.save()">Сохранить</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<!-- MODAL: Группа объектов -->
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<div class="modal fade" id="modal-group" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="modal-group-title">Группа объектов</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="form-group">
<input type="hidden" id="grp-edit-key">
<div class="row g-2 mb-2">
<div class="col-md-5">
<label class="form-label">Ключ <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="grp-key" required placeholder="set_dr">
</div>
<div class="col-md-7">
<label class="form-label">Имя</label>
<input type="text" class="form-control" id="grp-name" placeholder="set_dr">
</div>
</div>
<label class="form-label">Элементы группы</label>
<div class="row g-2 mb-2">
<div class="col-md-8">
<input type="text" class="form-control" id="grp-item-search" placeholder="Поиск объекта..." oninput="Groups.filterItems(this.value)">
</div>
<div class="col-md-4">
<select class="form-select" id="grp-item-type-filter" onchange="Groups.filterItems()">
<option value="">Все типы</option>
<option value="host">host</option>
<option value="network">network</option>
</select>
</div>
</div>
<div class="row g-2">
<div class="col-md-6">
<div class="fw-bold small mb-1 text-muted">Доступные объекты</div>
<div class="border rounded p-1" style="height:260px;overflow-y:auto;" id="grp-available-list"></div>
</div>
<div class="col-md-6">
<div class="fw-bold small mb-1 text-muted">Выбранные элементы <span class="badge bg-primary" id="grp-selected-count">0</span></div>
<div class="border rounded p-1" style="height:260px;overflow-y:auto;" id="grp-selected-list"></div>
</div>
</div>
<div id="grp-error" class="alert alert-danger mt-2 d-none"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-primary" onclick="Groups.save()">Сохранить</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<!-- MODAL: Сервис -->
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<div class="modal fade" id="modal-service" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="modal-service-title">Сервис</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="form-service">
<input type="hidden" id="svc-edit-key">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Ключ <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="svc-key" required placeholder="ssh">
</div>
<div class="col-md-6">
<label class="form-label">Имя</label>
<input type="text" class="form-control" id="svc-name" placeholder="ssh-22-tcp">
</div>
<div class="col-md-4">
<label class="form-label">Протокол</label>
<select class="form-select" id="svc-proto">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
<option value="icmp-request">icmp-request</option>
<option value="any">any</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Порт источника</label>
<input type="text" class="form-control" id="svc-sport" placeholder="any">
</div>
<div class="col-md-4">
<label class="form-label">Порт назначения</label>
<input type="text" class="form-control" id="svc-dport" placeholder="22">
</div>
</div>
<div id="svc-error" class="alert alert-danger mt-2 d-none"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-primary" onclick="Services.save()">Сохранить</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<!-- MODAL: Группа сервисов -->
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<div class="modal fade" id="modal-svcgroup" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="modal-svcgroup-title">Группа сервисов</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="form-svcgroup">
<input type="hidden" id="sg-edit-key">
<div class="row g-2 mb-2">
<div class="col-md-5">
<label class="form-label">Ключ <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="sg-key" required placeholder="sg_dns">
</div>
<div class="col-md-7">
<label class="form-label">Имя</label>
<input type="text" class="form-control" id="sg-name" placeholder="sg_dns">
</div>
</div>
<label class="form-label">Сервисы в группе</label>
<div class="mb-2">
<input type="text" class="form-control" id="sg-svc-search" placeholder="Поиск сервиса..." oninput="SvcGroups.filterItems(this.value)">
</div>
<div class="row g-2">
<div class="col-md-6">
<div class="fw-bold small mb-1 text-muted">Доступные сервисы</div>
<div class="border rounded p-1" style="height:240px;overflow-y:auto;" id="sg-available-list"></div>
</div>
<div class="col-md-6">
<div class="fw-bold small mb-1 text-muted">Выбранные сервисы <span class="badge bg-primary" id="sg-selected-count">0</span></div>
<div class="border rounded p-1" style="height:240px;overflow-y:auto;" id="sg-selected-list"></div>
</div>
</div>
<div id="sg-error" class="alert alert-danger mt-2 d-none"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-primary" onclick="SvcGroups.save()">Сохранить</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<!-- MODAL: Правило -->
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
<div class="modal fade" id="modal-rule" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="modal-rule-title">Правило</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="form-rule">
<input type="hidden" id="rule-edit-idx">
<div class="row g-2 mb-3">
<div class="col-md-5">
<label class="form-label">Имя правила <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="rule-name" required placeholder="to_dns">
</div>
<div class="col-md-2">
<label class="form-label">Порядок</label>
<input type="number" class="form-control" id="rule-order" placeholder="1000">
</div>
<div class="col-md-3">
<label class="form-label">Действие</label>
<select class="form-select" id="rule-action">
<option value="allow">allow</option>
<option value="deny">deny</option>
<option value="drop">drop</option>
<option value="reject">reject</option>
</select>
</div>
<div class="col-md-1">
<label class="form-label">Лог</label>
<select class="form-select" id="rule-log">
<option value="false">false</option>
<option value="true">true</option>
</select>
</div>
<div class="col-md-1">
<label class="form-label">IDP</label>
<select class="form-select" id="rule-idp">
<option value="false">false</option>
<option value="true">true</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Описание</label>
<input type="text" class="form-control" id="rule-description" placeholder="Описание правила">
</div>
<div class="col-12">
<label class="form-label">Affinity (через запятую)</label>
<input type="text" class="form-control" id="rule-affinity" placeholder="fw_cr, fw_cr_ca">
</div>
</div>
<!-- Источник / Назначение / Сервисы -->
<div class="row g-3">
<!-- Источник -->
<div class="col-md-4">
<div class="card h-100">
<div class="card-header py-1 fw-bold">Источник (src)</div>
<div class="card-body p-2">
<input type="text" class="form-control form-control-sm mb-1" id="rule-src-search" placeholder="Поиск..." oninput="Rules.filterSrc(this.value)">
<div class="fw-bold small text-muted mb-1">Доступные</div>
<div class="border rounded p-1 mb-2" style="height:180px;overflow-y:auto;" id="rule-src-available"></div>
<div class="fw-bold small text-muted mb-1">Выбранные <span class="badge bg-primary" id="rule-src-count">0</span></div>
<div class="border rounded p-1" style="height:120px;overflow-y:auto;" id="rule-src-selected"></div>
</div>
</div>
</div>
<!-- Назначение -->
<div class="col-md-4">
<div class="card h-100">
<div class="card-header py-1 fw-bold">Назначение (dst)</div>
<div class="card-body p-2">
<input type="text" class="form-control form-control-sm mb-1" id="rule-dst-search" placeholder="Поиск..." oninput="Rules.filterDst(this.value)">
<div class="fw-bold small text-muted mb-1">Доступные</div>
<div class="border rounded p-1 mb-2" style="height:180px;overflow-y:auto;" id="rule-dst-available"></div>
<div class="fw-bold small text-muted mb-1">Выбранные <span class="badge bg-primary" id="rule-dst-count">0</span></div>
<div class="border rounded p-1" style="height:120px;overflow-y:auto;" id="rule-dst-selected"></div>
</div>
</div>
</div>
<!-- Сервисы -->
<div class="col-md-4">
<div class="card h-100">
<div class="card-header py-1 fw-bold">Сервисы</div>
<div class="card-body p-2">
<input type="text" class="form-control form-control-sm mb-1" id="rule-svc-search" placeholder="Поиск..." oninput="Rules.filterSvc(this.value)">
<div class="fw-bold small text-muted mb-1">Доступные</div>
<div class="border rounded p-1 mb-2" style="height:180px;overflow-y:auto;" id="rule-svc-available"></div>
<div class="fw-bold small text-muted mb-1">Выбранные <span class="badge bg-primary" id="rule-svc-count">0</span></div>
<div class="border rounded p-1" style="height:120px;overflow-y:auto;" id="rule-svc-selected"></div>
</div>
</div>
</div>
</div>
<div id="rule-error" class="alert alert-danger mt-2 d-none"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-primary" onclick="Rules.save()">Сохранить</button>
</div>
</div>
</div>
</div>
<!-- MODAL: Разделитель (span) -->
<div class="modal fade" id="modal-span" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-secondary text-white">
<h5 class="modal-title">Разделитель (span)</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="span-edit-idx">
<div class="mb-2">
<label class="form-label">Название секции</label>
<input type="text" class="form-control" id="span-name" placeholder="Инфраструктурные правила">
</div>
<div class="mb-2">
<label class="form-label">Порядок</label>
<input type="number" class="form-control" id="span-order" placeholder="1000">
</div>
<div class="mb-2">
<label class="form-label">Affinity (через запятую)</label>
<input type="text" class="form-control" id="span-affinity" placeholder="fw_cr">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-primary" onclick="Rules.saveSpan()">Сохранить</button>
</div>
</div>
</div>
</div>
<!-- Toast уведомления -->
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toast-container"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/app.js"></script>
</body>
</html>