fix: доп. ОКВЭД из DaData (устойчивый парсинг + лог), сравнение угроз (шире окно, сортировка по УБИ), кнопка «Проверить через ИИ» (check_threats_ai), честные подсказки п.8/п.9, автозаполнение ΔДоходов из балансовой стоимости КИИ, диалог _ask_amount вместо askfloat (формат «121 118 270 руб»), формулировка Бусредн по документу ФСТЭК (трёхлетний период)

This commit is contained in:
LocoAgent
2026-08-05 10:41:31 +04:00
parent 5af23ae525
commit bf75d67f5e
2 changed files with 264 additions and 31 deletions
+215 -24
View File
@@ -761,9 +761,12 @@ class DokoGenApp:
self._company_entries['okved'].insert(0, resolve_okved(comp['okved'], _tok3)) self._company_entries['okved'].insert(0, resolve_okved(comp['okved'], _tok3))
if want('okved_extra') and comp.get('okved_extra') and hasattr(self, 'okved_extra_listbox'): if want('okved_extra') and comp.get('okved_extra') and hasattr(self, 'okved_extra_listbox'):
self.okved_extra_listbox.delete(0, tk.END) self.okved_extra_listbox.delete(0, tk.END)
_applied = 0
for code in comp['okved_extra']: for code in comp['okved_extra']:
if str(code).strip(): if str(code).strip():
self.okved_extra_listbox.insert(tk.END, resolve_okved(str(code).strip(), _tok3)) self.okved_extra_listbox.insert(tk.END, resolve_okved(str(code).strip(), _tok3))
_applied += 1
self.log(f"✅ Доп. ОКВЭД из DaData: применено {_applied} (всего в ответе: {len(comp['okved_extra'])})")
if want('chief'): if want('chief'):
if comp.get('chief_position') and 'chief_position' in self._official_entries: if comp.get('chief_position') and 'chief_position' in self._official_entries:
self._official_entries['chief_position'].delete(0, tk.END) self._official_entries['chief_position'].delete(0, tk.END)
@@ -1087,6 +1090,8 @@ class DokoGenApp:
command=self._suggest_threats_program_click).pack(side=tk.LEFT, padx=2) 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) 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="⚖️ Сравнить", ttk.Button(thr_actions, text="⚖️ Сравнить",
command=self._compare_threats).pack(side=tk.LEFT, padx=2) 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 = ttk.Label(thr_actions, text="", foreground='gray')
@@ -2245,14 +2250,24 @@ class DokoGenApp:
e_profit = _row("Прибыль субъекта КИИ за прошедший год (руб.):", 'profit', profit) e_profit = _row("Прибыль субъекта КИИ за прошедший год (руб.):", 'profit', profit)
e_tax = _row("Налог на прибыль за прошедший год (руб.):", 'tax', profit_tax) e_tax = _row("Налог на прибыль за прошедший год (руб.):", 'tax', profit_tax)
e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome', '') # ΔДоходов: если есть балансовая стоимость объекта КИИ — предзаполняем её
ttk.Label(frm, text="⚠️ ΔДоходов — экспертная оценка снижения доходов субъекта при наихудшем сценарии " _bv9 = getattr(isys, 'balance_value', '') or ''
"нарушения (в опроснике её нет, вводится вручную; можно приравнять к ущербу из показателя 8).", _bvn9 = _num(_bv9)
e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome',
f"{_bvn9:,.0f}" if _bvn9 else '')
if _bvn9:
ttk.Label(frm, text="⚠️ ΔДоходов — подставлена балансовая стоимость объекта КИИ "
f"({_bvn9:,.0f} руб.); можно поправить.",
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
e_budget = _row("Доход федерального бюджета, усреднённый за 3 года Бусредн (руб.):", 'budget', else:
ttk.Label(frm, text="⚠️ ΔДоходов — не заполнено: введите вручную или заполните "
"балансовую стоимость объекта КИИ во вкладке «Объект».",
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
e_budget = _row("Прогнозируемый годовой доход федерального бюджета, усреднённый за планируемый трёхлетний период — Бусредн (руб.):", 'budget',
f'{DEFAULT_BUDGET_AVG:.0f}') f'{DEFAULT_BUDGET_AVG:.0f}')
ttk.Label(frm, text="⚠️ Бусредн — средний доход федерального бюджета за 3 года (данные Минфина, " ttk.Label(frm, text="⚠️ Бусредн — прогнозируемый годовой доход федерального бюджета, "
"подставлено значение по умолчанию; при необходимости уточните).", "усреднённый за планируемый трёхлетний период (данные Минфина; "
"подставлено значение по умолчанию, при необходимости уточните).",
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
ttk.Label(frm, text="Прочие составляющие ΔБ (если применимы, иначе 0):", ttk.Label(frm, text="Прочие составляющие ΔБ (если применимы, иначе 0):",
font=('', 9, 'bold')).pack(anchor=tk.W, pady=(8, 0)) font=('', 9, 'bold')).pack(anchor=tk.W, pady=(8, 0))
@@ -2283,10 +2298,22 @@ class DokoGenApp:
_incs_filled = [x for x in _incs if x is not None] _incs_filled = [x for x in _incs if x is not None]
_d_avg = (sum(_incs_filled) / len(_incs_filled)) if _incs_filled else None _d_avg = (sum(_incs_filled) / len(_incs_filled)) if _incs_filled else None
_d_loss = _num(getattr(self.model, 'income_loss', '') or '') _d_loss = _num(getattr(self.model, 'income_loss', '') or '')
_bv8 = getattr(isys, 'balance_value', '') or ''
_bvn8 = _num(_bv8)
_dincome_def = _d_loss or _bvn8
e_dincome = _row("Снижение уровня дохода от нарушения критических процессов (руб.):", e_dincome = _row("Снижение уровня дохода от нарушения критических процессов (руб.):",
'dincome', f"{_d_loss:,.0f}" if _d_loss else '') 'dincome', f"{_dincome_def:,.0f}" if _dincome_def else '')
ttk.Label(frm, text="⚠️ ΔДоходов — поле «Снижение уровня дохода (показатель 8)» из вкладки «Организация» " if _d_loss:
"(подставлено автоматически, можно поправить).", ttk.Label(frm, text="⚠️ ΔДоходов — подставлено из вкладки «Организация» "
f"({_d_loss:,.0f} руб.); можно поправить.",
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
elif _bvn8:
ttk.Label(frm, text="⚠️ ΔДоходов — подставлена балансовая стоимость объекта КИИ "
f"({_bvn8:,.0f} руб.); можно поправить.",
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
else:
ttk.Label(frm, text="⚠️ ΔДоходов — не заполнено: введите вручную или заполните "
"«Снижение уровня дохода» во вкладке «Организация».",
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
e_budget = _row("Доходы субъекта КИИ, усреднённые за 5 лет (руб.):", e_budget = _row("Доходы субъекта КИИ, усреднённые за 5 лет (руб.):",
'budget', f"{_d_avg:,.0f}" if _d_avg else '') 'budget', f"{_d_avg:,.0f}" if _d_avg else '')
@@ -2350,7 +2377,7 @@ class DokoGenApp:
db = dnp + ndpi + div + vso + ns db = dnp + ndpi + div + vso + ns
lines.append(f"ΔНДПИ = {ndpi:,.0f} руб., ΔДИВ = {div:,.0f} руб., ΔВСО = {vso:,.0f} руб., ΔНС = {ns:,.0f} руб.") lines.append(f"ΔНДПИ = {ndpi:,.0f} руб., ΔДИВ = {div:,.0f} руб., ΔВСО = {vso:,.0f} руб., ΔНС = {ns:,.0f} руб.")
lines.append(f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.") lines.append(f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.")
lines.append(f"Бусредн (доход федерального бюджета за 3 года): {b:,.0f} руб.") lines.append(f"Бусредн (прогноз годового дохода федбюджета, усреднённый за планируемый трёхлетний период): {b:,.0f} руб.")
val = db / b * 100 val = db / b * 100
lines.append(f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {b:,.0f} × 100 = {val:.6f} %") lines.append(f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {b:,.0f} × 100 = {val:.6f} %")
if val > 0.001: if val > 0.001:
@@ -2678,20 +2705,24 @@ class DokoGenApp:
profit = _num2(getattr(self.model, 'profit', '') or '') profit = _num2(getattr(self.model, 'profit', '') or '')
tax = _num2(getattr(self.model, 'profit_tax', '') or '') tax = _num2(getattr(self.model, 'profit_tax', '') or '')
if profit and tax: if profit and tax:
# Подсказка: балансовая стоимость объекта как ориентир для ΔДоходов # ΔДоходов: если есть балансовая стоимость объекта КИИ —
_hint = '' # подставляем её автоматически (наихудший сценарий = объект
# выходит из строя, его надо восстанавливать/покупать заново)
_bv = getattr(isys, 'balance_value', '') or '' _bv = getattr(isys, 'balance_value', '') or ''
_bvn = _num2(_bv) _bvn = _num2(_bv)
if _bvn: if _bvn:
_hint = f"\n(балансовая стоимость объекта: {_bvn:,.0f} руб. — можно использовать как ориентир)" dincome = _bvn
dincome = simpledialog.askfloat( _dincome_note = (f"ΔДоходов = балансовая стоимость объекта КИИ "
"Показатель 9 — ущерб бюджетам", f"({dincome:,.0f} руб.) — подставлено автоматически")
else:
dincome = self._ask_amount(
dialog, "Показатель 9 — ущерб бюджетам",
"Снижение уровня доходов субъекта КИИ ΔДоходов (руб.):\n" "Снижение уровня доходов субъекта КИИ ΔДоходов (руб.):\n"
"(наихудший сценарий компьютерной атаки на объект)" + _hint, "(наихудший сценарий компьютерной атаки на объект)")
parent=dialog, minvalue=0)
if dincome is None: if dincome is None:
continue continue
budget = DEFAULT_BUDGET_AVG # Бусредн (доход федерального бюджета за 3 года) _dincome_note = f"ΔДоходов (введено оператором) = {dincome:,.0f} руб."
budget = DEFAULT_BUDGET_AVG # Бусредн (прогноз годового дохода федбюджета, усреднённый за 3 года)
knp = tax / profit knp = tax / profit
dnp = dincome * knp dnp = dincome * knp
if dincome > profit: if dincome > profit:
@@ -2718,11 +2749,12 @@ class DokoGenApp:
if target: if target:
var.set(target) var.set(target)
calc_txt = ( calc_txt = (
f"{_dincome_note}\n"
f"Коэффициент КНП = налог/прибыль = {tax:,.0f} / {profit:,.0f} = {knp:.4f}\n" f"Коэффициент КНП = налог/прибыль = {tax:,.0f} / {profit:,.0f} = {knp:.4f}\n"
f"ΔНП = ΔДоходов × КНП = {dincome:,.0f} × {knp:.4f} = {dnp:,.1f} руб.\n" f"ΔНП = ΔДоходов × КНП = {dincome:,.0f} × {knp:.4f} = {dnp:,.1f} руб.\n"
f"ΔНДПИ = {ndpi:,.0f}, ΔДИВ = {div:,.0f}, ΔВСО = {vso:,.0f}, ΔНС = {ns:,.0f} руб.\n" f"ΔНДПИ = {ndpi:,.0f}, ΔДИВ = {div:,.0f}, ΔВСО = {vso:,.0f}, ΔНС = {ns:,.0f} руб.\n"
f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.\n" f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.\n"
f"Бусредн (доход федерального бюджета за 3 года) = {budget:,.0f} руб.\n" f"Бусредн (прогноз годового дохода федбюджета, усреднённый за 3 года) = {budget:,.0f} руб.\n"
f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {budget:,.0f} × 100 = {val:.6f} %\n" f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {budget:,.0f} × 100 = {val:.6f} %\n"
f"Категория значимости: {cat or '— (не присваивается)'}" f"Категория значимости: {cat or '— (не присваивается)'}"
) )
@@ -2945,6 +2977,61 @@ class DokoGenApp:
self.log(f"🤖 ИИ подобрал угроз: {len(threats)}") self.log(f"🤖 ИИ подобрал угроз: {len(threats)}")
self._verify_log(f"🤖 ИИ: {len(threats)} угроз") self._verify_log(f"🤖 ИИ: {len(threats)} угроз")
def _ask_amount(self, parent, title, prompt, initial=''):
"""Свой диалог ввода суммы: принимает «121 118 270 руб», «1 000» и т.п.
simpledialog.askfloat НЕ умеет парсить такие строки (ошибка
«not a floating-point value please try again») — поэтому свой диалог.
Возвращает float или None (отмена).
"""
def _parse(s):
s = str(s or '').strip()
if not s:
return None
s = s.replace('\u00a0', ' ').replace(' ', '')\
.replace('руб', '').replace('', '').replace('р.', '').replace(',', '.')
try:
return float(s)
except Exception:
return None
result = {'val': None}
dlg = tk.Toplevel(parent)
dlg.title(title)
dlg.geometry("560x200")
dlg.transient(parent)
dlg.grab_set()
ttk.Label(dlg, text=prompt, wraplength=520, justify=tk.LEFT).pack(
padx=12, pady=(12, 6), anchor=tk.W)
e = ttk.Entry(dlg, width=42)
e.insert(0, str(initial or ''))
e.pack(padx=12, pady=4, fill=tk.X)
err = ttk.Label(dlg, text="", foreground='red')
err.pack(anchor=tk.W, padx=12)
def _ok():
v = _parse(e.get())
if v is None:
err.config(text="Введите число, например: 121 118 270")
return
result['val'] = v
dlg.grab_release()
dlg.destroy()
def _cancel():
dlg.grab_release()
dlg.destroy()
btns = ttk.Frame(dlg)
btns.pack(pady=8)
ttk.Button(btns, text="ОК", command=_ok).pack(side=tk.LEFT, padx=4)
ttk.Button(btns, text="Отмена", command=_cancel).pack(side=tk.LEFT, padx=4)
e.bind('<Return>', lambda _ev: _ok())
dlg.lift()
dlg.focus_force()
parent.wait_window(dlg)
return result['val']
def _compare_threats(self): def _compare_threats(self):
"""Сравнить угрозы, подобранные программой и ИИ.""" """Сравнить угрозы, подобранные программой и ИИ."""
if self._current_is_idx is None: if self._current_is_idx is None:
@@ -2979,27 +3066,38 @@ class DokoGenApp:
pset_norm = {_key(t) for t in program} pset_norm = {_key(t) for t in program}
cset_norm = {_key(t) for t in current} 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 = tk.Toplevel(self.root)
dlg.title("Сравнение угроз: программа ↔ ИИ/текущий") dlg.title("Сравнение угроз: программа ↔ ИИ/текущий")
dlg.geometry("960x600") dlg.geometry("1400x700")
dlg.transient(self.root) dlg.transient(self.root)
frm = ttk.Frame(dlg, padding=8) frm = ttk.Frame(dlg, padding=8)
frm.pack(fill=tk.BOTH, expand=True) frm.pack(fill=tk.BOTH, expand=True)
# Два списка на ~95% ширины окна (по ~47% + отступы)
left = ttk.Frame(frm) left = ttk.Frame(frm)
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 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) ttk.Label(left, text=f"⚙️ Программа ({len(program)}) — отсортировано по УБИ",
lb_p = tk.Listbox(left, height=18) 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) lb_p.pack(fill=tk.BOTH, expand=True, pady=4)
for t in program: for t in program:
lb_p.insert(tk.END, t) lb_p.insert(tk.END, t)
right = ttk.Frame(frm) right = ttk.Frame(frm)
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 0)) 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) ttk.Label(right, text=f"🤖 ИИ / текущий ({len(current)}) — отсортировано по УБИ",
lb_c = tk.Listbox(right, height=18) 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) lb_c.pack(fill=tk.BOTH, expand=True, pady=4)
for t in current: for t in current:
lb_c.insert(tk.END, t) lb_c.insert(tk.END, t)
@@ -3020,6 +3118,99 @@ class DokoGenApp:
foreground='gray') foreground='gray')
info.pack(anchor=tk.W, pady=(4, 0)) info.pack(anchor=tk.W, pady=(4, 0))
def _check_threats_ai_click(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
current = [str(self.is_threats_listbox.get(i)) for i in range(self.is_threats_listbox.size())]
if not current:
messagebox.showinfo("Угрозы", "Сначала подберите угрозы (программой или ИИ).")
return
self._collect_is()
isys = self.model.information_systems[self._current_is_idx]
from .threat_rules import _has_internet
has_internet = _has_internet(isys)
data = {
'name': isys.name or '',
'description': isys.description or '',
'object_type': isys.object_type or '',
'sphere': isys.sphere or '',
'architecture': isys.architecture or '',
'net_category': isys.net_category or '',
'attacker_category': isys.attacker_category or '',
'has_internet': has_internet,
}
self.log(f"🤖 Запрос к ИИ: проверка угроз для «{isys.name}» (в списке: {len(current)})")
self._verify_log("Проверка угроз через ИИ…")
def _run():
try:
from .verify import check_threats_ai
res, err = check_threats_ai(
data, current, settings['ai_api_key'], settings.get('ai_model'),
settings.get('ai_base_url'))
self.root.after(0, lambda: self._check_threats_ai_done(res, err, isys.name))
except Exception as e:
self.root.after(0, lambda: self._check_threats_ai_done(None, str(e), isys.name))
threading.Thread(target=_run, daemon=True).start()
def _check_threats_ai_done(self, res, err, obj_name):
"""Показ результата проверки угроз ИИ."""
if err:
self.log(f"❌ Проверка угроз: {err}")
self._verify_log(f"{err}")
return
if not isinstance(res, dict):
self.log("❌ ИИ не вернул результат проверки")
return
extra = res.get('лишние') or []
missing = res.get('недостающие') or []
comment = res.get('комментарий') or ''
if isinstance(extra, str):
extra = [extra]
if isinstance(missing, str):
missing = [missing]
lines = [f"🤖 Проверка угроз для «{obj_name}»:"]
lines.append(f"Всего в списке: {self.is_threats_listbox.size()}")
if extra:
lines.append(f"\n❌ Возможно ЛИШНИЕ ({len(extra)}):")
for t in extra:
lines.append(f"{t}")
else:
lines.append("\n❌ Лишних не найдено")
if missing:
lines.append(f"\n➕ Возможно НЕ ХВАТАЕТ ({len(missing)}):")
for t in missing:
lines.append(f"{t}")
else:
lines.append("\n➕ Недостающих не найдено")
if comment:
lines.append(f"\n💬 Комментарий: {comment}")
lines.append("\nРекомендации ИИ — оператор принимает решение.")
dlg = tk.Toplevel(self.root)
dlg.title(f"Проверка угроз ИИ — {obj_name}")
dlg.geometry("820x560")
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))
self.log(f"🤖 Проверка угроз ИИ: лишних {len(extra)}, недостающих {len(missing)}")
self._verify_log('\n'.join(lines))
def _check_security_ai(self): def _check_security_ai(self):
"""Проверка вкладки «Безопасность» через ИИ (DeepSeek/OpenAI-совместимый API).""" """Проверка вкладки «Безопасность» через ИИ (DeepSeek/OpenAI-совместимый API)."""
if self._current_is_idx is None: if self._current_is_idx is None:
+44 -2
View File
@@ -340,8 +340,15 @@ def check_company_dadata(inn, token):
# ОКВЭД: основной (okved) и дополнительные (okveds — список словарей с 'code') # ОКВЭД: основной (okved) и дополнительные (okveds — список словарей с 'code')
okved = d.get('okved', '') or '' okved = d.get('okved', '') or ''
okved_extra = [] okved_extra = []
for ok in (d.get('okveds') or []): raw_okveds = d.get('okveds') or []
code = (ok or {}).get('code') or '' if isinstance(raw_okveds, str):
# DaData иногда отдаёт доп. ОКВЭД одной строкой через запятую
raw_okveds = [x.strip() for x in raw_okveds.split(',') if x.strip()]
for ok in raw_okveds:
if isinstance(ok, dict):
code = (ok.get('code') or '').strip()
else:
code = str(ok or '').strip()
if code: if code:
okved_extra.append(code) okved_extra.append(code)
@@ -567,6 +574,41 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N
return threats_out, None return threats_out, None
def check_threats_ai(security_data, current_threats, api_key, model='deepseek-chat', base_url=None):
"""ИИ проверяет уже подобранный список угроз БИ для объекта КИИ.
В отличие от suggest_threats_ai (подбор заново), эта функция проверяет
текущий список: какие угрозы лишние, каких не хватает, и даёт комментарий.
Возвращает (dict, ошибка):
{"лишние": [...], "недостающие": [...], "комментарий": "..."}
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
if not current_threats:
return None, "Список угроз пуст — сначала подберите угрозы (программой или ИИ)."
system = (
"Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры "
"(187-ФЗ, приказ ФСТЭК № 236). Проверь, корректно ли подобран перечень угроз "
"безопасности информации для объекта КИИ."
)
user = (
"Характеристики объекта КИИ:\n"
f"{json.dumps(security_data, ensure_ascii=False)}\n\n"
"Текущий перечень угроз (подобран программой или оператором):\n"
+ "\n".join(f"{i+1}. {t}" for i, t in enumerate(current_threats))
+ "\n\nВерни ТОЛЬКО JSON: "
'{"лишние": ["угрозы, которые НЕ актуальны для этого объекта"], '
'"недостающие": ["актуальные угрозы, которых в списке не хватает"], '
'"комментарий": "краткий вывод по перечню"}'
)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
return None, err
return js, None
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None, def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
typical_objects=None, spheres=None, object_types=None, typical_objects=None, spheres=None, object_types=None,
architectures=None): architectures=None):