feat: вкладка «Безопасность» — словарики+чекбоксы, загрузка угроз ФСТЭК (БДУ) с датой актуализации, ИИ-проверка
- категория нарушителя — выпадающий список (attacker_categories.json) - характеристика нарушителя, угрозы, инциденты, показатели критериев, обоснование, орг./тех. меры — списки с кнопками (+/-/редактировать/из словаря) - словари: incidents.json, criteria_indicators.json, org_measures.json, tech_measures.json, threats.json - кнопка «⬇️ Загрузить угрозы ФСТЭК»: парсит bdu.fstec.ru (227 угроз), сохраняет threats_fstec.json + дату актуализации; подпись «Актуально на: …» или предупреждение, если ещё не скачивалось - кнопка «🤖 Проверить безопасность через ИИ»: DeepSeek сверяет заполненное с 236 приказом и даёт замечания/предложения (check_security_ai в verify.py) - новое поле модели attacker_characteristics
This commit is contained in:
+262
-29
@@ -941,25 +941,134 @@ class DokoGenApp:
|
||||
setattr(self, attr, box)
|
||||
sw_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# ===== Вкладка «Безопасность (236)» =====
|
||||
# ===== Вкладка «Безопасность» =====
|
||||
sec_tab = ttk.Frame(nb2)
|
||||
nb2.add(sec_tab, text="Безопасность")
|
||||
sec_frame = ttk.Frame(sec_tab, padding=8)
|
||||
sec_frame.pack(fill=tk.BOTH, expand=True)
|
||||
sec_fields = [
|
||||
("Категория нарушителя:", "is_attacker_category_entry"),
|
||||
("Основные угрозы БИ:", "is_threats_entry"),
|
||||
("Типы компьютерных инцидентов:", "is_incidents_entry"),
|
||||
("Показатели критериев значимости:", "is_criteria_values_entry"),
|
||||
("Обоснование значений показателей:", "is_criteria_justification_entry"),
|
||||
("Организационные меры:", "is_org_measures_entry"),
|
||||
("Технические меры:", "is_tech_measures_entry"),
|
||||
]
|
||||
for i, (label, attr) in enumerate(sec_fields):
|
||||
ttk.Label(sec_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
|
||||
entry = ttk.Entry(sec_frame, width=70)
|
||||
entry.grid(row=i, column=1, padx=8, sticky=tk.EW, pady=1)
|
||||
setattr(self, attr, entry)
|
||||
|
||||
# --- Категория нарушителя (словарик) ---
|
||||
ttk.Label(sec_frame, text="Категория нарушителя:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
self.is_attacker_category_combo = ttk.Combobox(sec_frame, width=68, state='readonly')
|
||||
self.is_attacker_category_combo.grid(row=0, column=1, padx=8, sticky=tk.EW, pady=1)
|
||||
try:
|
||||
import json as _json
|
||||
_ac = os.path.join(os.path.dirname(__file__), 'dictionaries', 'attacker_categories.json')
|
||||
with open(_ac, encoding='utf-8') as _f:
|
||||
self.is_attacker_category_combo['values'] = [''] + _json.load(_f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Характеристика нарушителя ---
|
||||
ttk.Label(sec_frame, text="Характеристика нарушителя:").grid(row=1, column=0, sticky=tk.NW, pady=2)
|
||||
self.is_attacker_characteristics_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
|
||||
self.is_attacker_characteristics_listbox.grid(row=1, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
|
||||
sec_char_btn = ttk.Frame(sec_frame)
|
||||
sec_char_btn.grid(row=1, column=2, sticky=tk.N, padx=(4, 0))
|
||||
ttk.Button(sec_char_btn, text="+ Добавить",
|
||||
command=lambda: self._add_sw_item(self.is_attacker_characteristics_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_char_btn, text="- Удалить",
|
||||
command=lambda: self._del_sw_item(self.is_attacker_characteristics_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_char_btn, text="✏️ Редактировать",
|
||||
command=lambda: self._edit_sw_item(self.is_attacker_characteristics_listbox)).pack(fill=tk.X, pady=1)
|
||||
|
||||
# --- Основные угрозы БИ (словарик + загрузка с ФСТЭК) ---
|
||||
ttk.Label(sec_frame, text="Основные угрозы БИ:").grid(row=2, column=0, sticky=tk.W, pady=2)
|
||||
self.is_threats_listbox = tk.Listbox(sec_frame, height=5, exportselection=False)
|
||||
self.is_threats_listbox.grid(row=2, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
|
||||
sec_thr_btn = ttk.Frame(sec_frame)
|
||||
sec_thr_btn.grid(row=2, column=2, sticky=tk.N, padx=(4, 0))
|
||||
ttk.Button(sec_thr_btn, text="+ Добавить",
|
||||
command=lambda: self._add_sw_item(self.is_threats_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_thr_btn, text="- Удалить",
|
||||
command=lambda: self._del_sw_item(self.is_threats_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_thr_btn, text="✏️ Редактировать",
|
||||
command=lambda: self._edit_sw_item(self.is_threats_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_thr_btn, text="📚 Из словаря",
|
||||
command=lambda: self._select_sw_from_dict(self.is_threats_listbox, 'threats_fstec.json')).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_thr_btn, text="⬇️ Загрузить угрозы ФСТЭК",
|
||||
command=self._download_fstec_threats).pack(fill=tk.X, pady=1)
|
||||
self._threats_date_label = ttk.Label(sec_frame, text="", foreground='gray')
|
||||
self._threats_date_label.grid(row=3, column=1, sticky=tk.W, padx=8, pady=(0, 2))
|
||||
|
||||
# --- Типы компьютерных инцидентов (словарик) ---
|
||||
ttk.Label(sec_frame, text="Типы компьютерных инцидентов:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
||||
self.is_incidents_listbox = tk.Listbox(sec_frame, height=4, exportselection=False)
|
||||
self.is_incidents_listbox.grid(row=4, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
|
||||
sec_inc_btn = ttk.Frame(sec_frame)
|
||||
sec_inc_btn.grid(row=4, column=2, sticky=tk.N, padx=(4, 0))
|
||||
ttk.Button(sec_inc_btn, text="+ Добавить",
|
||||
command=lambda: self._add_sw_item(self.is_incidents_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_inc_btn, text="- Удалить",
|
||||
command=lambda: self._del_sw_item(self.is_incidents_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_inc_btn, text="✏️ Редактировать",
|
||||
command=lambda: self._edit_sw_item(self.is_incidents_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_inc_btn, text="📚 Из словаря",
|
||||
command=lambda: self._select_sw_from_dict(self.is_incidents_listbox, 'incidents.json')).pack(fill=tk.X, pady=1)
|
||||
|
||||
# --- Показатели критериев значимости (словарик) ---
|
||||
ttk.Label(sec_frame, text="Показатели критериев значимости:").grid(row=5, column=0, sticky=tk.W, pady=2)
|
||||
self.is_criteria_values_listbox = tk.Listbox(sec_frame, height=4, exportselection=False)
|
||||
self.is_criteria_values_listbox.grid(row=5, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
|
||||
sec_crit_btn = ttk.Frame(sec_frame)
|
||||
sec_crit_btn.grid(row=5, column=2, sticky=tk.N, padx=(4, 0))
|
||||
ttk.Button(sec_crit_btn, text="+ Добавить",
|
||||
command=lambda: self._add_sw_item(self.is_criteria_values_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_crit_btn, text="- Удалить",
|
||||
command=lambda: self._del_sw_item(self.is_criteria_values_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_crit_btn, text="✏️ Редактировать",
|
||||
command=lambda: self._edit_sw_item(self.is_criteria_values_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_crit_btn, text="📚 Из словаря",
|
||||
command=lambda: self._select_sw_from_dict(self.is_criteria_values_listbox, 'criteria_indicators.json')).pack(fill=tk.X, pady=1)
|
||||
|
||||
# --- Обоснование значений показателей ---
|
||||
ttk.Label(sec_frame, text="Обоснование значений показателей:").grid(row=6, column=0, sticky=tk.NW, pady=2)
|
||||
self.is_criteria_justification_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
|
||||
self.is_criteria_justification_listbox.grid(row=6, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
|
||||
sec_just_btn = ttk.Frame(sec_frame)
|
||||
sec_just_btn.grid(row=6, column=2, sticky=tk.N, padx=(4, 0))
|
||||
ttk.Button(sec_just_btn, text="+ Добавить",
|
||||
command=lambda: self._add_sw_item(self.is_criteria_justification_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_just_btn, text="- Удалить",
|
||||
command=lambda: self._del_sw_item(self.is_criteria_justification_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_just_btn, text="✏️ Редактировать",
|
||||
command=lambda: self._edit_sw_item(self.is_criteria_justification_listbox)).pack(fill=tk.X, pady=1)
|
||||
|
||||
# --- Организационные меры (словарик) ---
|
||||
ttk.Label(sec_frame, text="Организационные меры:").grid(row=7, column=0, sticky=tk.W, pady=2)
|
||||
self.is_org_measures_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
|
||||
self.is_org_measures_listbox.grid(row=7, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
|
||||
sec_org_btn = ttk.Frame(sec_frame)
|
||||
sec_org_btn.grid(row=7, column=2, sticky=tk.N, padx=(4, 0))
|
||||
ttk.Button(sec_org_btn, text="+ Добавить",
|
||||
command=lambda: self._add_sw_item(self.is_org_measures_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_org_btn, text="- Удалить",
|
||||
command=lambda: self._del_sw_item(self.is_org_measures_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_org_btn, text="✏️ Редактировать",
|
||||
command=lambda: self._edit_sw_item(self.is_org_measures_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_org_btn, text="📚 Из словаря",
|
||||
command=lambda: self._select_sw_from_dict(self.is_org_measures_listbox, 'org_measures.json')).pack(fill=tk.X, pady=1)
|
||||
|
||||
# --- Технические меры (словарик) ---
|
||||
ttk.Label(sec_frame, text="Технические меры:").grid(row=8, column=0, sticky=tk.W, pady=2)
|
||||
self.is_tech_measures_listbox = tk.Listbox(sec_frame, height=3, exportselection=False)
|
||||
self.is_tech_measures_listbox.grid(row=8, column=1, sticky=tk.EW, padx=(8, 4), pady=1)
|
||||
sec_tech_btn = ttk.Frame(sec_frame)
|
||||
sec_tech_btn.grid(row=8, column=2, sticky=tk.N, padx=(4, 0))
|
||||
ttk.Button(sec_tech_btn, text="+ Добавить",
|
||||
command=lambda: self._add_sw_item(self.is_tech_measures_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_tech_btn, text="- Удалить",
|
||||
command=lambda: self._del_sw_item(self.is_tech_measures_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_tech_btn, text="✏️ Редактировать",
|
||||
command=lambda: self._edit_sw_item(self.is_tech_measures_listbox)).pack(fill=tk.X, pady=1)
|
||||
ttk.Button(sec_tech_btn, text="📚 Из словаря",
|
||||
command=lambda: self._select_sw_from_dict(self.is_tech_measures_listbox, 'tech_measures.json')).pack(fill=tk.X, pady=1)
|
||||
|
||||
# --- Кнопка ИИ-проверки ---
|
||||
ttk.Button(sec_frame, text="🤖 Проверить безопасность через ИИ",
|
||||
command=self._check_security_ai).grid(
|
||||
row=9, column=0, columnspan=3, sticky=tk.W, pady=(10, 0))
|
||||
|
||||
sec_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# ===== Вкладка «Средства защиты» =====
|
||||
@@ -985,6 +1094,8 @@ class DokoGenApp:
|
||||
ttk.Button(btnf, text="- Удалить", command=self._del_user_from_is).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
self._set_is_edit_state(False)
|
||||
# Дата актуализации списка угроз ФСТЭК (если уже скачивался)
|
||||
self._update_threats_date_label()
|
||||
|
||||
# ---------- ВКЛАДКА "КОМИССИЯ" ----------
|
||||
def _build_commission_tab(self):
|
||||
@@ -1228,13 +1339,14 @@ class DokoGenApp:
|
||||
('is_hw_sw_listbox', 'hw_sw'),
|
||||
('is_os_sw_listbox', 'os_sw'),
|
||||
('is_app_sw_listbox', 'app_sw'),
|
||||
('is_attacker_category_entry', 'attacker_category'),
|
||||
('is_threats_entry', 'threats'),
|
||||
('is_incidents_entry', 'incidents'),
|
||||
('is_criteria_values_entry', 'criteria_values'),
|
||||
('is_criteria_justification_entry', 'criteria_justification'),
|
||||
('is_org_measures_entry', 'org_measures'),
|
||||
('is_tech_measures_entry', 'tech_measures'),
|
||||
('is_attacker_category_combo', 'attacker_category'),
|
||||
('is_attacker_characteristics_listbox', 'attacker_characteristics'),
|
||||
('is_threats_listbox', 'threats'),
|
||||
('is_incidents_listbox', 'incidents'),
|
||||
('is_criteria_values_listbox', 'criteria_values'),
|
||||
('is_criteria_justification_listbox', 'criteria_justification'),
|
||||
('is_org_measures_listbox', 'org_measures'),
|
||||
('is_tech_measures_listbox', 'tech_measures'),
|
||||
]:
|
||||
w = getattr(self, attr, None)
|
||||
if w is not None:
|
||||
@@ -1462,13 +1574,14 @@ class DokoGenApp:
|
||||
('is_hw_sw_listbox', 'hw_sw'),
|
||||
('is_os_sw_listbox', 'os_sw'),
|
||||
('is_app_sw_listbox', 'app_sw'),
|
||||
('is_attacker_category_entry', 'attacker_category'),
|
||||
('is_threats_entry', 'threats'),
|
||||
('is_incidents_entry', 'incidents'),
|
||||
('is_criteria_values_entry', 'criteria_values'),
|
||||
('is_criteria_justification_entry', 'criteria_justification'),
|
||||
('is_org_measures_entry', 'org_measures'),
|
||||
('is_tech_measures_entry', 'tech_measures'),
|
||||
('is_attacker_category_combo', 'attacker_category'),
|
||||
('is_attacker_characteristics_listbox', 'attacker_characteristics'),
|
||||
('is_threats_listbox', 'threats'),
|
||||
('is_incidents_listbox', 'incidents'),
|
||||
('is_criteria_values_listbox', 'criteria_values'),
|
||||
('is_criteria_justification_listbox', 'criteria_justification'),
|
||||
('is_org_measures_listbox', 'org_measures'),
|
||||
('is_tech_measures_listbox', 'tech_measures'),
|
||||
]:
|
||||
w = getattr(self, attr, None)
|
||||
if w is not None:
|
||||
@@ -1538,6 +1651,7 @@ class DokoGenApp:
|
||||
os_sw=src.os_sw,
|
||||
app_sw=src.app_sw,
|
||||
attacker_category=src.attacker_category,
|
||||
attacker_characteristics=src.attacker_characteristics,
|
||||
threats=src.threats,
|
||||
incidents=src.incidents,
|
||||
criteria_values=src.criteria_values,
|
||||
@@ -1645,6 +1759,124 @@ class DokoGenApp:
|
||||
if user:
|
||||
self.users_listbox.insert(tk.END, user)
|
||||
|
||||
def _download_fstec_threats(self):
|
||||
"""Скачать перечень угроз с сайта ФСТЭК (БДУ) и обновить список."""
|
||||
try:
|
||||
from .fstec_threats import download_fstec_threats, get_threats_date
|
||||
except ImportError as e:
|
||||
messagebox.showerror("Ошибка", f"Модуль fstec_threats не найден: {e}")
|
||||
return
|
||||
|
||||
self.log("⬇️ Загрузка перечня угроз с bdu.fstec.ru…")
|
||||
ok, msg, threats = download_fstec_threats()
|
||||
if not ok:
|
||||
messagebox.showerror("Загрузка угроз",
|
||||
f"Не удалось загрузить:\n{msg}\n\n"
|
||||
"Проверьте доступ к интернету или попробуйте позже.")
|
||||
self.log(f"❌ Загрузка угроз: {msg}")
|
||||
return
|
||||
|
||||
# Обновляем label даты актуализации
|
||||
self._update_threats_date_label()
|
||||
# Если список угроз уже открыт — можно показать диалог выбора
|
||||
messagebox.showinfo("Загрузка угроз",
|
||||
f"{msg}\n\nСписок сохранён. Теперь нажмите «📚 Из словаря» "
|
||||
"в разделе «Основные угрозы БИ», чтобы выбрать нужные угрозы.")
|
||||
self.log(f"✅ {msg}")
|
||||
|
||||
def _update_threats_date_label(self):
|
||||
"""Обновить подпись даты актуализации списка угроз."""
|
||||
try:
|
||||
from .fstec_threats import get_threats_date
|
||||
d = get_threats_date()
|
||||
except Exception:
|
||||
d = None
|
||||
if hasattr(self, '_threats_date_label'):
|
||||
if d:
|
||||
self._threats_date_label.config(
|
||||
text=f"Актуально на: {d}", foreground='#2e7d32')
|
||||
else:
|
||||
self._threats_date_label.config(
|
||||
text="⚠ Список угроз ещё не скачан — нажмите «⬇️ Загрузить угрозы ФСТЭК»",
|
||||
foreground='#c62828')
|
||||
|
||||
def _check_security_ai(self):
|
||||
"""Проверка вкладки «Безопасность» через ИИ (DeepSeek/OpenAI-совместимый API)."""
|
||||
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]
|
||||
security_data = {
|
||||
'name': isys.name or '',
|
||||
'description': isys.description or '',
|
||||
'category': isys.category or '',
|
||||
'sphere': isys.sphere or '',
|
||||
'object_type': isys.object_type or '',
|
||||
'architecture': isys.architecture or '',
|
||||
'attacker_category': isys.attacker_category or '',
|
||||
'attacker_characteristics': isys.attacker_characteristics or '',
|
||||
'threats': isys.threats or '',
|
||||
'incidents': isys.incidents or '',
|
||||
'criteria_values': isys.criteria_values or '',
|
||||
'criteria_justification': isys.criteria_justification or '',
|
||||
'org_measures': isys.org_measures or '',
|
||||
'tech_measures': isys.tech_measures or '',
|
||||
}
|
||||
self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): проверка безопасности")
|
||||
self._verify_log("Проверка безопасности через ИИ…")
|
||||
|
||||
def _run():
|
||||
try:
|
||||
from .verify import check_security_ai
|
||||
result, err = check_security_ai(
|
||||
security_data,
|
||||
settings['ai_api_key'],
|
||||
settings.get('ai_model'),
|
||||
settings.get('ai_base_url'),
|
||||
)
|
||||
self.root.after(0, lambda: self._security_ai_done(result, err))
|
||||
except Exception as e:
|
||||
self.root.after(0, lambda: self._security_ai_done(None, str(e)))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _security_ai_done(self, result, err):
|
||||
"""Обработка ответа ИИ по безопасности."""
|
||||
if err:
|
||||
self._verify_log(f"❌ {err}")
|
||||
self.log(f"❌ Проверка безопасности: {err}")
|
||||
return
|
||||
d = result['data']
|
||||
lines = [f"Источник: {result['source']}"]
|
||||
lines.append(f"Оценка: {d.get('rating', 'warning')}")
|
||||
notes = d.get('notes') or []
|
||||
if notes:
|
||||
lines.append("\nЗамечания:")
|
||||
for n in notes:
|
||||
lines.append(f" • {n}")
|
||||
props = d.get('proposals') or {}
|
||||
if any(props.values()):
|
||||
lines.append("\nПредложения ИИ:")
|
||||
for key, label in [('attacker_category', 'Нарушитель'),
|
||||
('threats', 'Угрозы'),
|
||||
('incidents', 'Инциденты'),
|
||||
('criteria_values', 'Показатели'),
|
||||
('criteria_justification', 'Обоснование'),
|
||||
('org_measures', 'Орг. меры'),
|
||||
('tech_measures', 'Тех. меры')]:
|
||||
v = props.get(key)
|
||||
if v:
|
||||
lines.append(f" {label}: {v}")
|
||||
self._verify_log('\n'.join(lines))
|
||||
self.log(f"🤖 ИИ: оценка {d.get('rating', 'warning')}, замечаний: {len(notes)}")
|
||||
|
||||
def _fill_subject_from_company(self):
|
||||
"""Заполнить поля субъекта данными организации (если объект в той же организации)."""
|
||||
if self._current_is_idx is None:
|
||||
@@ -2204,6 +2436,7 @@ class DokoGenApp:
|
||||
os_sw=is_data.get('os_sw', ''),
|
||||
app_sw=is_data.get('app_sw', ''),
|
||||
attacker_category=is_data.get('attacker_category', ''),
|
||||
attacker_characteristics=is_data.get('attacker_characteristics', ''),
|
||||
threats=is_data.get('threats', ''),
|
||||
incidents=is_data.get('incidents', ''),
|
||||
criteria_values=is_data.get('criteria_values', ''),
|
||||
|
||||
Reference in New Issue
Block a user