yeah, baby!

This commit is contained in:
Alexander gritsenko 2026-04-19 19:55:21 +03:00
parent 0c407fc431
commit 9133480863
10 changed files with 6256 additions and 1 deletions

View File

@ -1 +0,0 @@
hello world

Binary file not shown.

664
app/app.py Normal file
View File

@ -0,0 +1,664 @@
"""
app.py Flask backend для Firewall Rules Builder
"""
import json
import os
import sys
import re
from flask import Flask, jsonify, request, render_template, send_from_directory
app = Flask(__name__, template_folder='templates', static_folder='static')
# ─── Хранилище данных ────────────────────────────────────────────────────────
DATA_FILE = os.path.join(os.path.dirname(__file__), 'data.json')
EMPTY_DATA = {
"servers": {},
"nets": {},
"groups": {},
"services": {},
"service_groups": {},
"rules": []
}
def load_data():
if os.path.exists(DATA_FILE):
with open(DATA_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return json.loads(json.dumps(EMPTY_DATA))
def save_data(data):
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# ─── Главная страница ─────────────────────────────────────────────────────────
@app.route('/')
def index():
return render_template('index.html')
# ─── API: Объекты (servers + nets) ───────────────────────────────────────────
@app.route('/api/objects', methods=['GET'])
def get_objects():
data = load_data()
result = {}
for k, v in data['servers'].items():
result[k] = v
for k, v in data['nets'].items():
result[k] = v
return jsonify(result)
@app.route('/api/objects', methods=['POST'])
def create_object():
data = load_data()
obj = request.json
key = obj.get('key', '').strip()
if not key:
return jsonify({'error': 'Ключ обязателен'}), 400
obj_type = obj.get('type', 'host')
obj_data = {
'hostname': obj.get('hostname', key),
'ip': obj.get('ip', ''),
'prefix': obj.get('prefix', '24'),
'gw': obj.get('gw', ''),
'domain': obj.get('domain', ''),
'description': obj.get('description', ''),
'type': obj_type,
'affinity': obj.get('affinity', []),
}
if obj_type == 'network':
if key in data['nets']:
return jsonify({'error': f'Объект "{key}" уже существует'}), 400
data['nets'][key] = obj_data
else:
if key in data['servers']:
return jsonify({'error': f'Объект "{key}" уже существует'}), 400
data['servers'][key] = obj_data
save_data(data)
return jsonify({'ok': True, 'key': key})
@app.route('/api/objects/<key>', methods=['PUT'])
def update_object(key):
data = load_data()
obj = request.json
obj_type = obj.get('type', 'host')
obj_data = {
'hostname': obj.get('hostname', key),
'ip': obj.get('ip', ''),
'prefix': obj.get('prefix', '24'),
'gw': obj.get('gw', ''),
'domain': obj.get('domain', ''),
'description': obj.get('description', ''),
'type': obj_type,
'affinity': obj.get('affinity', []),
}
# Удаляем из обоих словарей (тип мог измениться)
data['servers'].pop(key, None)
data['nets'].pop(key, None)
if obj_type == 'network':
data['nets'][key] = obj_data
else:
data['servers'][key] = obj_data
save_data(data)
return jsonify({'ok': True})
@app.route('/api/objects/<key>', methods=['DELETE'])
def delete_object(key):
data = load_data()
data['servers'].pop(key, None)
data['nets'].pop(key, None)
save_data(data)
return jsonify({'ok': True})
# ─── API: Группы объектов ─────────────────────────────────────────────────────
@app.route('/api/groups', methods=['GET'])
def get_groups():
data = load_data()
return jsonify(data['groups'])
@app.route('/api/groups', methods=['POST'])
def create_group():
data = load_data()
grp = request.json
key = grp.get('key', '').strip()
if not key:
return jsonify({'error': 'Ключ обязателен'}), 400
if key in data['groups']:
return jsonify({'error': f'Группа "{key}" уже существует'}), 400
data['groups'][key] = {
'name': grp.get('name', key),
'items': grp.get('items', [])
}
save_data(data)
return jsonify({'ok': True, 'key': key})
@app.route('/api/groups/<key>', methods=['PUT'])
def update_group(key):
data = load_data()
grp = request.json
data['groups'][key] = {
'name': grp.get('name', key),
'items': grp.get('items', [])
}
save_data(data)
return jsonify({'ok': True})
@app.route('/api/groups/<key>', methods=['DELETE'])
def delete_group(key):
data = load_data()
data['groups'].pop(key, None)
save_data(data)
return jsonify({'ok': True})
# ─── API: Сервисы ─────────────────────────────────────────────────────────────
@app.route('/api/services', methods=['GET'])
def get_services():
data = load_data()
return jsonify(data['services'])
@app.route('/api/services', methods=['POST'])
def create_service():
data = load_data()
svc = request.json
key = svc.get('key', '').strip()
if not key:
return jsonify({'error': 'Ключ обязателен'}), 400
if key in data['services']:
return jsonify({'error': f'Сервис "{key}" уже существует'}), 400
data['services'][key] = {
'name': svc.get('name', key),
'sport': svc.get('sport', 'any'),
'dport': svc.get('dport', ''),
'proto': svc.get('proto', 'tcp'),
}
save_data(data)
return jsonify({'ok': True, 'key': key})
@app.route('/api/services/<key>', methods=['PUT'])
def update_service(key):
data = load_data()
svc = request.json
data['services'][key] = {
'name': svc.get('name', key),
'sport': svc.get('sport', 'any'),
'dport': svc.get('dport', ''),
'proto': svc.get('proto', 'tcp'),
}
save_data(data)
return jsonify({'ok': True})
@app.route('/api/services/<key>', methods=['DELETE'])
def delete_service(key):
data = load_data()
data['services'].pop(key, None)
save_data(data)
return jsonify({'ok': True})
# ─── API: Группы сервисов ─────────────────────────────────────────────────────
@app.route('/api/service_groups', methods=['GET'])
def get_service_groups():
data = load_data()
return jsonify(data['service_groups'])
@app.route('/api/service_groups', methods=['POST'])
def create_service_group():
data = load_data()
sg = request.json
key = sg.get('key', '').strip()
if not key:
return jsonify({'error': 'Ключ обязателен'}), 400
if key in data['service_groups']:
return jsonify({'error': f'Группа сервисов "{key}" уже существует'}), 400
data['service_groups'][key] = {
'name': sg.get('name', key),
'items': sg.get('items', [])
}
save_data(data)
return jsonify({'ok': True, 'key': key})
@app.route('/api/service_groups/<key>', methods=['PUT'])
def update_service_group(key):
data = load_data()
sg = request.json
data['service_groups'][key] = {
'name': sg.get('name', key),
'items': sg.get('items', [])
}
save_data(data)
return jsonify({'ok': True})
@app.route('/api/service_groups/<key>', methods=['DELETE'])
def delete_service_group(key):
data = load_data()
data['service_groups'].pop(key, None)
save_data(data)
return jsonify({'ok': True})
# ─── API: Правила ─────────────────────────────────────────────────────────────
@app.route('/api/rules', methods=['GET'])
def get_rules():
data = load_data()
return jsonify(data['rules'])
@app.route('/api/rules', methods=['POST'])
def create_rule():
data = load_data()
rule = request.json
data['rules'].append(rule)
save_data(data)
return jsonify({'ok': True, 'index': len(data['rules']) - 1})
@app.route('/api/rules/<int:idx>', methods=['PUT'])
def update_rule(idx):
data = load_data()
if idx < 0 or idx >= len(data['rules']):
return jsonify({'error': 'Индекс вне диапазона'}), 404
data['rules'][idx] = request.json
save_data(data)
return jsonify({'ok': True})
@app.route('/api/rules/<int:idx>', methods=['DELETE'])
def delete_rule(idx):
data = load_data()
if idx < 0 or idx >= len(data['rules']):
return jsonify({'error': 'Индекс вне диапазона'}), 404
data['rules'].pop(idx)
save_data(data)
return jsonify({'ok': True})
@app.route('/api/rules/reorder', methods=['POST'])
def reorder_rules():
data = load_data()
new_order = request.json.get('order', [])
try:
data['rules'] = [data['rules'][i] for i in new_order]
except IndexError:
return jsonify({'error': 'Неверный порядок'}), 400
save_data(data)
return jsonify({'ok': True})
# ─── API: Экспорт fw_settings.py ─────────────────────────────────────────────
@app.route('/api/export', methods=['GET'])
def export_settings():
data = load_data()
content = generate_fw_settings(data)
return app.response_class(
response=content,
status=200,
mimetype='text/plain; charset=utf-8',
headers={'Content-Disposition': 'attachment; filename=fw_settings.py'}
)
def py_repr(val):
"""Представление Python-значения в виде строки."""
if isinstance(val, str):
# Используем двойные кавычки
escaped = val.replace('\\', '\\\\').replace('"', '\\"')
return f'"{escaped}"'
elif isinstance(val, list):
items = ', '.join(py_repr(i) for i in val)
return f'[{items}]'
elif isinstance(val, dict):
pairs = ', '.join(f'{py_repr(k)}: {py_repr(v)}' for k, v in val.items())
return '{' + pairs + '}'
elif isinstance(val, bool):
return 'True' if val else 'False'
elif val is None:
return 'None'
else:
return repr(val)
def generate_fw_settings(data):
lines = []
# servers
lines.append('servers = {')
for key, obj in data['servers'].items():
lines.append(f' {py_repr(key)}: {{')
lines.append(f' "hostname": {py_repr(obj.get("hostname", key))},')
lines.append(f' "ip": {py_repr(obj.get("ip", ""))},')
lines.append(f' "prefix": {py_repr(obj.get("prefix", "24"))},')
lines.append(f' "gw": {py_repr(obj.get("gw", ""))},')
lines.append(f' "domain": {py_repr(obj.get("domain", ""))},')
lines.append(f' "description": {py_repr(obj.get("description", ""))},')
lines.append(f' "type": "host",')
affinity = obj.get('affinity', [])
lines.append(f' "affinity": {py_repr(affinity)},')
lines.append(f' }},')
lines.append('}')
lines.append('')
# nets
lines.append('# networks')
lines.append('nets = {')
for key, obj in data['nets'].items():
lines.append(f' {py_repr(key)}: {{')
lines.append(f' "hostname": {py_repr(obj.get("hostname", key))},')
lines.append(f' "description": {py_repr(obj.get("description", ""))},')
lines.append(f' "domain": {py_repr(obj.get("domain", ""))},')
lines.append(f' "ip": {py_repr(obj.get("ip", ""))},')
prefix = obj.get('prefix', 24)
try:
prefix = int(prefix)
except (ValueError, TypeError):
pass
lines.append(f' "prefix": {prefix},')
lines.append(f' "type": "network",')
affinity = obj.get('affinity', [])
lines.append(f' "affinity": {py_repr(affinity)},')
lines.append(f' }},')
lines.append('}')
lines.append('')
lines.append('')
# groups
lines.append('groups = {')
for key, grp in data['groups'].items():
items = grp.get('items', [])
items_repr = '[' + ', '.join(f'{{"hostname": {py_repr(i.get("hostname",""))}}}' for i in items) + ']'
lines.append(f' {py_repr(key)}: {{"name": {py_repr(grp.get("name", key))}, "items": {items_repr}}},')
lines.append('}')
lines.append('')
lines.append('')
# services
lines.append('# services')
lines.append('services = {')
for key, svc in data['services'].items():
lines.append(f' {py_repr(key)}: {{')
lines.append(f' "name": {py_repr(svc.get("name", key))},')
lines.append(f' "sport": {py_repr(svc.get("sport", "any"))},')
lines.append(f' "dport": {py_repr(svc.get("dport", ""))},')
lines.append(f' "proto": {py_repr(svc.get("proto", "tcp"))},')
lines.append(f' }},')
lines.append('}')
lines.append('')
# service groups
lines.append('# service groups')
lines.append('service_groups = {')
for key, sg in data['service_groups'].items():
items = sg.get('items', [])
lines.append(f' {py_repr(key)}: {{')
lines.append(f' "name": {py_repr(sg.get("name", key))},')
lines.append(f' "items": [')
for svc_key in items:
lines.append(f' services[{py_repr(svc_key)}],')
lines.append(f' ],')
lines.append(f' }},')
lines.append('}')
lines.append('')
# rules
lines.append('# rules')
lines.append('rules = [')
for rule in data['rules']:
lines.append(' {')
lines.append(f' "name": {py_repr(rule.get("name", ""))},')
lines.append(f' "order": {rule.get("order", 0)},')
rtype = rule.get('type', 'rule')
lines.append(f' "type": {py_repr(rtype)},')
if rtype == 'span':
affinity = rule.get('affinity', [])
lines.append(f' "affinity": {py_repr(affinity)},')
else:
lines.append(f' "description": {py_repr(rule.get("description", ""))},')
# src_list
src_list = rule.get('src_list', [])
src_parts = _rule_ref_list(src_list)
lines.append(f' "src_list": [{", ".join(src_parts)}],')
# dst_list
dst_list = rule.get('dst_list', [])
dst_parts = _rule_ref_list(dst_list)
lines.append(f' "dst_list": [{", ".join(dst_parts)}],')
# service_list
svc_list = rule.get('service_list', [])
svc_parts = [f'services[{py_repr(s)}]' for s in svc_list] if svc_list else []
lines.append(f' "service_list": [{", ".join(svc_parts)}],')
# service_group_list
sg_list = rule.get('service_group_list', [])
sg_parts = [f'service_groups[{py_repr(s)}]' for s in sg_list] if sg_list else []
sg_val = f'[{", ".join(sg_parts)}]' if sg_parts else 'None'
lines.append(f' "service_group_list": {sg_val},')
lines.append(f' "action": {py_repr(rule.get("action", "allow"))},')
lines.append(f' "log": {py_repr(rule.get("log", "false"))},')
lines.append(f' "idp": {py_repr(rule.get("idp", "false"))},')
affinity = rule.get('affinity', [])
lines.append(f' "affinity": {py_repr(affinity)},')
lines.append(' },')
lines.append(']')
lines.append('')
return '\n'.join(lines)
def _rule_ref_list(items):
"""Генерирует Python-ссылки для src_list/dst_list."""
parts = []
for item in items:
ref_type = item.get('ref_type', 'group')
ref_key = item.get('ref_key', '')
if ref_type == 'server':
parts.append(f'servers[{py_repr(ref_key)}]')
elif ref_type == 'net':
parts.append(f'nets[{py_repr(ref_key)}]')
else:
parts.append(f'groups[{py_repr(ref_key)}]')
return parts
# ─── API: Импорт fw_settings.py ──────────────────────────────────────────────
@app.route('/api/import', methods=['POST'])
def import_settings():
"""Импортирует данные из fw_settings.py через exec()."""
try:
fw_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'fw_settings.py')
if not os.path.exists(fw_path):
return jsonify({'error': 'fw_settings.py не найден'}), 404
ns = {}
with open(fw_path, 'r', encoding='utf-8') as f:
source = f.read()
exec(compile(source, fw_path, 'exec'), ns)
raw_servers = ns.get('servers', {})
raw_nets = ns.get('nets', {})
raw_groups = ns.get('groups', {})
raw_services = ns.get('services', {})
raw_service_groups = ns.get('service_groups', {})
raw_rules = ns.get('rules', [])
data = {
'servers': {},
'nets': {},
'groups': {},
'services': {},
'service_groups': {},
'rules': []
}
# Серверы
for k, v in raw_servers.items():
data['servers'][k] = {
'hostname': v.get('hostname', k),
'ip': v.get('ip', ''),
'prefix': str(v.get('prefix', '24')),
'gw': v.get('gw', ''),
'domain': v.get('domain', ''),
'description': v.get('description', ''),
'type': 'host',
'affinity': v.get('affinity', []),
}
# Сети
for k, v in raw_nets.items():
data['nets'][k] = {
'hostname': v.get('hostname', k),
'ip': v.get('ip', ''),
'prefix': str(v.get('prefix', '24')),
'gw': v.get('gw', ''),
'domain': v.get('domain', ''),
'description': v.get('description', ''),
'type': 'network',
'affinity': v.get('affinity', []),
}
# Группы
for k, v in raw_groups.items():
items = []
for item in v.get('items', []):
if isinstance(item, dict):
items.append({'hostname': item.get('hostname', '')})
data['groups'][k] = {
'name': v.get('name', k),
'items': items
}
# Сервисы
for k, v in raw_services.items():
data['services'][k] = {
'name': v.get('name', k),
'sport': v.get('sport', 'any'),
'dport': v.get('dport', ''),
'proto': v.get('proto', 'tcp'),
}
# Группы сервисов — сохраняем ключи сервисов
svc_by_name = {v['name']: k for k, v in raw_services.items()}
for k, v in raw_service_groups.items():
item_keys = []
for svc in v.get('items', []):
if isinstance(svc, dict):
svc_name = svc.get('name', '')
# Ищем ключ по имени
found_key = svc_by_name.get(svc_name)
if found_key:
item_keys.append(found_key)
else:
# Ищем по совпадению значений
for sk, sv in raw_services.items():
if sv == svc:
item_keys.append(sk)
break
data['service_groups'][k] = {
'name': v.get('name', k),
'items': item_keys
}
# Правила — конвертируем ссылки в ref_type/ref_key
all_server_ids = {id(v): k for k, v in raw_servers.items()}
all_net_ids = {id(v): k for k, v in raw_nets.items()}
all_group_ids = {id(v): k for k, v in raw_groups.items()}
all_svc_ids = {id(v): k for k, v in raw_services.items()}
all_sg_ids = {id(v): k for k, v in raw_service_groups.items()}
for rule in raw_rules:
rtype = rule.get('type', 'rule')
new_rule = {
'name': rule.get('name', ''),
'order': rule.get('order', 0),
'type': rtype,
'affinity': rule.get('affinity', []),
}
if rtype != 'span':
new_rule['description'] = rule.get('description', '')
new_rule['action'] = rule.get('action', 'allow')
new_rule['log'] = str(rule.get('log', 'false'))
new_rule['idp'] = str(rule.get('idp', 'false'))
# src_list
src_list = []
for item in (rule.get('src_list') or []):
ref = _resolve_ref(item, all_server_ids, all_net_ids, all_group_ids)
if ref:
src_list.append(ref)
new_rule['src_list'] = src_list
# dst_list
dst_list = []
for item in (rule.get('dst_list') or []):
ref = _resolve_ref(item, all_server_ids, all_net_ids, all_group_ids)
if ref:
dst_list.append(ref)
new_rule['dst_list'] = dst_list
# service_list
svc_list = []
for svc in (rule.get('service_list') or []):
key = all_svc_ids.get(id(svc))
if key:
svc_list.append(key)
new_rule['service_list'] = svc_list
# service_group_list
sg_list = []
for sg in (rule.get('service_group_list') or []):
key = all_sg_ids.get(id(sg))
if key:
sg_list.append(key)
new_rule['service_group_list'] = sg_list
data['rules'].append(new_rule)
save_data(data)
return jsonify({'ok': True, 'message': 'Импорт выполнен успешно'})
except Exception as e:
return jsonify({'error': str(e)}), 500
def _resolve_ref(item, server_ids, net_ids, group_ids):
obj_id = id(item)
if obj_id in server_ids:
return {'ref_type': 'server', 'ref_key': server_ids[obj_id]}
if obj_id in net_ids:
return {'ref_type': 'net', 'ref_key': net_ids[obj_id]}
if obj_id in group_ids:
return {'ref_type': 'group', 'ref_key': group_ids[obj_id]}
# Попробуем по hostname
if isinstance(item, dict):
hostname = item.get('hostname', '')
if hostname in server_ids.values():
return {'ref_type': 'server', 'ref_key': hostname}
if hostname in net_ids.values():
return {'ref_type': 'net', 'ref_key': hostname}
name = item.get('name', '')
if name in group_ids.values():
return {'ref_type': 'group', 'ref_key': name}
return None
# ─── API: Получить все данные сразу ──────────────────────────────────────────
@app.route('/api/all', methods=['GET'])
def get_all():
return jsonify(load_data())
if __name__ == '__main__':
app.run(debug=True, port=5000)

