Files
dokogenMU/dokogen/security_tab_187_reference.py
T
LocoAgent baa8f095d0 feat: перенос вкладки «Безопасность» из DokoGen 187-ФЗ
- security_tab_187_reference.py — код вкладки (секция построения +
  методы-обработчики + ИИ-проверка) для переноса интерфейса
- словари: attacker_categories, incidents, criteria_indicators,
  org_measures, tech_measures, threats, threats_fstec (+дата актуализации)
- fstec_threats.py — скачивание перечня угроз с bdu.fstec.ru
- verify.py: check_security_ai — ИИ-проверка безопасности по 236 приказу
2026-08-04 17:36:02 +04:00

481 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
ВКЛАДКА «БЕЗОПАСНОСТЬ» из DokoGen 187-ФЗ (dokogen187, ветка source)
=====================================================================
Скопировано для переноса в DokoGen MU, чтобы интерфейс был одинаковым.
ЧТО ВХОДИТ:
1. Секция построения вкладки (вставить в метод _build_is_tab перед вкладкой «Средства защиты»)
2. Методы-обработчики (вставить в класс приложения)
3. Словари — скопированы в dokogen/dictionaries/:
attacker_categories.json, incidents.json, criteria_indicators.json,
org_measures.json, tech_measures.json, threats.json,
threats_fstec.json, threats_fstec_date.txt
4. Модуль fstec_threats.py (скачивание перечня угроз с bdu.fstec.ru)
5. check_security_ai() — ИИ-проверка (см. конец файла, добавить в verify.py)
ЗАВИСИМОСТИ:
- tkinter/ttk (стандарт)
- dokogen/fstec_threats.py (загрузка угроз ФСТЭК)
- dokogen/verify.py: check_security_ai (ИИ-проверка)
- dokogen/api_settings.py: load_api_settings
- Поля модели InformationSystem (добавить в models.py, если нет):
attacker_category, attacker_characteristics, threats, incidents,
criteria_values, criteria_justification, org_measures, tech_measures
- Маппинги _collect_is/_on_is_select (см. комментарии в конце)
ПРИМЕЧАНИЕ: в DokoGen MU (152-ФЗ) модель InformationSystem другая —
при переносе адаптировать имена полей или добавить новые.
"""
# ============================================================
# 1. СЕКЦИЯ ПОСТРОЕНИЯ ВКЛАДКИ (вставить в _build_is_tab)
# ============================================================
# ===== Вкладка «Безопасность» =====
sec_tab = ttk.Frame(nb2)
nb2.add(sec_tab, text="Безопасность")
sec_frame = ttk.Frame(sec_tab, padding=8)
sec_frame.pack(fill=tk.BOTH, expand=True)
# --- Категория нарушителя (словарик) ---
ttk.Label(sec_frame, text="Категория нарушителя:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.is_attacker_category_combo = ttk.Combobox(sec_frame, width=68, state='readonly')
self.is_attacker_category_combo.grid(row=0, column=1, padx=8, sticky=tk.EW, pady=1)
try:
import json as _json
_ac = os.path.join(os.path.dirname(__file__), 'dictionaries', 'attacker_categories.json')
with open(_ac, encoding='utf-8') as _f:
self.is_attacker_category_combo['values'] = [''] + _json.load(_f)
except Exception:
pass
# --- Характеристика нарушителя ---
ttk.Label(sec_frame, text="Характеристика нарушителя:").grid(row=1, column=0, sticky=tk.NW, pady=2)
self.is_attacker_characteristics_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
self.is_attacker_characteristics_listbox.grid(row=1, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
sec_char_btn = ttk.Frame(sec_frame)
sec_char_btn.grid(row=1, column=2, sticky=tk.N, padx=(4, 0))
ttk.Button(sec_char_btn, text="+ Добавить",
command=lambda: self._add_sw_item(self.is_attacker_characteristics_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_char_btn, text="- Удалить",
command=lambda: self._del_sw_item(self.is_attacker_characteristics_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_char_btn, text="✏️ Редактировать",
command=lambda: self._edit_sw_item(self.is_attacker_characteristics_listbox)).pack(fill=tk.X, pady=1)
# --- Основные угрозы БИ (словарик + загрузка с ФСТЭК) ---
ttk.Label(sec_frame, text="Основные угрозы БИ:").grid(row=2, column=0, sticky=tk.W, pady=2)
self.is_threats_listbox = tk.Listbox(sec_frame, height=5, exportselection=False)
self.is_threats_listbox.grid(row=2, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
sec_thr_btn = ttk.Frame(sec_frame)
sec_thr_btn.grid(row=2, column=2, sticky=tk.N, padx=(4, 0))
ttk.Button(sec_thr_btn, text="+ Добавить",
command=lambda: self._add_sw_item(self.is_threats_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_thr_btn, text="- Удалить",
command=lambda: self._del_sw_item(self.is_threats_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_thr_btn, text="✏️ Редактировать",
command=lambda: self._edit_sw_item(self.is_threats_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_thr_btn, text="📚 Из словаря",
command=lambda: self._select_sw_from_dict(self.is_threats_listbox, 'threats_fstec.json')).pack(fill=tk.X, pady=1)
ttk.Button(sec_thr_btn, text="⬇️ Загрузить угрозы ФСТЭК",
command=self._download_fstec_threats).pack(fill=tk.X, pady=1)
self._threats_date_label = ttk.Label(sec_frame, text="", foreground='gray')
self._threats_date_label.grid(row=3, column=1, sticky=tk.W, padx=8, pady=(0, 2))
# --- Типы компьютерных инцидентов (словарик) ---
ttk.Label(sec_frame, text="Типы компьютерных инцидентов:").grid(row=4, column=0, sticky=tk.W, pady=2)
self.is_incidents_listbox = tk.Listbox(sec_frame, height=4, exportselection=False)
self.is_incidents_listbox.grid(row=4, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
sec_inc_btn = ttk.Frame(sec_frame)
sec_inc_btn.grid(row=4, column=2, sticky=tk.N, padx=(4, 0))
ttk.Button(sec_inc_btn, text="+ Добавить",
command=lambda: self._add_sw_item(self.is_incidents_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_inc_btn, text="- Удалить",
command=lambda: self._del_sw_item(self.is_incidents_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_inc_btn, text="✏️ Редактировать",
command=lambda: self._edit_sw_item(self.is_incidents_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_inc_btn, text="📚 Из словаря",
command=lambda: self._select_sw_from_dict(self.is_incidents_listbox, 'incidents.json')).pack(fill=tk.X, pady=1)
# --- Показатели критериев значимости (словарик) ---
ttk.Label(sec_frame, text="Показатели критериев значимости:").grid(row=5, column=0, sticky=tk.W, pady=2)
self.is_criteria_values_listbox = tk.Listbox(sec_frame, height=4, exportselection=False)
self.is_criteria_values_listbox.grid(row=5, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
sec_crit_btn = ttk.Frame(sec_frame)
sec_crit_btn.grid(row=5, column=2, sticky=tk.N, padx=(4, 0))
ttk.Button(sec_crit_btn, text="+ Добавить",
command=lambda: self._add_sw_item(self.is_criteria_values_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_crit_btn, text="- Удалить",
command=lambda: self._del_sw_item(self.is_criteria_values_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_crit_btn, text="✏️ Редактировать",
command=lambda: self._edit_sw_item(self.is_criteria_values_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_crit_btn, text="📚 Из словаря",
command=lambda: self._select_sw_from_dict(self.is_criteria_values_listbox, 'criteria_indicators.json')).pack(fill=tk.X, pady=1)
# --- Обоснование значений показателей ---
ttk.Label(sec_frame, text="Обоснование значений показателей:").grid(row=6, column=0, sticky=tk.NW, pady=2)
self.is_criteria_justification_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
self.is_criteria_justification_listbox.grid(row=6, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
sec_just_btn = ttk.Frame(sec_frame)
sec_just_btn.grid(row=6, column=2, sticky=tk.N, padx=(4, 0))
ttk.Button(sec_just_btn, text="+ Добавить",
command=lambda: self._add_sw_item(self.is_criteria_justification_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_just_btn, text="- Удалить",
command=lambda: self._del_sw_item(self.is_criteria_justification_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_just_btn, text="✏️ Редактировать",
command=lambda: self._edit_sw_item(self.is_criteria_justification_listbox)).pack(fill=tk.X, pady=1)
# --- Организационные меры (словарик) ---
ttk.Label(sec_frame, text="Организационные меры:").grid(row=7, column=0, sticky=tk.W, pady=2)
self.is_org_measures_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
self.is_org_measures_listbox.grid(row=7, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
sec_org_btn = ttk.Frame(sec_frame)
sec_org_btn.grid(row=7, column=2, sticky=tk.N, padx=(4, 0))
ttk.Button(sec_org_btn, text="+ Добавить",
command=lambda: self._add_sw_item(self.is_org_measures_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_org_btn, text="- Удалить",
command=lambda: self._del_sw_item(self.is_org_measures_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_org_btn, text="✏️ Редактировать",
command=lambda: self._edit_sw_item(self.is_org_measures_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_org_btn, text="📚 Из словаря",
command=lambda: self._select_sw_from_dict(self.is_org_measures_listbox, 'org_measures.json')).pack(fill=tk.X, pady=1)
# --- Технические меры (словарик) ---
ttk.Label(sec_frame, text="Технические меры:").grid(row=8, column=0, sticky=tk.W, pady=2)
self.is_tech_measures_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
self.is_tech_measures_listbox.grid(row=8, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
sec_tech_btn = ttk.Frame(sec_frame)
sec_tech_btn.grid(row=8, column=2, sticky=tk.N, padx=(4, 0))
ttk.Button(sec_tech_btn, text="+ Добавить",
command=lambda: self._add_sw_item(self.is_tech_measures_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_tech_btn, text="- Удалить",
command=lambda: self._del_sw_item(self.is_tech_measures_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_tech_btn, text="✏️ Редактировать",
command=lambda: self._edit_sw_item(self.is_tech_measures_listbox)).pack(fill=tk.X, pady=1)
ttk.Button(sec_tech_btn, text="📚 Из словаря",
command=lambda: self._select_sw_from_dict(self.is_tech_measures_listbox, 'tech_measures.json')).pack(fill=tk.X, pady=1)
# --- Кнопка ИИ-проверки ---
ttk.Button(sec_frame, text="🤖 Проверить безопасность через ИИ",
command=self._check_security_ai).grid(
row=9, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))
sec_frame.columnconfigure(1, weight=1)
# ============================================================
# 2. МЕТОДЫ-ОБРАБОТЧИКИ (вставить в класс приложения)
# ============================================================
def _download_fstec_threats(self):
"""Скачать перечень угроз с сайта ФСТЭК (БДУ) и обновить список."""
try:
from .fstec_threats import download_fstec_threats, get_threats_date
except ImportError as e:
messagebox.showerror("Ошибка", f"Модуль fstec_threats не найден: {e}")
return
self.log("⬇️ Загрузка перечня угроз с bdu.fstec.ru…")
ok, msg, threats = download_fstec_threats()
if not ok:
messagebox.showerror("Загрузка угроз",
f"Не удалось загрузить:\n{msg}\n\n"
"Проверьте доступ к интернету или попробуйте позже.")
self.log(f"❌ Загрузка угроз: {msg}")
return
# Обновляем label даты актуализации
self._update_threats_date_label()
# Если список угроз уже открыт — можно показать диалог выбора
messagebox.showinfo("Загрузка угроз",
f"{msg}\n\nСписок сохранён. Теперь нажмите «📚 Из словаря» "
"в разделе «Основные угрозы БИ», чтобы выбрать нужные угрозы.")
self.log(f"✅ {msg}")
def _update_threats_date_label(self):
"""Обновить подпись даты актуализации списка угроз."""
try:
from .fstec_threats import get_threats_date
d = get_threats_date()
except Exception:
d = None
if hasattr(self, '_threats_date_label'):
if d:
self._threats_date_label.config(
text=f"Актуально на: {d}", foreground='#2e7d32')
else:
self._threats_date_label.config(
text="⚠ Список угроз ещё не скачан — нажмите «⬇️ Загрузить угрозы ФСТЭК»",
foreground='#c62828')
def _check_security_ai(self):
"""Проверка вкладки «Безопасность» через ИИ (DeepSeek/OpenAI-совместимый API)."""
if self._current_is_idx is None:
messagebox.showinfo("Безопасность", "Сначала выберите объект КИИ в списке слева")
return
settings = load_api_settings()
if not settings.get('ai_api_key'):
messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
return
# Собираем текущие данные безопасности объекта
self._collect_is()
isys = self.model.information_systems[self._current_is_idx]
security_data = {
'name': isys.name or '',
'description': isys.description or '',
'category': isys.category or '',
'sphere': isys.sphere or '',
'object_type': isys.object_type or '',
'architecture': isys.architecture or '',
'attacker_category': isys.attacker_category or '',
'attacker_characteristics': isys.attacker_characteristics or '',
'threats': isys.threats or '',
'incidents': isys.incidents or '',
'criteria_values': isys.criteria_values or '',
'criteria_justification': isys.criteria_justification or '',
'org_measures': isys.org_measures or '',
'tech_measures': isys.tech_measures or '',
}
self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): проверка безопасности")
self._verify_log("Проверка безопасности через ИИ…")
def _run():
try:
from .verify import check_security_ai
result, err = check_security_ai(
security_data,
settings['ai_api_key'],
settings.get('ai_model'),
settings.get('ai_base_url'),
)
self.root.after(0, lambda: self._security_ai_done(result, err))
except Exception as e:
self.root.after(0, lambda: self._security_ai_done(None, str(e)))
threading.Thread(target=_run, daemon=True).start()
def _security_ai_done(self, result, err):
"""Обработка ответа ИИ по безопасности."""
if err:
self._verify_log(f"❌ {err}")
self.log(f"❌ Проверка безопасности: {err}")
return
d = result['data']
lines = [f"Источник: {result['source']}"]
lines.append(f"Оценка: {d.get('rating', 'warning')}")
notes = d.get('notes') or []
if notes:
lines.append("\nЗамечания:")
for n in notes:
lines.append(f" • {n}")
props = d.get('proposals') or {}
if any(props.values()):
lines.append("\nПредложения ИИ:")
for key, label in [('attacker_category', 'Нарушитель'),
('threats', 'Угрозы'),
('incidents', 'Инциденты'),
('criteria_values', 'Показатели'),
('criteria_justification', 'Обоснование'),
('org_measures', 'Орг. меры'),
('tech_measures', 'Тех. меры')]:
v = props.get(key)
if v:
lines.append(f" {label}: {v}")
self._verify_log('\n'.join(lines))
self.log(f"🤖 ИИ: оценка {d.get('rating', 'warning')}, замечаний: {len(notes)}")
def _refine_dict_item_dialog(self, base_item):
"""Окно уточнения модели и количества для позиции из словаря.
Возвращает полную строку вида «Windows 10 Pro 5 шт.» или
«Сервер Supermicro SSG-2027R -E1R24N 2 шт.» или None.
"""
import re
# Убираем из базы маркер «(уточнить …)»
base = re.sub(r'\s*\([^)]*уточн[^)]*\)', '', base_item, flags=re.I).strip() or base_item
dialog = tk.Toplevel(self.root)
dialog.title("Уточнить модель и количество")
dialog.geometry("500x180")
dialog.transient(self.root)
dialog.grab_set()
frame = ttk.Frame(dialog, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text=f"Позиция: {base}", wraplength=470,
font=('Arial', 9, 'bold')).pack(anchor=tk.W, pady=(0, 8))
ttk.Label(frame, text="Модель / версия / конфигурация (необязательно):").pack(anchor=tk.W)
model_entry = ttk.Entry(frame, width=60)
model_entry.pack(fill=tk.X, pady=(2, 6))
model_entry.focus_set()
ttk.Label(frame, text="Количество:").pack(anchor=tk.W)
count_entry = ttk.Entry(frame, width=20)
count_entry.pack(anchor=tk.W, pady=(2, 6))
result = {}
def _ok():
model = model_entry.get().strip()
count = count_entry.get().strip()
text = base
if model:
text = f"{text} {model}"
if count:
text = f"{text} {count} шт."
result['val'] = text
dialog.destroy()
def _cancel():
result['val'] = None
dialog.destroy()
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X, pady=(6, 0))
ttk.Button(btn_frame, text="✅ ОК", command=_ok).pack(side=tk.RIGHT, padx=2)
ttk.Button(btn_frame, text="Отмена", command=_cancel).pack(side=tk.RIGHT, padx=2)
dialog.bind('<Return>', lambda e: _ok())
dialog.bind('<Escape>', lambda e: _cancel())
dialog.wait_window()
return result.get('val')
def _select_sw_from_dict(self, listbox, dict_name):
"""Выбор позиций из словаря (JSON) — можно несколько."""
import json
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', dict_name)
try:
with open(dict_path, 'r', encoding='utf-8') as f:
items = json.load(f)
title = "Выбор из словаря"
self._select_from_dialog(title, items, listbox)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
def _sw_dialog(self, title, initial=""):
"""Большое окно для ввода/редактирования позиции ПО."""
dialog = tk.Toplevel(self.root)
dialog.title(title)
dialog.geometry("560x220")
dialog.transient(self.root)
dialog.grab_set()
dialog.resizable(True, True)
frame = ttk.Frame(dialog, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="Текст позиции:").pack(anchor=tk.W)
txt = tk.Text(frame, width=70, height=6, wrap=tk.WORD)
txt.pack(fill=tk.BOTH, expand=True, pady=5)
txt.insert('1.0', initial)
txt.focus_set()
result = {}
def _ok():
result['val'] = txt.get('1.0', 'end-1c').strip()
dialog.destroy()
def _cancel():
result['val'] = None
dialog.destroy()
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X)
ttk.Button(btn_frame, text="✅ ОК", command=_ok).pack(side=tk.RIGHT, padx=2)
ttk.Button(btn_frame, text="Отмена", command=_cancel).pack(side=tk.RIGHT, padx=2)
dialog.bind('<Return>', lambda e: _ok())
dialog.bind('<Escape>', lambda e: _cancel())
dialog.wait_window()
return result.get('val')
def _add_sw_item(self, listbox):
"""Добавить строку в список ПО (большое окно)."""
item = self._sw_dialog("Добавить позицию")
if item:
listbox.insert(tk.END, item)
def _del_sw_item(self, listbox):
"""Удалить выбранную строку из списка ПО."""
sel = listbox.curselection()
if sel:
listbox.delete(sel[0])
def _edit_sw_item(self, listbox):
"""Редактировать выбранную строку списка ПО (большое окно)."""
sel = listbox.curselection()
if not sel:
messagebox.showinfo("Редактировать", "Сначала выберите позицию в списке")
return
current = listbox.get(sel[0])
item = self._sw_dialog("Редактировать позицию", initial=current)
if item is not None:
listbox.delete(sel[0])
listbox.insert(sel[0], item)
# ============================================================
# 3. ИИ-ПРОВЕРКА (добавить в verify.py)
# ============================================================
def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-проверка заполненной вкладки «Безопасность» объекта КИИ (236 приказ).
Возвращает (результат, ошибка): результат — dict с 'оценка', 'замечания', 'предложения'.
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
system = (
"Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, "
"приказ ФСТЭК России от 25.12.2017 № 239 «Об утверждении Требований по обеспечению "
"безопасности значимых объектов КИИ» и приказ № 236). Проверь заполненные сведения "
"об объекте КИИ и дай рекомендации. Верни ТОЛЬКО JSON: "
'{"оценка": "ok|warning|error", '
'"замечания": ["..."], '
'"предложения": {"attacker_category": "...", "threats": "...", "incidents": "...", '
'"criteria_values": "...", "criteria_justification": "...", '
'"org_measures": "...", "tech_measures": "..."}} '
"В «предложениях» укажи, что добавить/исправить по каждому разделу, если нужно, иначе пустые строки."
)
user = json.dumps(security_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
return None, err
notes = js.get('замечания') or []
if isinstance(notes, str):
notes = [notes]
proposals = js.get('предложения') or {}
if not isinstance(proposals, dict):
proposals = {}
return {
'source': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'security',
'data': {
'rating': js.get('оценка', 'warning'),
'notes': notes,
'proposals': proposals,
},
}, None
# ============================================================
# 4. МАППИНГИ ПОЛЕЙ
# ============================================================
# В _collect_is() (список виджетов → модель):
# ('is_attacker_category_combo', 'attacker_category'),
# ('is_attacker_characteristics_listbox', 'attacker_characteristics'),
# ('is_threats_listbox', 'threats'),
# ('is_incidents_listbox', 'incidents'),
# ('is_criteria_values_listbox', 'criteria_values'),
# ('is_criteria_justification_listbox', 'criteria_justification'),
# ('is_org_measures_listbox', 'org_measures'),
# ('is_tech_measures_listbox', 'tech_measures'),
# В _on_is_select() — обратный порядок (combo → .set(), listbox → split('\n'))
# После построения вкладок вызвать: self._update_threats_date_label()