fix: окна авторасчёта/сравнения не обрезают текст; СЗИ перенесены в «Безопасность»; ИИ-рекомендации по СЗИ; ИИ-проверка категорий ПП 127; пояснения к полям 8/9; упрощение панели угроз

This commit is contained in:
2026-08-05 01:31:13 +04:00
parent fafeea4fcf
commit d376a28184
4 changed files with 329 additions and 21 deletions
+238 -21
View File
@@ -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