2080
app/data.json Normal file

File diff suppressed because it is too large Load Diff

1073
app/static/app.js Normal file

File diff suppressed because it is too large Load Diff

239
app/static/style.css Normal file
View File

@ -0,0 +1,239 @@
/* ─── Общие стили ─────────────────────────────────────────────────────────── */
body {
background: #f4f6f9;
font-size: 13px;
}
.navbar-brand {
font-size: 1.1rem;
letter-spacing: 0.5px;
}
/* ─── Таблицы ─────────────────────────────────────────────────────────────── */
.table th {
font-size: 12px;
white-space: nowrap;
}
.table td {
vertical-align: middle;
font-size: 12px;
}
.table-responsive {
max-height: calc(100vh - 200px);
overflow-y: auto;
}
/* Закреплённый заголовок таблицы */
.table thead th {
position: sticky;
top: 0;
z-index: 2;
}
/* ─── Строки правил ───────────────────────────────────────────────────────── */
tr.rule-allow {
background-color: #e8f5e9 !important;
}
tr.rule-deny, tr.rule-drop, tr.rule-reject {
background-color: #fce4d6 !important;
}
tr.rule-span td {
background-color: #d6e4f0 !important;
color: #1f4e79;
font-weight: bold;
text-align: center;
font-size: 13px;
}
/* ─── Бейджи типов ────────────────────────────────────────────────────────── */
.badge-host {
background-color: #0d6efd;
color: white;
padding: 2px 7px;
border-radius: 4px;
font-size: 11px;
}
.badge-network {
background-color: #198754;
color: white;
padding: 2px 7px;
border-radius: 4px;
font-size: 11px;
}
.badge-action-allow {
background-color: #198754;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
}
.badge-action-deny, .badge-action-drop, .badge-action-reject {
background-color: #dc3545;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
}
/* ─── Элементы выбора (picker) ────────────────────────────────────────────── */
.picker-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 6px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 2px;
font-size: 12px;
transition: background 0.1s;
}
.picker-item:hover {
background: #e9ecef;
}
.picker-item .item-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.picker-item .item-badge {
font-size: 10px;
margin-left: 4px;
flex-shrink: 0;
}
.picker-item .item-action {
flex-shrink: 0;
margin-left: 4px;
color: #6c757d;
font-size: 14px;
line-height: 1;
}
.picker-item .item-action:hover {
color: #0d6efd;
}
.picker-item-selected {
background: #cfe2ff;
}
.picker-item-selected:hover {
background: #b6d4fe;
}
.picker-item-selected .item-action:hover {
color: #dc3545;
}
/* ─── Кнопки действий в таблице ───────────────────────────────────────────── */
.btn-action {
padding: 2px 6px;
font-size: 12px;
}
/* ─── Теги в ячейках таблицы ─────────────────────────────────────────────── */
.cell-tags {
display: flex;
flex-wrap: wrap;
gap: 2px;
}
.cell-tag {
background: #e9ecef;
border-radius: 3px;
padding: 1px 5px;
font-size: 11px;
white-space: nowrap;
}
.cell-tag-group {
background: #d1ecf1;
color: #0c5460;
}
.cell-tag-server {
background: #d4edda;
color: #155724;
}
.cell-tag-net {
background: #fff3cd;
color: #856404;
}
.cell-tag-svc {
background: #f8d7da;
color: #721c24;
}
.cell-tag-sgr {
background: #e2d9f3;
color: #432874;
}
/* ─── Модальные окна ──────────────────────────────────────────────────────── */
.modal-header {
padding: 10px 16px;
}
.modal-body {
padding: 16px;
}
.modal-footer {
padding: 8px 16px;
}
/* ─── Toast ───────────────────────────────────────────────────────────────── */
.toast {
min-width: 260px;
}
/* ─── Скроллбар ───────────────────────────────────────────────────────────── */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #adb5bd;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #6c757d;
}
/* ─── Drag handle для правил ──────────────────────────────────────────────── */
.drag-handle {
cursor: grab;
color: #adb5bd;
font-size: 16px;
padding: 0 4px;
}
.drag-handle:active {
cursor: grabbing;
}
tr.dragging {
opacity: 0.5;
background: #cfe2ff !important;
}
tr.drag-over {
border-top: 2px solid #0d6efd;
}

