fix: доп. ОКВЭД из DaData (устойчивый парсинг + лог), сравнение угроз (шире окно, сортировка по УБИ), кнопка «Проверить через ИИ» (check_threats_ai), честные подсказки п.8/п.9, автозаполнение ΔДоходов из балансовой стоимости КИИ, диалог _ask_amount вместо askfloat (формат «121 118 270 руб»), формулировка Бусредн по документу ФСТЭК (трёхлетний период)
This commit is contained in:
+220
-29
@@ -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)
|
||||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome',
|
||||||
e_budget = _row("Доход федерального бюджета, усреднённый за 3 года Бусредн (руб.):", 'budget',
|
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)
|
||||||
|
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,11 +2298,23 @@ 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="⚠️ ΔДоходов — подставлено из вкладки «Организация» "
|
||||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
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)
|
||||||
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 '')
|
||||||
_n_years = len(_incs_filled)
|
_n_years = len(_incs_filled)
|
||||||
@@ -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} руб.) — подставлено автоматически")
|
||||||
"Снижение уровня доходов субъекта КИИ ΔДоходов (руб.):\n"
|
else:
|
||||||
"(наихудший сценарий компьютерной атаки на объект)" + _hint,
|
dincome = self._ask_amount(
|
||||||
parent=dialog, minvalue=0)
|
dialog, "Показатель 9 — ущерб бюджетам",
|
||||||
if dincome is None:
|
"Снижение уровня доходов субъекта КИИ ΔДоходов (руб.):\n"
|
||||||
continue
|
"(наихудший сценарий компьютерной атаки на объект)")
|
||||||
budget = DEFAULT_BUDGET_AVG # Бусредн (доход федерального бюджета за 3 года)
|
if dincome is None:
|
||||||
|
continue
|
||||||
|
_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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user