yeah, baby!
This commit is contained in:
+664
@@ -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
File diff suppressed because it is too large
Load Diff
+1073
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>
|
||||
Reference in New Issue
Block a user