fix: угрозы — фильтр по словарю УБИ для всего от ИИ (мусор отбрасывается, УБИ.069 дополняется названием), «Подобрать угрозы» — окно с галочками (синий — программа, красный — ИИ, зелёный — совпало), «Проверить через ИИ» — вердикт + точечное применение по словарю, возвращена кнопка «Загрузить угрозы ФСТЭК»
This commit is contained in:
+182
-71
@@ -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))
|
||||
|
||||
|
||||
+69
-1
@@ -525,6 +525,66 @@ def compare_company_values(old_data, new_data):
|
||||
return diffs
|
||||
|
||||
|
||||
def _ubi_code(text):
|
||||
"""Извлекает код УБИ из строки: «УБИ.069 Название» → '069'."""
|
||||
m = re.search(r'уби\.?\s*(\d+)', str(text), re.IGNORECASE)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _norm_threat_text(text):
|
||||
"""Нормализация названия угрозы для сравнения: без ведущего номера, лишних пробелов."""
|
||||
s = re.sub(r'^\d+[.)]?\s*', '', str(text or '').strip())
|
||||
s = re.sub(r'\s+', ' ', s).strip()
|
||||
return s.lower()
|
||||
|
||||
|
||||
def filter_threats_by_dict(lines, threats_full=None):
|
||||
"""Приводит строки угроз (от ИИ или оператора) к каноническому виду по словарю УБИ.
|
||||
|
||||
Для каждой строки:
|
||||
- есть код УБИ → берём полную запись «УБИ.xxx Название» из словаря;
|
||||
- кода нет → ищем по названию (точное совпадение или вхождение);
|
||||
- не нашлось в словаре → отбрасываем (мусор от ИИ).
|
||||
|
||||
Возвращает уникальные канонические строки, отсортированные по коду УБИ.
|
||||
"""
|
||||
if threats_full is None:
|
||||
from .threat_rules import load_threats
|
||||
threats_full = load_threats()
|
||||
by_code = {}
|
||||
by_norm = {}
|
||||
for t in threats_full:
|
||||
t = str(t).strip()
|
||||
code = _ubi_code(t)
|
||||
if code:
|
||||
by_code.setdefault(code, t)
|
||||
by_norm.setdefault(_norm_threat_text(t), t)
|
||||
out = []
|
||||
seen = set()
|
||||
for line in lines or []:
|
||||
line = str(line or '').strip()
|
||||
if not line:
|
||||
continue
|
||||
code = _ubi_code(line)
|
||||
canon = None
|
||||
if code and code in by_code:
|
||||
canon = by_code[code]
|
||||
else:
|
||||
n = _norm_threat_text(line)
|
||||
if n in by_norm:
|
||||
canon = by_norm[n]
|
||||
else:
|
||||
for k, v in by_norm.items():
|
||||
if n and (n in k or k in n):
|
||||
canon = v
|
||||
break
|
||||
if canon and canon not in seen:
|
||||
seen.add(canon)
|
||||
out.append(canon)
|
||||
out.sort(key=lambda t: (int(_ubi_code(t)) if _ubi_code(t) else 10 ** 9, t))
|
||||
return out
|
||||
|
||||
|
||||
def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=None,
|
||||
threats=None):
|
||||
"""ИИ предлагает актуальные угрозы БИ для объекта КИИ по его характеристикам.
|
||||
@@ -571,6 +631,9 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N
|
||||
if isinstance(threats_out, str):
|
||||
threats_out = [threats_out]
|
||||
threats_out = [str(t).strip() for t in threats_out if str(t).strip()]
|
||||
# Жёсткий фильтр по словарю: мусор отбрасываем, «УБИ.069» дополняем
|
||||
# полным названием из актуального перечня (если он передан).
|
||||
threats_out = filter_threats_by_dict(threats_out, threats)
|
||||
return threats_out, None
|
||||
|
||||
|
||||
@@ -606,7 +669,12 @@ def check_threats_ai(security_data, current_threats, api_key, model='deepseek-ch
|
||||
js, err = _ai_json(api_key, model, base_url, system, user)
|
||||
if err:
|
||||
return None, err
|
||||
return js, None
|
||||
# «Недостающие»/«лишние» — тоже через словарь: мусор отбрасываем,
|
||||
# неполные строки («УБИ.069») дополняем полными названиями.
|
||||
missing = filter_threats_by_dict(js.get('недостающие') or [])
|
||||
extra = filter_threats_by_dict(js.get('лишние') or [])
|
||||
comment = js.get('комментарий') or ''
|
||||
return {'лишние': extra, 'недостающие': missing, 'комментарий': comment}, None
|
||||
|
||||
|
||||
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
|
||||
|
||||
Reference in New Issue
Block a user