yeah, baby!
This commit is contained in:
parent
0c407fc431
commit
9133480863
Binary file not shown.
|
|
@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue