fix: окна авторасчёта/сравнения не обрезают текст; СЗИ перенесены в «Безопасность»; ИИ-рекомендации по СЗИ; ИИ-проверка категорий ПП 127; пояснения к полям 8/9; упрощение панели угроз
This commit is contained in:
+238
-21
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user