From dadec026653024f535068797f5e90b6d9ebe990b Mon Sep 17 00:00:00 2001 From: prog1764 Date: Tue, 4 Aug 2026 22:43:04 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B4=D0=B1=D0=BE=D1=80=20?= =?UTF-8?q?=D1=85=D0=B0=D1=80=D0=B0=D0=BA=D1=82=D0=B5=D1=80=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=B0?= =?UTF-8?q?=20=D0=9A=D0=98=D0=98=20=E2=80=94=20=D0=BF=D1=80=D0=BE=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=BC=D0=B0=20=D0=B8=20=D0=98=D0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вкладка «Основное», кнопки под полем типового объекта: - ⚙️ «Подобрать типовой объект (программа)»: скоринг по совпадению значимых слов названия/назначения объекта с перечнем 392 типовых отраслевых объектов (Распоряжение 360-р), IDF-веса (редкие слова важнее общих) → окно с вариантами, оператор выбирает и применяет (заполняет сферу + типовой объект) - 🤖 «Подобрать характеристики (ИИ)»: DeepSeek предлагает назначение, сферу, тип, архитектуру и типовой объект (из перечня 360-р) как рекомендацию → окно с полями, оператор подтверждает/правит Примеры: СКУД → «ИС, АСУ, ИТКС, обеспечивающие контроль и управление доступом на территорию»; 1С:УПП → «ИС для ведения бухгалтерского учета» --- dokogen/threat_rules.py | 132 ++++++++++++++++++++++++++ dokogen/ui.py | 200 ++++++++++++++++++++++++++++++++++++++++ dokogen/verify.py | 57 ++++++++++++ 3 files changed, 389 insertions(+) diff --git a/dokogen/threat_rules.py b/dokogen/threat_rules.py index 0408f0b..4cc7973 100644 --- a/dokogen/threat_rules.py +++ b/dokogen/threat_rules.py @@ -342,3 +342,135 @@ def auto_fill_security(isys, threats=None): res['tech_measures'] = '\n'.join(tech) return res + + +# --------------------------------------------------------------- +# Подбор типового отраслевого объекта КИИ по характеристикам объекта +# --------------------------------------------------------------- + +# Стоп-слова: не несут смысла при сопоставлении названия объекта и типового объекта +STOPWORDS = { + 'информационные', 'информационно', 'телекоммуникационные', 'автоматизированные', + 'системы', 'система', 'сети', 'сеть', 'обеспечивающие', 'обеспечивающих', + 'предназначенные', 'предназначенных', 'управление', 'контроль', 'мониторинг', + 'объекта', 'объектов', 'объект', 'объекты', 'кии', 'рф', 'российской', + 'федерации', 'исполнительной', 'власти', 'органов', 'организаций', 'организации', + 'подведомственных', 'функций', 'функции', 'осуществление', 'полномочий', + 'обеспечение', 'процессов', 'процесса', 'процессы', 'данных', 'информации', + 'сбор', 'хранение', 'обработка', 'представление', 'использование', 'передача', + 'автоматизации', 'средств', 'программных', 'программно', 'аппаратных', + 'технических', 'деятельность', 'деятельности', 'осуществляющих', + 'размещенные', 'размещенных', 'расположенных', 'используемых', 'используемые', + 'проведении', 'проведения', 'выполнении', 'выполнения', 'реализация', 'реализации', + 'электронных', 'электронного', 'информационных', 'информационного', + # Общие словоформы, встречающиеся почти у всех типовых объектов: + 'контроль', 'контроля', 'контролю', 'контролем', 'контроле', + 'управление', 'управления', 'управлению', 'управлением', 'управлении', + 'безопасность', 'безопасности', 'безопасностью', 'безопасном', + 'обеспечение', 'обеспечения', 'обеспечению', 'обеспечением', + 'предназначен', 'предназначена', 'предназначено', 'предназначены', + 'предоставление', 'предоставления', 'предоставлению', + 'взаимодействие', 'взаимодействия', 'взаимодействию', + 'функционирование', 'функционирования', 'функционированию', + 'деятельность', 'деятельности', 'деятельностью', + 'автоматизирован', 'автоматизированной', 'автоматизированных', + 'телекоммуникацион', 'телекоммуникационной', 'телекоммуникационных', + 'информационн', 'информационной', 'информационных', 'информационное', + 'осуществление', 'осуществления', 'осуществлению', + 'для', 'систем', 'системы', 'система', 'ресурсов', 'ресурсы', + 'целью', 'цели', 'помощи', 'помощь', 'также', 'или', 'иных', 'иное', + 'при', 'на', 'по', 'в', 'с', 'и', 'не', 'то', 'числе', +} + + +def _tokenize(text): + """Значимые слова текста (без стоп-слов).""" + import re as _re + words = _re.findall(r'[а-яёa-z0-9]+', (text or '').lower()) + return [w for w in words if len(w) > 2 and w not in STOPWORDS] + + +def _load_all_typical(dict_dir=None): + """Все типовые отраслевые объекты из typical_industry_objects.json (по сферам).""" + import os as _os + if dict_dir is None: + dict_dir = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'dictionaries') + path = _os.path.join(dict_dir, 'typical_industry_objects.json') + try: + with open(path, encoding='utf-8') as f: + data = json.load(f) + except Exception: + return [] + out = [] + for sphere, items in data.items(): + if sphere == 'Иная сфера деятельности': + continue + for it in items: + out.append({'sphere': sphere, 'num': it.get('num', ''), + 'name': it.get('name', '')}) + return out + + +_IDF_CACHE = None + + +def _idf_weights(cands): + """IDF-веса слов: чем реже слово в перечне типовых объектов, тем больше вес. + + Общие слова («информационные», «управление», «контроль») встречаются почти + у всех 392 объектов и почти ничего не значат; редкие («доступом», «СКУД», + «сигнализации») — сильно различают объекты. + """ + global _IDF_CACHE + if _IDF_CACHE is not None: + return _IDF_CACHE + import math + df = {} + for c in cands: + for w in set(_tokenize(c['name'])): + df[w] = df.get(w, 0) + 1 + n = max(len(cands), 1) + weights = {w: math.log((n + 1) / (df.get(w, 0) + 1)) + 1.0 for w in df} + _IDF_CACHE = weights + return weights + + +def suggest_typical_object_program(isys, limit=10, sphere=None): + """Подбор типового отраслевого объекта КИИ по характеристикам объекта. + + Логика: значимые слова из названия/назначения/описания объекта КИИ + сопоставляются со значимыми словами названий типовых объектов + (перечень Распоряжения ПРФ № 360-р, 392 объекта). Скоринг по IDF: + редкие слова (например, «доступом», «сигнализации») весят больше общих. + Пример: объект «СКУД» / «контроль и управление доступом» → + «ИС, АСУ, ИТКС, обеспечивающие контроль и управление доступом на территорию». + + Возвращает список dict: {sphere, num, name, score} — по убыванию релевантности. + """ + text = ' '.join([ + getattr(isys, 'name', '') or '', + getattr(isys, 'purpose', '') or '', + getattr(isys, 'description', '') or '', + ]) + obj_words = set(_tokenize(text)) + if not obj_words: + return [] + + cands = _load_all_typical() + if sphere: + cands = [c for c in cands if c['sphere'] == sphere] + + weights = _idf_weights(cands) + + scored = [] + for c in cands: + tw = set(_tokenize(c['name'])) + common = tw & obj_words + if common: + score = sum(weights.get(w, 1.0) for w in common) + scored.append({'sphere': c['sphere'], 'num': c['num'], + 'name': c['name'], 'score': round(score, 2), + 'matched': sorted(common)}) + + scored.sort(key=lambda x: (-x['score'], x['name'])) + return scored[:limit] diff --git a/dokogen/ui.py b/dokogen/ui.py index 5740acf..2e4b38f 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -868,6 +868,16 @@ class DokoGenApp: self._typical_objects_all = [''] self.is_typical_object_combo['values'] = self._typical_objects_all + # Кнопки подбора характеристик: программа (типовой объект) и ИИ (всё) + suggest_row = ttk.Frame(main_frame) + suggest_row.grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=(4, 0)) + ttk.Button(suggest_row, text="⚙️ Подобрать типовой объект (программа)", + command=self._suggest_typical_program_click).pack(side=tk.LEFT, padx=2) + ttk.Button(suggest_row, text="🤖 Подобрать характеристики (ИИ)", + command=self._suggest_object_params_ai_click).pack(side=tk.LEFT, padx=2) + ttk.Label(suggest_row, text="Подбор по названию/назначению объекта — как рекомендация, оператор подтверждает.", + foreground='gray').pack(side=tk.LEFT, padx=8) + main_frame.columnconfigure(1, weight=1) # ===== Вкладка «Субъект» ===== @@ -1554,6 +1564,196 @@ class DokoGenApp: def _set_is_edit_state(self, enabled): state = 'normal' if enabled else 'disabled' + def _suggest_typical_program_click(self): + """⚙️ Подобрать типовой отраслевой объект программой (по названию/назначению объекта).""" + if self._current_is_idx is None: + messagebox.showinfo("Подбор", "Сначала выберите объект КИИ в списке слева") + return + self._collect_is() + isys = self.model.information_systems[self._current_is_idx] + try: + from .threat_rules import suggest_typical_object_program + sphere = self.is_sphere_combo.get() or None + results = suggest_typical_object_program(isys, limit=12, sphere=sphere) + except Exception as e: + messagebox.showerror("Ошибка", str(e)) + return + if not results: + messagebox.showinfo("Подбор", + "Не удалось подобрать типовой объект по названию/назначению.\n" + "Попробуйте кнопку «🤖 Подобрать характеристики (ИИ)».") + return + + dlg = tk.Toplevel(self.root) + dlg.title("⚙️ Подбор типового отраслевого объекта") + dlg.geometry("860x520") + dlg.transient(self.root) + frm = ttk.Frame(dlg, padding=8) + frm.pack(fill=tk.BOTH, expand=True) + + ttk.Label(frm, text=f"Объект: {isys.name}", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(frm, text="Наиболее подходящие типовые отраслевые объекты (по совпадению слов " + "с названием/назначением). Выберите и нажмите «Применить».", + foreground='gray', wraplength=820, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) + + lb = tk.Listbox(frm, height=16, exportselection=False) + lb.pack(fill=tk.BOTH, expand=True) + for r in results: + lb.insert(tk.END, f"[{r['score']}] ({r['sphere']}) {r['name']}") + + btn_row = ttk.Frame(frm) + btn_row.pack(fill=tk.X, pady=(8, 0)) + ttk.Button(btn_row, text="✔️ Применить выбранное", + command=lambda: self._apply_typical_selection(lb, results, dlg)).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_row, text="Отмена", command=dlg.destroy).pack(side=tk.LEFT, padx=2) + + def _apply_typical_selection(self, lb, results, dlg): + sel = lb.curselection() + if not sel: + messagebox.showinfo("Подбор", "Выберите типовой объект из списка") + return + r = results[sel[0]] + # Устанавливаем сферу и типовой объект + if hasattr(self, 'is_sphere_combo'): + self.is_sphere_combo.set(r['sphere']) + self._update_typical_objects_by_sphere() + if hasattr(self, 'is_typical_object_combo'): + self.is_typical_object_combo.set(r['name']) + self._collect_is() + self.log(f"⚙️ Типовой отраслевой объект: {r['name'][:70]}") + dlg.destroy() + + def _suggest_object_params_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 + self._collect_is() + isys = self.model.information_systems[self._current_is_idx] + # Перечень типовых объектов для выбора ИИ (всех сфер) + typical_list = [] + if hasattr(self, '_typical_industry'): + for sph, items in self._typical_industry.items(): + if sph == 'Иная сфера деятельности': + continue + for it in items: + typical_list.append(it.get('name', '')) + data = { + 'name': isys.name or '', + 'description': isys.description or '', + 'purpose': isys.purpose or '', + 'sphere': isys.sphere or '', + 'object_type': isys.object_type or '', + } + self.log(f"🤖 Запрос к ИИ: подбор характеристик для «{isys.name}»") + self._verify_log("Подбор характеристик через ИИ…") + + def _run(): + try: + from .verify import suggest_object_params_ai + res, err = suggest_object_params_ai( + data, settings['ai_api_key'], settings.get('ai_model'), + settings.get('ai_base_url'), typical_objects=typical_list) + self.root.after(0, lambda: self._suggest_object_params_ai_done(res, err, isys.name)) + except Exception as e: + self.root.after(0, lambda: self._suggest_object_params_ai_done(None, str(e), isys.name)) + + threading.Thread(target=_run, daemon=True).start() + + def _suggest_object_params_ai_done(self, res, err, obj_name): + if err: + self._verify_log(f"❌ {err}") + self.log(f"❌ Подбор характеристик ИИ: {err}") + return + if not res: + self._verify_log("❌ ИИ не вернул характеристики") + return + + dlg = tk.Toplevel(self.root) + dlg.title("🤖 Рекомендации ИИ по характеристикам объекта") + dlg.geometry("860x560") + dlg.transient(self.root) + frm = ttk.Frame(dlg, padding=8) + frm.pack(fill=tk.BOTH, expand=True) + + ttk.Label(frm, text=f"Объект: {obj_name} — рекомендации ИИ (подтвердите или скорректируйте)", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + + rows = [ + ('назначение', 'Назначение объекта'), + ('сфера', 'Сфера деятельности'), + ('тип', 'Тип объекта'), + ('архитектура', 'Архитектура объекта'), + ('типовой_объект', 'Типовой отраслевой объект КИИ'), + ] + entries = {} + for key, label in rows: + ttk.Label(frm, text=label + ":").pack(anchor=tk.W, pady=(6, 0)) + e = tk.Text(frm, height=2 if key == 'типовой_объект' else 1, wrap=tk.WORD) + e.pack(fill=tk.X) + e.insert('1.0', res.get(key, '') or '') + entries[key] = e + + ttk.Label(frm, text="Обоснование ИИ:").pack(anchor=tk.W, pady=(6, 0)) + just = tk.Text(frm, height=2, wrap=tk.WORD) + just.pack(fill=tk.X) + just.insert('1.0', res.get('обоснование', '') or '') + + btn_row = ttk.Frame(frm) + btn_row.pack(fill=tk.X, pady=(8, 0)) + ttk.Button(btn_row, text="✔️ Применить", + command=lambda: self._apply_ai_params(entries, dlg)).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_row, text="Отмена", command=dlg.destroy).pack(side=tk.LEFT, padx=2) + + def _apply_ai_params(self, entries, dlg): + """Применить рекомендации ИИ к полям объекта.""" + def _get(e): + return e.get('1.0', 'end-1c').strip() + + purpose = _get(entries['назначение']) + sphere = _get(entries['сфера']) + otype = _get(entries['тип']) + arch = _get(entries['архитектура']) + typical = _get(entries['типовой_объект']) + + if purpose and hasattr(self, 'is_purpose_entry'): + self.is_purpose_entry.delete(0, tk.END) + self.is_purpose_entry.insert(0, purpose) + + # Сфера — проверим, есть ли в списке + if sphere and hasattr(self, 'is_sphere_combo'): + values = list(self.is_sphere_combo['values']) + exact = next((v for v in values if v.lower() == sphere.lower()), None) + if exact: + self.is_sphere_combo.set(exact) + self._update_typical_objects_by_sphere() + else: + self.is_sphere_combo.set(sphere) + + if otype and hasattr(self, 'is_object_type_combo'): + values = list(self.is_object_type_combo['values']) + exact = next((v for v in values if v.lower() == otype.lower()), None) + self.is_object_type_combo.set(exact or otype) + + if arch and hasattr(self, 'is_architecture_combo'): + values = list(self.is_architecture_combo['values']) + exact = next((v for v in values if v.lower() == arch.lower()), None) + self.is_architecture_combo.set(exact or arch) + + if typical and hasattr(self, 'is_typical_object_combo'): + values = list(self.is_typical_object_combo['values']) + exact = next((v for v in values if v.lower() == typical.lower()), None) + self.is_typical_object_combo.set(exact or typical) + + self._collect_is() + self.log("🤖 Рекомендации ИИ применены к характеристикам объекта") + dlg.destroy() + def _on_sphere_changed(self, event=None): """При смене сферы объекта — подставляем типовые отраслевые объекты этой сферы (Распоряжение ПРФ № 360-р).""" self._update_typical_objects_by_sphere() diff --git a/dokogen/verify.py b/dokogen/verify.py index caed246..0558c44 100644 --- a/dokogen/verify.py +++ b/dokogen/verify.py @@ -519,6 +519,63 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N return threats_out, None +def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None, + typical_objects=None): + """ИИ предлагает характеристики объекта КИИ: назначение, сферу, тип, архитектуру, + типовой отраслевой объект — как рекомендацию оператору. + + object_data — dict: name, description, purpose (что известно об объекте). + typical_objects — перечень типовых отраслевых объектов (из 360-р) для выбора. + + Возвращает (dict, ошибка): dict с полями + purpose / sphere / object_type / architecture / typical_object / обоснование. + """ + if not api_key: + return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" + + if typical_objects: + to_text = "\n".join(f"{i+1}. {t}" for i, t in enumerate(typical_objects[:400])) + else: + to_text = "(перечень не предоставлен)" + + system = ( + "Ты — эксперт ФСТЭК по категорированию объектов критической информационной инфраструктуры " + "(187-ФЗ, ПП РФ № 127, Распоряжение ПРФ № 360-р, приказ ФСТЭК № 236). " + "По описанию объекта КИИ предложи его характеристики. Верни ТОЛЬКО JSON: " + '{"назначение": "короткое назначение объекта", ' + '"сфера": "одна из сфер: Здравоохранение, Наука, Транспорт, Связь, Энергетика, ' + 'Государственная регистрация прав, Банковская сфера и финансовый рынок, ' + 'Топливно-энергетический комплекс, Атомная энергетика, Оборонная промышленность, ' + 'Ракетно-космическая промышленность, Горнодобывающая промышленность, ' + 'Металлургическая промышленность, Химическая промышленность, Иная сфера деятельности", ' + '"тип": "Информационная система | Автоматизированные системы управления | ' + 'Информационно-телекоммуникационная сеть", ' + '"архитектура": "одно из: клиент-серверная система, автономная (standalone), ' + 'распределенная система, иное", ' + '"типовой_объект": "полная строка из предоставленного перечня, наиболее подходящая, ' + 'или пустая строка", ' + '"обоснование": "почему выбраны такие характеристики (1-2 предложения)"} ' + "Не выдумывай типовой объект — бери только из перечня." + ) + user = ( + "Информация об объекте КИИ:\n" + f"{json.dumps(object_data, ensure_ascii=False)}\n\n" + "Перечень типовых отраслевых объектов КИИ (Распоряжение ПРФ № 360-р):\n" + f"{to_text}" + ) + js, err = _ai_json(api_key, model, base_url, system, user) + if err: + return None, err + return { + 'назначение': str(js.get('назначение', '') or '').strip(), + 'сфера': str(js.get('сфера', '') or '').strip(), + 'тип': str(js.get('тип', '') or '').strip(), + 'архитектура': str(js.get('архитектура', '') or '').strip(), + 'типовой_объект': str(js.get('типовой_объект', '') or '').strip(), + 'обоснование': str(js.get('обоснование', '') or '').strip(), + }, None + + def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=None): """ИИ-проверка заполненной вкладки «Безопасность» объекта КИИ (236 приказ).