fix: угрозы — фильтр по словарю УБИ для всего от ИИ (мусор отбрасывается, УБИ.069 дополняется названием), «Подобрать угрозы» — окно с галочками (синий — программа, красный — ИИ, зелёный — совпало), «Проверить через ИИ» — вердикт + точечное применение по словарю, возвращена кнопка «Загрузить угрозы ФСТЭК»

This commit is contained in:
LocoAgent
2026-08-05 11:03:28 +04:00
parent 033c21fccc
commit a8fa3f908c
2 changed files with 251 additions and 72 deletions
+182 -71
View File
@@ -1092,8 +1092,10 @@ class DokoGenApp:
command=self._suggest_threats_ai_click).pack(side=tk.LEFT, padx=2)
ttk.Button(thr_actions, text="🤖 Проверить через ИИ",
command=self._check_threats_ai_click).pack(side=tk.LEFT, padx=2)
ttk.Button(thr_actions, text="⚖️ Сравнить",
command=self._compare_threats).pack(side=tk.LEFT, padx=2)
ttk.Button(thr_actions, text="🎯 Подобрать угрозы (программа+ИИ)",
command=self._open_threats_picker).pack(side=tk.LEFT, padx=2)
ttk.Button(thr_actions, text="⬇️ Загрузить угрозы ФСТЭК",
command=self._download_fstec_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)
@@ -2969,13 +2971,12 @@ class DokoGenApp:
if not threats:
self._verify_log("❌ ИИ не вернул угрозы")
return
lb = self.is_threats_listbox
lb.delete(0, tk.END)
for t in threats:
lb.insert(tk.END, t)
self._collect_is() # сохранить в модель — галочки в словаре будут стоять
self.log(f"🤖 ИИ подобрал угроз: {len(threats)}")
self._verify_log(f"🤖 ИИ: {len(threats)} угроз")
# Запоминаем рекомендации ИИ (уже отфильтрованы по словарю УБИ) —
# не заменяем список молча, а открываем окно подбора с галочками.
self._last_ai_threats = list(threats)
self.log(f"🤖 ИИ рекомендует угроз: {len(threats)}")
self._verify_log(f"🤖 ИИ: {len(threats)} угроз (отфильтровано по словарю УБИ)")
self._open_threats_picker()
def _ask_amount(self, parent, title, prompt, initial=''):
"""Свой диалог ввода суммы: принимает «121 118 270 руб», «1 000» и т.п.
@@ -3032,23 +3033,25 @@ class DokoGenApp:
parent.wait_window(dlg)
return result['val']
def _compare_threats(self):
"""Сравнить угрозы, подобранные программой и ИИ."""
def _open_threats_picker(self):
"""🎯 Окно подбора угроз: программа (синий) + ИИ (красный) + совпавшие (зелёный).
Оператор отмечает галочками нужные угрозы (как в словаре) и жмёт
«✅ Применить выбранные» — список угроз заменяется выбранными.
По умолчанию отмечены все подобранные (программа + ИИ + текущие),
оператор снимает галочки с неугодных.
"""
if self._current_is_idx is None:
messagebox.showinfo("Сравнение", "Сначала выберите объект КИИ в списке слева")
messagebox.showinfo("Угрозы", "Сначала выберите объект КИИ в списке слева")
return
self._collect_is()
isys = self.model.information_systems[self._current_is_idx]
from .threat_rules import suggest_threats_program
program = suggest_threats_program(isys)
current = [str(self.is_threats_listbox.get(i)) for i in range(self.is_threats_listbox.size())]
if not current:
messagebox.showinfo("Сравнение", "Сначала подберите угрозы (программой или ИИ).")
return
ai = list(getattr(self, '_last_ai_threats', None) or [])
# Нормализация строк угроз для сравнения:
# «1. УБИ.001 Угроза…», «УБИ.001 Угроза…» и «УБИ.001 Угроза…» — это одна и та же угроза.
# Ключ сравнения — код УБИ, если он есть (ИИ может обрезать/перефразировать название);
# Нормализация строк угроз: ключ сравнения — код УБИ (если есть),
# иначе — нормализованный текст.
def _ubi_code(s):
m = re.search(r'уби\.?\s*(\d+)', str(s).lower())
@@ -3063,60 +3066,117 @@ class DokoGenApp:
code = _ubi_code(s)
return ('ubi', code) if code else ('name', _norm(s))
pset_norm = {_key(t) for t in program}
cset_norm = {_key(t) for t in current}
# Сортировка обоих списков по коду УБИ — чтобы совпадающие угрозы
# были на одних строках (удобно сверять).
def _sort_key(t):
code = _ubi_code(t)
return (0, int(code)) if code else (1, _norm(t))
program = sorted(program, key=_sort_key)
current = sorted(current, key=_sort_key)
# Показываем окно: слева подобрано программой, справа текущий список
dlg = tk.Toplevel(self.root)
dlg.title("Сравнение угроз: программа ↔ ИИ/текущий")
dlg.geometry("1400x700")
dlg.transient(self.root)
frm = ttk.Frame(dlg, padding=8)
frm.pack(fill=tk.BOTH, expand=True)
# Два списка на ~95% ширины окна (по ~47% + отступы)
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=22, width=70, font=('Courier', 9))
lb_p.pack(fill=tk.BOTH, expand=True, pady=4)
# Объединяем все источники: key → {text, src: {program, ai, current}}
merged = {}
def _add(t, src):
if not str(t or '').strip():
return
k = _key(t)
if k not in merged:
merged[k] = {'text': str(t).strip(), 'src': set()}
merged[k]['src'].add(src)
for t in program:
lb_p.insert(tk.END, t)
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=22, width=70, font=('Courier', 9))
lb_c.pack(fill=tk.BOTH, expand=True, pady=4)
_add(t, 'program')
for t in current:
lb_c.insert(tk.END, t)
_add(t, 'current')
for t in ai:
_add(t, 'ai')
# Подсветка совпадений (по коду УБИ / нормализованному виду)
common = pset_norm & cset_norm
for i in range(lb_p.size()):
if _key(lb_p.get(i)) in common:
lb_p.itemconfig(i, bg='#c8e6c9')
for i in range(lb_c.size()):
if _key(lb_c.get(i)) in common:
lb_c.itemconfig(i, bg='#c8e6c9')
items = sorted(merged.values(),
key=lambda v: (0, int(_ubi_code(v['text'])))
if _ubi_code(v['text']) else (1, _norm(v['text'])))
info = ttk.Label(frm,
text=f"Совпало угроз: {len(common)} (зелёным). "
f"Только программа: {len(program) - len(common)}, "
f"только ИИ/текущий: {len(current) - len(common)}.",
foreground='gray')
info.pack(anchor=tk.W, pady=(4, 0))
# Окно с чекбоксами (как выбор из словаря)
dlg = tk.Toplevel(self.root)
dlg.title(f"Подбор угроз: {isys.name or ''}")
dlg.geometry("980x700")
dlg.transient(self.root)
dlg.grab_set()
frame = ttk.Frame(dlg, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
legend = ttk.Label(frame,
text="🟦 синий — рекомендация программы | 🟥 красный — рекомендация ИИ | "
"🟩 зелёный — совпало | ⬜ белый — уже было в списке",
foreground='gray')
legend.pack(anchor=tk.W, pady=(0, 5))
# Поиск
search_var = tk.StringVar()
search_entry = ttk.Entry(frame, textvariable=search_var)
search_entry.pack(fill=tk.X, pady=(0, 5))
search_entry.focus_set()
canvas = tk.Canvas(frame, highlightthickness=0)
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
check_frame = ttk.Frame(canvas)
check_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=check_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
vars = {}
widgets = {}
for v in items:
src = v['src']
has_p = 'program' in src
has_a = 'ai' in src
if has_p and has_a:
bg = '#c8e6c9' # зелёный — совпало
elif has_p:
bg = '#b3d4fc' # синий — только программа
elif has_a:
bg = '#ffd0d0' # красный — только ИИ
else:
bg = '#ffffff' # белый — только текущий список
var = tk.BooleanVar(value=True) # по умолчанию отмечено всё
cb = tk.Checkbutton(check_frame, text=v['text'], variable=var, anchor=tk.W,
bg=bg, activebackground=bg, selectcolor='white',
justify=tk.LEFT, wraplength=900)
cb.pack(fill=tk.X, pady=1)
vars[v['text']] = var
widgets[v['text']] = cb
def _apply_search(*_):
q = search_var.get().strip().lower()
for text, cb in widgets.items():
if q and q not in text.lower():
cb.pack_forget()
else:
cb.pack(fill=tk.X, pady=1)
search_var.trace_add('write', _apply_search)
btn_frame = ttk.Frame(dlg, padding=10)
btn_frame.pack(fill=tk.X)
def _select_all():
for var in vars.values():
var.set(True)
def _clear_all():
for var in vars.values():
var.set(False)
def _apply():
selected = [t for t, var in vars.items() if var.get()]
selected.sort(key=lambda s: (0, int(_ubi_code(s)))
if _ubi_code(s) else (1, _norm(s)))
lb = self.is_threats_listbox
lb.delete(0, tk.END)
for t in selected:
lb.insert(tk.END, t)
self._collect_is()
self.log(f"🎯 Применено угроз: {len(selected)}")
dlg.destroy()
ttk.Button(btn_frame, text="Выбрать всё", command=_select_all).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="Снять всё", command=_clear_all).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="✅ Применить выбранные", command=_apply).pack(side=tk.RIGHT, padx=2)
ttk.Button(btn_frame, text="Отмена", command=dlg.destroy).pack(side=tk.RIGHT, padx=2)
dlg.wait_window()
def _check_threats_ai_click(self):
"""🤖 Проверить текущий список угроз через ИИ (не подбирать заново).
@@ -3200,14 +3260,65 @@ class DokoGenApp:
dlg = tk.Toplevel(self.root)
dlg.title(f"Проверка угроз ИИ — {obj_name}")
dlg.geometry("820x560")
dlg.geometry("860x600")
dlg.transient(self.root)
txt = tk.Text(dlg, wrap=tk.WORD, font=('Arial', 10))
txt.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
txt.insert('1.0', '\n'.join(lines))
txt.config(state=tk.DISABLED)
ttk.Button(dlg, text="Закрыть",
command=dlg.destroy).pack(pady=(0, 8))
def _ubi_code(s):
m = re.search(r'уби\.?\s*(\d+)', str(s).lower())
return m.group(1) if m else None
def _add_missing():
"""Добавить недостающие (только из словаря УБИ, полные названия)."""
lb = self.is_threats_listbox
existing = {(_ubi_code(lb.get(i)) or lb.get(i).strip().lower())
for i in range(lb.size())}
added = 0
for t in missing:
code = _ubi_code(t)
key = code or str(t).strip().lower()
if key in existing:
continue
lb.insert(tk.END, t)
existing.add(key)
added += 1
if added:
self._collect_is()
self.log(f"➕ Добавлено недостающих угроз (по словарю): {added}")
else:
self.log("➕ Недостающие уже есть в списке — ничего не добавлено")
dlg.destroy()
def _remove_extra():
"""Убрать лишние (только те, что реально есть в списке, по коду УБИ)."""
lb = self.is_threats_listbox
extra_codes = {c for c in (_ubi_code(t) for t in extra) if c}
removed = 0
for i in range(lb.size() - 1, -1, -1):
code = _ubi_code(lb.get(i))
if code and code in extra_codes:
lb.delete(i)
removed += 1
if removed:
self._collect_is()
self.log(f"➖ Убрано лишних угроз: {removed}")
else:
self.log("➖ Лишние не найдены в списке — ничего не убрано")
dlg.destroy()
btns = ttk.Frame(dlg)
btns.pack(pady=(0, 8))
if missing:
ttk.Button(btns, text="➕ Добавить недостающие (по словарю)",
command=_add_missing).pack(side=tk.LEFT, padx=4)
if extra:
ttk.Button(btns, text=" Убрать лишние",
command=_remove_extra).pack(side=tk.LEFT, padx=4)
ttk.Button(btns, text="Закрыть",
command=dlg.destroy).pack(side=tk.LEFT, padx=4)
self.log(f"🤖 Проверка угроз ИИ: лишних {len(extra)}, недостающих {len(missing)}")
self._verify_log('\n'.join(lines))