diff --git a/dokogen/dictionaries/prompts/check_criteria.json b/dokogen/dictionaries/prompts/check_criteria.json new file mode 100644 index 0000000..75d5d52 --- /dev/null +++ b/dokogen/dictionaries/prompts/check_criteria.json @@ -0,0 +1,4 @@ +{ + "system": "Ты — эксперт по категорированию объектов критической информационной инфраструктуры (187-ФЗ, Постановление Правительства РФ № 127). Проверь корректность присвоенных категорий значимости по показателям критериев. Категория значимости объекта определяется по максимальной из категорий показателей: I — если хотя бы один показатель I; II — если есть II и нет I; III — если есть III и нет I/II; «—» — если показатели не присвоены. Учитывай сферу деятельности субъекта и применяемость показателей (показатель 8 — госкорпорации/ГУП/госкомпании/стратегические АО; показатель 9 — все субъекты; показатель 10 — финансовый рынок/банки; показатель 13 — оборонно-промышленный комплекс).", + "response_format": "Верни ТОЛЬКО JSON: {\"оценка\": \"ok|warning|error\", \"замечания\": [{\"показатель\": \"номер\", \"категория\": \"текущая\", \"комментарий\": \"что не так и как должно быть\"}], \"предложения\": {\"criteria_values\": \"исправленный список показателей с категориями\", \"criteria_justification\": \"исправленное обоснование\"}, \"итоговая_категория\": \"I|II|III|—\"}. В «предложениях» — только если нужно что-то исправить, иначе пустые строки." +} diff --git a/dokogen/dictionaries/prompts/suggest_defense.json b/dokogen/dictionaries/prompts/suggest_defense.json new file mode 100644 index 0000000..6a2c72b --- /dev/null +++ b/dokogen/dictionaries/prompts/suggest_defense.json @@ -0,0 +1,4 @@ +{ + "system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). По категории значимости объекта КИИ, актуальным угрозам и уже выбранным средствам защиты информации (СЗИ) определи, каких средств не хватает, и дай рекомендации. Учитывай требования по категориям: для I категории — максимальный набор (антивирус, СКЗИ, SIEM, межсетевые экраны, средства анти-APT и т.д.), для II — расширенный набор, для III — базовый набор. Выбирай СЗИ ТОЛЬКО из реально существующих классов средств защиты (антивирусная защита, СКЗИ, межсетевой экран, SIEM, DLP, IDS/IPS, средства доверенной загрузки, контроля целостности, управления доступом и т.п.).", + "response_format": "Верни ТОЛЬКО JSON: {\"рекомендации\": [{\"сзи\": \"название класса/средства\", \"зачем\": \"для чего нужно\"}], \"общие_советы\": [\"...\"], \"достаточно\": true|false}. Поле «достаточно» — true, если текущий набор СЗИ покрывает требования категории. Не придумывай конкретные торговые марки, если их нет в списке — указывай класс средства." +} diff --git a/dokogen/ui.py b/dokogen/ui.py index d6587d4..f6dae1e 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -1076,13 +1076,11 @@ class DokoGenApp: thr_actions = ttk.Frame(sec_threats) thr_actions.pack(fill=tk.X, pady=(0, 6)) - ttk.Button(thr_actions, text="⬇️ Загрузить угрозы ФСТЭК", - command=self._download_fstec_threats).pack(side=tk.LEFT, padx=2) - ttk.Button(thr_actions, text="⚙️ Подобрать (программа)", + ttk.Button(thr_actions, text="⚡ Подобрать угрозы автоматически", command=self._suggest_threats_program_click).pack(side=tk.LEFT, padx=2) - ttk.Button(thr_actions, text="🤖 Подобрать (ИИ)", + ttk.Button(thr_actions, text="🤖 Подобрать через ИИ", command=self._suggest_threats_ai_click).pack(side=tk.LEFT, padx=2) - ttk.Button(thr_actions, text="⚖️ Сравнить программа ↔ ИИ", + ttk.Button(thr_actions, text="⚖️ Сравнить", command=self._compare_threats).pack(side=tk.LEFT, padx=2) self._threats_date_label = ttk.Label(thr_actions, text="", foreground='gray') self._threats_date_label.pack(side=tk.LEFT, padx=8) @@ -1127,8 +1125,12 @@ class DokoGenApp: 'is_incidents_listbox', 'incidents.json', height=3) _sec_row(sec_inc, "Показатели критериев значимости:", 'is_criteria_values_listbox', 'criteria_indicators.json', height=3) - ttk.Button(sec_inc, text="📋 Заполнить по таблице ПП № 127", - command=self._open_criteria_127_dialog).pack(anchor=tk.W, pady=(0, 4)) + btns_inc = ttk.Frame(sec_inc) + btns_inc.pack(anchor=tk.W, pady=(0, 4)) + ttk.Button(btns_inc, text="📋 Заполнить по таблице ПП № 127", + command=self._open_criteria_127_dialog).pack(side=tk.LEFT, padx=2) + ttk.Button(btns_inc, text="🤖 Проверить категории через ИИ", + command=self._check_criteria_ai).pack(side=tk.LEFT, padx=2) _sec_row(sec_inc, "Обоснование значений показателей:", 'is_criteria_justification_listbox', None, height=3) @@ -1141,15 +1143,17 @@ class DokoGenApp: _sec_row(sec_meas, "Технические меры:", 'is_tech_measures_listbox', 'tech_measures.json', height=5, allow_add=True) - # ===== Вкладка «Средства защиты» ===== - def_tab = ttk.Frame(nb2) - nb2.add(def_tab, text="Средства защиты") - def_btn_frame = ttk.Frame(def_tab) - def_btn_frame.pack(fill=tk.X) + # --- Подвкладка 5: Средства защиты (СЗИ) — внутри «Безопасности» --- + sec_def = ttk.Frame(nb_sec, padding=8) + nb_sec.add(sec_def, text="Средства защиты (СЗИ)") + def_btn_frame = ttk.Frame(sec_def) + def_btn_frame.pack(fill=tk.X, pady=(0, 4)) ttk.Button(def_btn_frame, text="+ Добавить", command=self._add_defense_item).pack(side=tk.LEFT, padx=2) ttk.Button(def_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.defense_tools_listbox)).pack(side=tk.LEFT, padx=2) ttk.Button(def_btn_frame, text="Выбрать из списка", command=self._select_from_defense_list).pack(side=tk.LEFT, padx=2) - self.defense_tools_listbox = tk.Listbox(def_tab, height=6) + ttk.Button(def_btn_frame, text="🤖 Рекомендации ИИ по СЗИ", + command=self._suggest_defense_ai).pack(side=tk.LEFT, padx=2) + self.defense_tools_listbox = tk.Listbox(sec_def, height=6) self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # ===== Вкладка «Пользователи ОКИИ» ===== @@ -2197,7 +2201,7 @@ class DokoGenApp: # grab_set + lift — чтобы окно не уходило на задний план w = tk.Toplevel(dialog) w.title(f"🧮 Авторасчёт показателя {num} — {name[:60]}") - w.geometry("820x700") + w.geometry("980x760") w.transient(dialog) w.lift() w.focus_force() @@ -2223,8 +2227,8 @@ class DokoGenApp: foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) def _row(label, key, default=''): - ttk.Label(frm, text=label).pack(anchor=tk.W, pady=(4, 0)) - e = ttk.Entry(frm, width=70) + ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0)) + e = ttk.Entry(frm, width=80) e.insert(0, str(default or '')) e.pack(fill=tk.X) return e @@ -2232,8 +2236,14 @@ class DokoGenApp: e_profit = _row("Прибыль субъекта КИИ за прошедший год (руб.):", 'profit', profit) e_tax = _row("Налог на прибыль за прошедший год (руб.):", 'tax', profit_tax) e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome', '') + ttk.Label(frm, text="⚠️ ΔДоходов — экспертная оценка снижения доходов субъекта при наихудшем сценарии " + "нарушения (в опроснике её нет, вводится вручную; можно приравнять к ущербу из показателя 8).", + foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) e_budget = _row("Доход федерального бюджета, усреднённый за 3 года Бусредн (руб.):", 'budget', '21228053357700') + ttk.Label(frm, text="⚠️ Бусредн — средний доход федерального бюджета за 3 года (данные Минфина, " + "подставлено значение по умолчанию; при необходимости уточните).", + foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) ttk.Label(frm, text="Прочие составляющие ΔБ (если применимы, иначе 0):", font=('', 9, 'bold')).pack(anchor=tk.W, pady=(8, 0)) e_ndpi = _row("Снижение выплат от налога на добычу ПИ ΔНДПИ (руб.):", 'ndpi', '0') @@ -2250,8 +2260,8 @@ class DokoGenApp: foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) def _row(label, key, default=''): - ttk.Label(frm, text=label).pack(anchor=tk.W, pady=(4, 0)) - e = ttk.Entry(frm, width=70) + ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0)) + e = ttk.Entry(frm, width=80) e.insert(0, str(default or '')) e.pack(fill=tk.X) return e @@ -2264,8 +2274,14 @@ class DokoGenApp: _d_loss = _num(getattr(self.model, 'income_loss', '') or '') e_dincome = _row("Суммарный экономический ущерб от нарушения критических процессов (руб.):", 'dincome', f"{_d_loss:,.0f}" if _d_loss else '') + ttk.Label(frm, text="⚠️ Ущерб — поле «Суммарный ущерб (показатель 8)» из вкладки «Организация» " + "(подставлено автоматически, можно поправить).", + foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) e_budget = _row("Доходы субъекта КИИ, усреднённые за 5 лет (руб.):", 'budget', f"{_d_avg:,.0f}" if _d_avg else '') + ttk.Label(frm, text="⚠️ Доходы за 5 лет — среднее по полям «Доход за …» из вкладки «Организация» " + "(подставлено автоматически, можно поправить).", + foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) result = tk.Text(frm, height=8, wrap=tk.WORD, state=tk.NORMAL) result.pack(fill=tk.BOTH, expand=True, pady=(8, 0)) @@ -2934,7 +2950,7 @@ class DokoGenApp: # Показываем окно: слева подобрано программой, справа текущий список dlg = tk.Toplevel(self.root) dlg.title("Сравнение угроз: программа ↔ ИИ/текущий") - dlg.geometry("780x460") + dlg.geometry("960x600") dlg.transient(self.root) frm = ttk.Frame(dlg, padding=8) @@ -2943,7 +2959,7 @@ class DokoGenApp: left = ttk.Frame(frm) left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) ttk.Label(left, text=f"⚙️ Программа ({len(program)})", font=('Arial', 9, 'bold')).pack(anchor=tk.W) - lb_p = tk.Listbox(left, height=14) + lb_p = tk.Listbox(left, height=18) lb_p.pack(fill=tk.BOTH, expand=True, pady=4) for t in program: lb_p.insert(tk.END, t) @@ -2951,7 +2967,7 @@ class DokoGenApp: right = ttk.Frame(frm) right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 0)) ttk.Label(right, text=f"🤖 ИИ / текущий ({len(current)})", font=('Arial', 9, 'bold')).pack(anchor=tk.W) - lb_c = tk.Listbox(right, height=14) + lb_c = tk.Listbox(right, height=18) lb_c.pack(fill=tk.BOTH, expand=True, pady=4) for t in current: lb_c.insert(tk.END, t) @@ -3290,6 +3306,207 @@ class DokoGenApp: dialog.wait_window() return result.get('val') + def _suggest_defense_ai(self): + """🤖 Рекомендации ИИ по средствам защиты (чего не хватает для категории).""" + 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] + data = { + 'name': isys.name or '', + 'category': isys.category or '', + 'sphere': isys.sphere or '', + 'object_type': isys.object_type or '', + 'threats': isys.threats or '', + 'attacker_category': isys.attacker_category or '', + 'defense_tools': list(self.defense_tools_listbox.get(0, tk.END)) or [], + } + self._verify_log("🤖 Запрос к ИИ: рекомендации по СЗИ…") + self.log(f"🤖 Запрос к ИИ: СЗИ для «{isys.name}» (категория {isys.category or '—'})") + + def _run(): + try: + from .verify import suggest_defense_ai + res, err = suggest_defense_ai( + data, settings['ai_api_key'], settings.get('ai_model'), + settings.get('ai_base_url')) + self.root.after(0, lambda: self._suggest_defense_ai_done(res, err)) + except Exception as e: + self.root.after(0, lambda: self._suggest_defense_ai_done(None, str(e))) + + threading.Thread(target=_run, daemon=True).start() + + def _suggest_defense_ai_done(self, res, err): + if err: + self._verify_log(f"❌ {err}") + self.log(f"❌ Рекомендации ИИ по СЗИ: {err}") + return + if not res: + self._verify_log("❌ ИИ не вернул рекомендации") + return + data = res.get('data', {}) + lines = [f"🤖 Рекомендации ИИ по СЗИ (категория: {self.model.information_systems[self._current_is_idx].category or '—'})", ""] + recs = data.get('рекомендации') or [] + if recs: + lines.append("Чего не хватает:") + for r in recs: + if isinstance(r, dict): + lines.append(f"• {r.get('сзи', '')} — {r.get('зачем', '')}") + else: + lines.append(f"• {r}") + else: + lines.append("ИИ не указал конкретных средств.") + tips = data.get('общие_советы') or [] + if tips: + lines.append("") + lines.append("Общие советы:") + for t in tips: + lines.append(f"• {t}") + self._show_security_ai_result(lines) + self._verify_log("🤖 Рекомендации ИИ по СЗИ получены") + self.log(f"🤖 Рекомендации ИИ по СЗИ: {len(recs)} позиций") + + def _suggest_defense_ai(self): + """🤖 Рекомендации ИИ по средствам защиты (чего не хватает для категории).""" + 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] + data = { + 'name': isys.name or '', + 'category': isys.category or '', + 'sphere': isys.sphere or '', + 'object_type': isys.object_type or '', + 'threats': isys.threats or '', + 'attacker_category': isys.attacker_category or '', + 'defense_tools': list(self.defense_tools_listbox.get(0, tk.END)) or [], + } + self._verify_log("🤖 Запрос к ИИ: рекомендации по СЗИ…") + self.log(f"🤖 Запрос к ИИ: СЗИ для «{isys.name}» (категория {isys.category or '—'})") + + def _run(): + try: + from .verify import suggest_defense_ai + res, err = suggest_defense_ai( + data, settings['ai_api_key'], settings.get('ai_model'), + settings.get('ai_base_url')) + self.root.after(0, lambda: self._suggest_defense_ai_done(res, err)) + except Exception as e: + self.root.after(0, lambda: self._suggest_defense_ai_done(None, str(e))) + + threading.Thread(target=_run, daemon=True).start() + + def _suggest_defense_ai_done(self, res, err): + if err: + self._verify_log(f"❌ {err}") + self.log(f"❌ Рекомендации ИИ по СЗИ: {err}") + return + if not res: + self._verify_log("❌ ИИ не вернул рекомендации") + return + data = res.get('data', {}) + lines = [f"🤖 Рекомендации ИИ по СЗИ (категория: {self.model.information_systems[self._current_is_idx].category or '—'})", ""] + recs = data.get('рекомендации') or [] + if recs: + lines.append("Чего не хватает:") + for r in recs: + if isinstance(r, dict): + lines.append(f"• {r.get('сзи', '')} — {r.get('зачем', '')}") + else: + lines.append(f"• {r}") + else: + lines.append("ИИ не указал конкретных средств.") + tips = data.get('общие_советы') or [] + if tips: + lines.append("") + lines.append("Общие советы:") + for t in tips: + lines.append(f"• {t}") + self._show_security_ai_result(lines) + self._verify_log("🤖 Рекомендации ИИ по СЗИ получены") + self.log(f"🤖 Рекомендации ИИ по СЗИ: {len(recs)} позиций") + + def _check_criteria_ai(self): + """🤖 Проверка категорий значимости по ПП 127 через ИИ.""" + 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] + data = { + 'name': isys.name or '', + 'description': isys.description or '', + 'category': isys.category or '', + 'sphere': isys.sphere or '', + 'object_type': isys.object_type or '', + 'criteria_values': isys.criteria_values or '', + 'criteria_justification': isys.criteria_justification or '', + } + self._verify_log("🤖 Запрос к ИИ: проверка категорий по ПП 127…") + self.log(f"🤖 Запрос к ИИ: категории ПП 127 для «{isys.name}»") + + def _run(): + try: + from .verify import check_criteria_ai + res, err = check_criteria_ai( + data, settings['ai_api_key'], settings.get('ai_model'), + settings.get('ai_base_url')) + self.root.after(0, lambda: self._check_criteria_ai_done(res, err)) + except Exception as e: + self.root.after(0, lambda: self._check_criteria_ai_done(None, str(e))) + + threading.Thread(target=_run, daemon=True).start() + + def _check_criteria_ai_done(self, res, err): + if err: + self._verify_log(f"❌ {err}") + self.log(f"❌ Проверка категорий ИИ: {err}") + return + if not res: + self._verify_log("❌ ИИ не вернул результат") + return + data = res.get('data', {}) + isys = self.model.information_systems[self._current_is_idx] + lines = [f"🤖 Проверка категорий по ПП № 127 — «{isys.name}»", ""] + lines.append(f"Оценка: {data.get('rating', 'warning')}") + if data.get('final_category'): + lines.append(f"Итоговая категория (по мнению ИИ): {data['final_category']}") + notes = data.get('notes') or [] + if notes: + lines.append("") + lines.append("Замечания:") + for n in notes: + if isinstance(n, dict): + lines.append(f"• Показатель {n.get('показатель', '?')} ({n.get('категория', '—')}): " + f"{n.get('комментарий', '')}") + else: + lines.append(f"• {n}") + props = data.get('proposals') or {} + if props.get('criteria_values') or props.get('criteria_justification'): + lines.append("") + lines.append("Предложения ИИ (можно применить вручную):") + if props.get('criteria_values'): + lines.append(f"Показатели: {props['criteria_values']}") + if props.get('criteria_justification'): + lines.append(f"Обоснование: {props['criteria_justification']}") + self._show_security_ai_result(lines) + self._verify_log("🤖 Проверка категорий ИИ завершена") + self.log("🤖 Проверка категорий ИИ завершена") + def _select_from_defense_list(self): """Выбор СЗИ из JSON словаря.""" import json diff --git a/dokogen/verify.py b/dokogen/verify.py index cdee3df..8231d74 100644 --- a/dokogen/verify.py +++ b/dokogen/verify.py @@ -733,3 +733,86 @@ def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None }, 'apply': {'company': {k: v for k, v in proposals.items() if v}}, }, None + + +def suggest_defense_ai(defense_data, api_key, model='deepseek-chat', base_url=None): + """ИИ-рекомендации по средствам защиты информации (СЗИ). + + По категории значимости, угрозам и текущему списку СЗИ ИИ подсказывает, + чего не хватает (например, антивирус, СКЗИ, SIEM и т.п.). + Возвращает (результат, ошибка). + """ + if not api_key: + return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" + pr = _load_prompt('suggest_defense') + system = pr.get('system', ( + "Ты — эксперт по безопасности КИИ (187-ФЗ, приказ ФСТЭК № 239). " + "По категории значимости объекта и актуальным угрозам определи, каких " + "средств защиты информации (СЗИ) не хватает, и дай рекомендации." + )) + system = system + " " + pr.get('response_format', + 'Верни ТОЛЬКО JSON: {"рекомендации": [{"сзи": "...", "зачем": "..."}], ' + '"общие_советы": ["..."]}') + user = json.dumps(defense_data, ensure_ascii=False) + js, err = _ai_json(api_key, model, base_url, system, user) + if err: + return None, err + recs = js.get('рекомендации') or [] + if not isinstance(recs, list): + recs = [] + tips = js.get('общие_советы') or [] + if isinstance(tips, str): + tips = [tips] + return { + 'source': 'ИИ', + 'source_url': 'openai-совместимый API', + 'disclaimer': AI_DISCLAIMER, + 'kind': 'defense', + 'data': { + 'рекомендации': recs, + 'общие_советы': tips, + }, + }, None + + +def check_criteria_ai(criteria_data, api_key, model='deepseek-chat', base_url=None): + """ИИ-проверка категорий значимости по ПП РФ № 127. + + По данным объекта КИИ (назначение, сфера, категории по показателям, + обоснования) ИИ проверяет корректность присвоенных категорий. + Возвращает (результат, ошибка). + """ + if not api_key: + return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" + pr = _load_prompt('check_criteria') + system = pr.get('system', ( + "Ты — эксперт по категорированию объектов КИИ (187-ФЗ, ПП РФ № 127). " + "Проверь присвоенные категории значимости по показателям критериев." + )) + system = system + " " + pr.get('response_format', + 'Верни ТОЛЬКО JSON: {"оценка": "ok|warning|error", ' + '"замечания": [{"показатель": "...", "категория": "...", "комментарий": "..."}], ' + '"предложения": {"criteria_values": "...", "criteria_justification": "..."}, ' + '"итоговая_категория": "I|II|III|—"}') + user = json.dumps(criteria_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': 'criteria', + 'data': { + 'rating': js.get('оценка', 'warning'), + 'notes': notes, + 'proposals': proposals, + 'final_category': js.get('итоговая_категория', ''), + }, + }, None