fix: окна авторасчёта/сравнения не обрезают текст; СЗИ перенесены в «Безопасность»; ИИ-рекомендации по СЗИ; ИИ-проверка категорий ПП 127; пояснения к полям 8/9; упрощение панели угроз

This commit is contained in:
2026-08-05 01:31:13 +04:00
parent fafeea4fcf
commit d376a28184
4 changed files with 329 additions and 21 deletions
@@ -0,0 +1,4 @@
{
"system": "Ты — эксперт по категорированию объектов критической информационной инфраструктуры (187-ФЗ, Постановление Правительства РФ № 127). Проверь корректность присвоенных категорий значимости по показателям критериев. Категория значимости объекта определяется по максимальной из категорий показателей: I — если хотя бы один показатель I; II — если есть II и нет I; III — если есть III и нет I/II; «—» — если показатели не присвоены. Учитывай сферу деятельности субъекта и применяемость показателей (показатель 8 — госкорпорации/ГУП/госкомпании/стратегические АО; показатель 9 — все субъекты; показатель 10 — финансовый рынок/банки; показатель 13 — оборонно-промышленный комплекс).",
"response_format": "Верни ТОЛЬКО JSON: {\"оценка\": \"ok|warning|error\", \"замечания\": [{\"показатель\": \"номер\", \"категория\": \"текущая\", \"комментарий\": \"что не так и как должно быть\"}], \"предложения\": {\"criteria_values\": \"исправленный список показателей с категориями\", \"criteria_justification\": \"исправленное обоснование\"}, \"итоговая_категория\": \"I|II|III|—\"}. В «предложениях» — только если нужно что-то исправить, иначе пустые строки."
}
@@ -0,0 +1,4 @@
{
"system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). По категории значимости объекта КИИ, актуальным угрозам и уже выбранным средствам защиты информации (СЗИ) определи, каких средств не хватает, и дай рекомендации. Учитывай требования по категориям: для I категории — максимальный набор (антивирус, СКЗИ, SIEM, межсетевые экраны, средства анти-APT и т.д.), для II — расширенный набор, для III — базовый набор. Выбирай СЗИ ТОЛЬКО из реально существующих классов средств защиты (антивирусная защита, СКЗИ, межсетевой экран, SIEM, DLP, IDS/IPS, средства доверенной загрузки, контроля целостности, управления доступом и т.п.).",
"response_format": "Верни ТОЛЬКО JSON: {\"рекомендации\": [{\"сзи\": \"название класса/средства\", \"зачем\": \"для чего нужно\"}], \"общие_советы\": [\"...\"], \"достаточно\": true|false}. Поле «достаточно» — true, если текущий набор СЗИ покрывает требования категории. Не придумывай конкретные торговые марки, если их нет в списке — указывай класс средства."
}
+238 -21
View File
@@ -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
+83
View File
@@ -733,3 +733,86 @@ def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None
},
'apply': {'company': {k: v for k, v in proposals.items() if v}},
}, None
def suggest_defense_ai(defense_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-рекомендации по средствам защиты информации (СЗИ).
По категории значимости, угрозам и текущему списку СЗИ ИИ подсказывает,
чего не хватает (например, антивирус, СКЗИ, SIEM и т.п.).
Возвращает (результат, ошибка).
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
pr = _load_prompt('suggest_defense')
system = pr.get('system', (
"Ты — эксперт по безопасности КИИ (187-ФЗ, приказ ФСТЭК № 239). "
"По категории значимости объекта и актуальным угрозам определи, каких "
"средств защиты информации (СЗИ) не хватает, и дай рекомендации."
))
system = system + " " + pr.get('response_format',
'Верни ТОЛЬКО JSON: {"рекомендации": [{"сзи": "...", "зачем": "..."}], '
'"общие_советы": ["..."]}')
user = json.dumps(defense_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
return None, err
recs = js.get('рекомендации') or []
if not isinstance(recs, list):
recs = []
tips = js.get('общие_советы') or []
if isinstance(tips, str):
tips = [tips]
return {
'source': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'defense',
'data': {
'рекомендации': recs,
'общие_советы': tips,
},
}, None
def check_criteria_ai(criteria_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-проверка категорий значимости по ПП РФ № 127.
По данным объекта КИИ (назначение, сфера, категории по показателям,
обоснования) ИИ проверяет корректность присвоенных категорий.
Возвращает (результат, ошибка).
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
pr = _load_prompt('check_criteria')
system = pr.get('system', (
"Ты — эксперт по категорированию объектов КИИ (187-ФЗ, ПП РФ № 127). "
"Проверь присвоенные категории значимости по показателям критериев."
))
system = system + " " + pr.get('response_format',
'Верни ТОЛЬКО JSON: {"оценка": "ok|warning|error", '
'"замечания": [{"показатель": "...", "категория": "...", "комментарий": "..."}], '
'"предложения": {"criteria_values": "...", "criteria_justification": "..."}, '
'"итоговая_категория": "I|II|III|—"}')
user = json.dumps(criteria_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
return None, err
notes = js.get('замечания') or []
if isinstance(notes, str):
notes = [notes]
proposals = js.get('предложения') or {}
if not isinstance(proposals, dict):
proposals = {}
return {
'source': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'criteria',
'data': {
'rating': js.get('оценка', 'warning'),
'notes': notes,
'proposals': proposals,
'final_category': js.get('итоговая_категория', ''),
},
}, None