# -*- 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('', lambda e: _ok()) dialog.bind('', 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('', lambda e: _ok()) dialog.bind('', 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()