548
app/templates/index.html Normal file
View File

@ -0,0 +1,548 @@
<!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">
<button class="btn btn-sm btn-outline-warning" onclick="App.importSettings()">
<i class="bi bi-upload me-1"></i>Импорт fw_settings.py
</button>
<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>
</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>

330
fw_report.py Normal file
View File

@ -0,0 +1,330 @@
"""
fw_report.py генератор Excel-отчёта по правилам firewall.
Читает данные из fw_settings.py и создаёт файл fw_report.xlsx со страницами:
- Правила (Rules)
- Объекты (Objects)
- Группы объектов (Groups)
- Сервисы (Services)
- Группы сервисов (Service Groups)
"""
import sys
import os
# Добавляем текущую директорию в путь, чтобы импортировать fw_settings
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fw_settings import servers, nets, groups, services, service_groups, rules
from openpyxl import Workbook
from openpyxl.styles import (
Font, PatternFill, Alignment, Border, Side, GradientFill
)
from openpyxl.utils import get_column_letter
# ─── Цветовая палитра ────────────────────────────────────────────────────────
CLR_HEADER_BG = "1F4E79" # тёмно-синий — заголовки таблиц
CLR_HEADER_FG = "FFFFFF" # белый текст
CLR_SPAN_BG = "D6E4F0" # голубой — строки-разделители (span)
CLR_SPAN_FG = "1F4E79" # тёмно-синий текст
CLR_ALLOW_BG = "E2EFDA" # светло-зелёный — allow
CLR_DENY_BG = "FCE4D6" # светло-красный — deny
CLR_ALT_BG = "F2F2F2" # светло-серый — чётные строки
CLR_WHITE = "FFFFFF"
THIN = Side(style="thin", color="BFBFBF")
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
def make_fill(hex_color: str) -> PatternFill:
return PatternFill("solid", fgColor=hex_color)
def header_font(bold=True) -> Font:
return Font(name="Calibri", bold=bold, color=CLR_HEADER_FG, size=11)
def cell_font(bold=False, color="000000") -> Font:
return Font(name="Calibri", bold=bold, color=color, size=10)
def apply_header_row(ws, row: int, headers: list[str]):
"""Записывает строку заголовков с форматированием."""
for col, text in enumerate(headers, start=1):
c = ws.cell(row=row, column=col, value=text)
c.font = header_font()
c.fill = make_fill(CLR_HEADER_BG)
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
c.border = BORDER
def set_col_widths(ws, widths: list[int]):
for i, w in enumerate(widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
def fmt_cell(ws, row, col, value, bold=False, bg=None, align="left", wrap=True, color="000000"):
c = ws.cell(row=row, column=col, value=value)
c.font = cell_font(bold=bold, color=color)
c.alignment = Alignment(horizontal=align, vertical="center", wrap_text=wrap)
c.border = BORDER
if bg:
c.fill = make_fill(bg)
return c
# ─── Вспомогательные функции для извлечения имён ────────────────────────────
def obj_display(obj: dict) -> str:
"""Возвращает строку вида 'hostname (ip/prefix)' или 'name' для группы."""
if "ip" in obj and "prefix" in obj:
return f"{obj.get('hostname', '')} ({obj['ip']}/{obj['prefix']})"
if "hostname" in obj:
return obj["hostname"]
if "name" in obj:
return obj["name"]
return str(obj)
def group_display(grp: dict) -> str:
return grp.get("name", "")
def svc_display(svc: dict) -> str:
return svc.get("name", "")
def list_to_str(items, fn) -> str:
if not items:
return ""
return "\n".join(fn(i) for i in items)
# ─── Лист: Объекты ───────────────────────────────────────────────────────────
def build_objects_sheet(wb: Workbook):
ws = wb.create_sheet("Объекты")
ws.freeze_panes = "A2"
headers = ["Имя", "Тип", "IP-адрес", "Префикс", "Шлюз", "Домен", "Описание"]
apply_header_row(ws, 1, headers)
set_col_widths(ws, [20, 10, 16, 8, 16, 16, 40])
all_objects = {}
for k, v in servers.items():
all_objects[k] = v
for k, v in nets.items():
all_objects[k] = v
for row_idx, (key, obj) in enumerate(all_objects.items(), start=2):
bg = CLR_WHITE if row_idx % 2 == 0 else CLR_ALT_BG
fmt_cell(ws, row_idx, 1, obj.get("hostname", key), bg=bg)
fmt_cell(ws, row_idx, 2, obj.get("type", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 3, obj.get("ip", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 4, str(obj.get("prefix", "")), bg=bg, align="center")
fmt_cell(ws, row_idx, 5, obj.get("gw", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 6, obj.get("domain", ""), bg=bg)
fmt_cell(ws, row_idx, 7, obj.get("description", ""), bg=bg)
ws.row_dimensions[1].height = 30
# ─── Лист: Группы объектов ───────────────────────────────────────────────────
def build_groups_sheet(wb: Workbook):
ws = wb.create_sheet("Группы объектов")
ws.freeze_panes = "A2"
headers = ["Имя группы", "Элементы группы"]
apply_header_row(ws, 1, headers)
set_col_widths(ws, [35, 60])
for row_idx, (key, grp) in enumerate(groups.items(), start=2):
bg = CLR_WHITE if row_idx % 2 == 0 else CLR_ALT_BG
items = grp.get("items", [])
items_str = "\n".join(i.get("hostname", str(i)) for i in items) if items else ""
fmt_cell(ws, row_idx, 1, grp.get("name", key), bold=True, bg=bg)
c = fmt_cell(ws, row_idx, 2, items_str, bg=bg)
# авто-высота строки по числу элементов
ws.row_dimensions[row_idx].height = max(15, 15 * max(1, len(items)))
ws.row_dimensions[1].height = 30
# ─── Лист: Сервисы ───────────────────────────────────────────────────────────
def build_services_sheet(wb: Workbook):
ws = wb.create_sheet("Сервисы")
ws.freeze_panes = "A2"
headers = ["Ключ", "Имя сервиса", "Протокол", "Порт источника", "Порт назначения"]
apply_header_row(ws, 1, headers)
set_col_widths(ws, [28, 38, 14, 18, 18])
for row_idx, (key, svc) in enumerate(services.items(), start=2):
bg = CLR_WHITE if row_idx % 2 == 0 else CLR_ALT_BG
fmt_cell(ws, row_idx, 1, key, bg=bg)
fmt_cell(ws, row_idx, 2, svc.get("name", ""), bg=bg)
fmt_cell(ws, row_idx, 3, svc.get("proto", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 4, svc.get("sport", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 5, svc.get("dport", ""), bg=bg, align="center")
ws.row_dimensions[1].height = 30
# ─── Лист: Группы сервисов ───────────────────────────────────────────────────
def build_service_groups_sheet(wb: Workbook):
ws = wb.create_sheet("Группы сервисов")
ws.freeze_panes = "A2"
headers = ["Ключ", "Имя группы", "Сервисы (имя | протокол | dport)"]
apply_header_row(ws, 1, headers)
set_col_widths(ws, [25, 35, 60])
for row_idx, (key, sg) in enumerate(service_groups.items(), start=2):
bg = CLR_WHITE if row_idx % 2 == 0 else CLR_ALT_BG
items = sg.get("items", [])
lines = []
for svc in items:
lines.append(
f"{svc.get('name','')} | {svc.get('proto','')} | {svc.get('dport','')}"
)
items_str = "\n".join(lines) if lines else ""
fmt_cell(ws, row_idx, 1, key, bg=bg)
fmt_cell(ws, row_idx, 2, sg.get("name", key), bold=True, bg=bg)
fmt_cell(ws, row_idx, 3, items_str, bg=bg)
ws.row_dimensions[row_idx].height = max(15, 15 * max(1, len(items)))
ws.row_dimensions[1].height = 30
# ─── Лист: Правила ───────────────────────────────────────────────────────────
def _extract_src_dst(lst) -> str:
"""Преобразует список src/dst (может содержать dict серверов или групп) в строку."""
if not lst:
return ""
parts = []
for item in lst:
if not isinstance(item, dict):
parts.append(str(item))
continue
# Это группа (есть ключ 'items') или сервер/сеть (есть ключ 'ip')
if "items" in item:
parts.append(item.get("name", "?"))
elif "ip" in item:
hostname = item.get("hostname", "")
ip = item.get("ip", "")
prefix = item.get("prefix", "")
parts.append(f"{hostname} ({ip}/{prefix})")
else:
parts.append(item.get("hostname") or item.get("name") or str(item))
return "\n".join(parts)
def _extract_services(svc_list, sg_list) -> str:
parts = []
if svc_list:
for s in svc_list:
if isinstance(s, dict):
parts.append(s.get("name", str(s)))
if sg_list:
for sg in sg_list:
if isinstance(sg, dict):
parts.append(f"[{sg.get('name', str(sg))}]")
return "\n".join(parts) if parts else ""
def build_rules_sheet(wb: Workbook):
ws = wb.create_sheet("Правила", 0) # первый лист
ws.freeze_panes = "A3"
headers = [
"", "Порядок", "Имя правила", "Описание",
"Источник", "Назначение", "Сервисы",
"Действие", "Лог", "IDP", "Affinity"
]
apply_header_row(ws, 1, headers)
set_col_widths(ws, [5, 8, 28, 40, 30, 30, 35, 10, 6, 6, 25])
rule_num = 0
for row_idx, rule in enumerate(rules, start=2):
rtype = rule.get("type", "rule")
if rtype == "span":
# Строка-разделитель (заголовок секции)
ws.merge_cells(
start_row=row_idx, start_column=1,
end_row=row_idx, end_column=len(headers)
)
c = ws.cell(row=row_idx, column=1, value=rule.get("name", ""))
c.font = Font(name="Calibri", bold=True, color=CLR_SPAN_FG, size=11)
c.fill = make_fill(CLR_SPAN_BG)
c.alignment = Alignment(horizontal="center", vertical="center")
c.border = BORDER
ws.row_dimensions[row_idx].height = 22
continue
# Обычное правило
rule_num += 1
action = rule.get("action", "")
if action == "allow":
bg = CLR_ALLOW_BG
elif action in ("deny", "drop", "reject"):
bg = CLR_DENY_BG
else:
bg = CLR_WHITE if row_idx % 2 == 0 else CLR_ALT_BG
src_str = _extract_src_dst(rule.get("src_list"))
dst_str = _extract_src_dst(rule.get("dst_list"))
svc_str = _extract_services(
rule.get("service_list"), rule.get("service_group_list")
)
affinity_str = ", ".join(rule.get("affinity", []))
fmt_cell(ws, row_idx, 1, rule_num, bg=bg, align="center")
fmt_cell(ws, row_idx, 2, rule.get("order", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 3, rule.get("name", ""), bg=bg, bold=True)
fmt_cell(ws, row_idx, 4, rule.get("description", ""), bg=bg)
fmt_cell(ws, row_idx, 5, src_str, bg=bg)
fmt_cell(ws, row_idx, 6, dst_str, bg=bg)
fmt_cell(ws, row_idx, 7, svc_str, bg=bg)
fmt_cell(ws, row_idx, 8, action, bg=bg, align="center", bold=True)
fmt_cell(ws, row_idx, 9, rule.get("log", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 10, rule.get("idp", ""), bg=bg, align="center")
fmt_cell(ws, row_idx, 11, affinity_str, bg=bg)
# Высота строки — по числу строк в самом длинном поле
max_lines = max(
len(src_str.split("\n")),
len(dst_str.split("\n")),
len(svc_str.split("\n")),
1
)
ws.row_dimensions[row_idx].height = max(18, 15 * max_lines)
ws.row_dimensions[1].height = 30
# ─── Главная функция ─────────────────────────────────────────────────────────
def main():
output_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fw_report.xlsx")
wb = Workbook()
# Удаляем дефолтный лист
wb.remove(wb.active)
build_rules_sheet(wb)
build_objects_sheet(wb)
build_groups_sheet(wb)
build_services_sheet(wb)
build_service_groups_sheet(wb)
wb.save(output_file)
print(f"Отчёт сохранён: {output_file}")
if __name__ == "__main__":
main()

BIN
fw_report.xlsx Normal file

Binary file not shown.

1322
fw_settings.py Normal file

File diff suppressed because it is too large Load Diff