diff --git a/dokogen/dictionaries/attacker_categories.json b/dokogen/dictionaries/attacker_categories.json
new file mode 100644
index 0000000..08b757b
--- /dev/null
+++ b/dokogen/dictionaries/attacker_categories.json
@@ -0,0 +1,5 @@
+[
+ "внешний нарушитель",
+ "внутренний нарушитель",
+ "внешний и внутренний нарушитель"
+]
diff --git a/dokogen/dictionaries/criteria_indicators.json b/dokogen/dictionaries/criteria_indicators.json
new file mode 100644
index 0000000..1487a52
--- /dev/null
+++ b/dokogen/dictionaries/criteria_indicators.json
@@ -0,0 +1,7 @@
+[
+ "Социальная значимость — количество граждан, на которые может повлиять нарушение",
+ "Политическая значимость — международный или внутриполитический резонанс",
+ "Экономическая значимость — возможный размер ущерба (в рублях)",
+ "Экологическая значимость — возможный ущерб окружающей среде",
+ "Значимость для обороны страны, безопасности государства и правопорядка"
+]
diff --git a/dokogen/dictionaries/incidents.json b/dokogen/dictionaries/incidents.json
new file mode 100644
index 0000000..9096d78
--- /dev/null
+++ b/dokogen/dictionaries/incidents.json
@@ -0,0 +1,8 @@
+[
+ "отказ в обслуживании",
+ "несанкционированный доступ",
+ "утечка данных (нарушение конфиденциальности)",
+ "модификация (подмена) данных",
+ "нарушение функционирования технических средств",
+ "несанкционированное использование вычислительных ресурсов объекта"
+]
diff --git a/dokogen/dictionaries/org_measures.json b/dokogen/dictionaries/org_measures.json
new file mode 100644
index 0000000..a5a5b2e
--- /dev/null
+++ b/dokogen/dictionaries/org_measures.json
@@ -0,0 +1,9 @@
+[
+ "Установление контролируемой зоны",
+ "Контроль физического доступа к объекту",
+ "Разработка документов (регламентов, инструкций, руководств) по обеспечению безопасности объекта",
+ "Назначение ответственных за обеспечение безопасности объекта",
+ "Обучение и инструктаж персонала",
+ "Регламентация действий при компьютерных инцидентах",
+ "Утверждение перечня лиц, имеющих доступ к объекту"
+]
diff --git a/dokogen/dictionaries/tech_measures.json b/dokogen/dictionaries/tech_measures.json
new file mode 100644
index 0000000..55805aa
--- /dev/null
+++ b/dokogen/dictionaries/tech_measures.json
@@ -0,0 +1,14 @@
+[
+ "Идентификация и аутентификация пользователей",
+ "Управление доступом",
+ "Ограничение программной среды",
+ "Антивирусная защита",
+ "Защита информации от несанкционированного доступа (СЗИ от НСД)",
+ "Регистрация и мониторинг событий безопасности",
+ "Межсетевое экранирование",
+ "Обнаружение вторжений (СОВ)",
+ "Криптографическая защита информации (СКЗИ)",
+ "Защита среды виртуализации",
+ "Резервное копирование и восстановление",
+ "Защита технических средств и линий связи"
+]
diff --git a/dokogen/dictionaries/threats.json b/dokogen/dictionaries/threats.json
new file mode 100644
index 0000000..db774ce
--- /dev/null
+++ b/dokogen/dictionaries/threats.json
@@ -0,0 +1,12 @@
+[
+ "Угроза несанкционированного доступа к информационным ресурсам объекта",
+ "Угроза нарушения функционирования объекта (отказ в обслуживании)",
+ "Угроза утечки (нарушения конфиденциальности) информации",
+ "Угроза модификации (подмены) данных",
+ "Угроза внедрения вредоносного кода",
+ "Угроза несанкционированного использования вычислительных ресурсов",
+ "Угроза перехвата (прослушивания) сетевого трафика",
+ "Угроза социальной инженерии",
+ "Угроза использования уязвимостей программного обеспечения",
+ "Угроза физического воздействия на технические средства"
+]
diff --git a/dokogen/dictionaries/threats_fstec.json b/dokogen/dictionaries/threats_fstec.json
new file mode 100644
index 0000000..f1335e3
--- /dev/null
+++ b/dokogen/dictionaries/threats_fstec.json
@@ -0,0 +1,229 @@
+[
+ "Угроза автоматического распространения вредоносного кода в грид-системе",
+ "Угроза агрегирования данных, передаваемых в грид-системе",
+ "Угроза использования слабостей криптографических алгоритмов и уязвимостей в программном обеспечении их реализации",
+ "Угроза аппаратного сброса пароля BIOS",
+ "Угроза внедрения вредоносного кода в BIOS",
+ "Угроза внедрения кода или данных",
+ "Угроза воздействия на программы с высокими привилегиями",
+ "Угроза восстановления и/или повторного использования аутентификационной информации",
+ "Угроза восстановления предыдущей уязвимой версии BIOS",
+ "Угроза выхода процесса за пределы виртуальной машины",
+ "Угроза деавторизации санкционированного клиента беспроводной сети",
+ "Угроза деструктивного изменения конфигурации/среды окружения программ",
+ "Угроза деструктивного использования декларированного функционала BIOS",
+ "Угроза длительного удержания вычислительных ресурсов пользователями",
+ "Угроза доступа к защищаемым файлам с использованием обходного пути",
+ "Угроза доступа к локальным файлам сервера при помощи URL",
+ "Угроза доступа/перехвата/изменения HTTP cookies",
+ "Угроза загрузки нештатной операционной системы",
+ "Угроза заражения DNS-кеша",
+ "Угроза злоупотребления возможностями, предоставленными потребителям облачных услуг",
+ "Угроза злоупотребления доверием потребителей облачных услуг",
+ "Угроза избыточного выделения оперативной памяти",
+ "Угроза изменения компонентов информационной (автоматизированной) системы",
+ "Угроза изменения режимов работы аппаратных элементов компьютера",
+ "Угроза изменения системных и глобальных переменных",
+ "Угроза искажения XML-схемы",
+ "Угроза искажения вводимой и выводимой на периферийные устройства информации",
+ "Угроза использования альтернативных путей доступа к ресурсам",
+ "Угроза использования вычислительных ресурсов суперкомпьютера «паразитными» процессами",
+ "Угроза использования информации идентификации/аутентификации, заданной по умолчанию",
+ "Угроза использования механизмов авторизации для повышения привилегий",
+ "Угроза использования поддельных цифровых подписей BIOS",
+ "Угроза использования слабостей кодирования входных данных",
+ "Угроза использования слабостей протоколов сетевого/локального обмена данными",
+ "Угроза использования слабых криптографических алгоритмов BIOS",
+ "Угроза исследования механизмов работы программы",
+ "Угроза исследования приложения через отчёты об ошибках",
+ "Угроза исчерпания вычислительных ресурсов хранилища больших данных",
+ "Угроза исчерпания запаса ключей, необходимых для обновления BIOS",
+ "Угроза конфликта юрисдикций различных стран",
+ "Угроза межсайтового скриптинга",
+ "Угроза межсайтовой подделки запроса",
+ "Угроза нарушения доступности облачного сервера",
+ "Угроза нарушения изоляции пользовательских данных внутри виртуальной машины",
+ "Угроза нарушения изоляции среды исполнения BIOS",
+ "Угроза нарушения процедуры аутентификации субъектов виртуального информационного взаимодействия",
+ "Угроза нарушения работоспособности грид-системы при нетипичной сетевой нагрузке",
+ "Угроза нарушения технологии обработки информации путём несанкционированного внесения изменений в образы виртуальных машин",
+ "Угроза нарушения целостности данных кеша",
+ "Угроза неверного определения формата входных данных, поступающих в хранилище больших данных",
+ "Угроза невозможности восстановления сессии работы на ПЭВМ при выводе из промежуточных состояний питания",
+ "Угроза невозможности миграции образов виртуальных машин из-за несовместимости аппаратного и программного обеспечения",
+ "Угроза невозможности управления правами пользователей BIOS",
+ "Угроза недобросовестного исполнения обязательств поставщиками облачных услуг",
+ "Угроза незащищённого администрирования облачных услуг",
+ "Угроза некачественного переноса инфраструктуры в облако",
+ "Угроза неконтролируемого копирования данных внутри хранилища больших данных",
+ "Угроза неконтролируемого роста числа виртуальных машин",
+ "Угроза неконтролируемого роста числа зарезервированных вычислительных ресурсов",
+ "Угроза неконтролируемого уничтожения информации хранилищем больших данных",
+ "Угроза некорректного задания структуры данных транзакции",
+ "Угроза некорректного использования прозрачного прокси-сервера за счёт плагинов браузера",
+ "Угроза некорректного использования функционала программного и аппаратного обеспечения",
+ "Угроза некорректной реализации политики лицензирования в облаке",
+ "Угроза неопределённости в распределении ответственности между ролями в облаке",
+ "Угроза неопределённости ответственности за обеспечение безопасности облака",
+ "Угроза неправомерного ознакомления с защищаемой информацией",
+ "Угроза неправомерного/некорректного использования интерфейса взаимодействия с приложением",
+ "Угроза неправомерных действий в каналах связи",
+ "Угроза непрерывной модернизации облачной инфраструктуры",
+ "Угроза несанкционированного восстановления удалённой защищаемой информации",
+ "Угроза несанкционированного выключения или обхода механизма защиты от записи в BIOS",
+ "Угроза несанкционированного доступа к активному и (или) пассивному виртуальному и (или) физическому сетевому оборудованию из физической и (или) виртуальной сети",
+ "Угроза несанкционированного доступа к аутентификационной информации",
+ "Угроза несанкционированного доступа к виртуальным каналам передачи",
+ "Угроза несанкционированного доступа к гипервизору из виртуальной машины и (или) физической сети",
+ "Угроза несанкционированного доступа к данным за пределами зарезервированного адресного пространства, в том числе выделенного под виртуальное аппаратное обеспечение",
+ "Угроза несанкционированного доступа к защищаемым виртуальным машинам из виртуальной и (или) физической сети",
+ "Угроза несанкционированного доступа к защищаемым виртуальным машинам со стороны других виртуальных машин",
+ "Угроза несанкционированного доступа к защищаемым виртуальным устройствам из виртуальной и (или) физической сети",
+ "Угроза несанкционированного доступа к локальному компьютеру через клиента грид-системы",
+ "Угроза несанкционированного доступа к сегментам вычислительного поля",
+ "Угроза несанкционированного доступа к системе по беспроводным каналам",
+ "Угроза несанкционированного доступа к системе хранения данных из виртуальной и (или) физической сети",
+ "Угроза несанкционированного доступа к хранимой в виртуальном пространстве защищаемой информации",
+ "Угроза несанкционированного изменения аутентификационной информации",
+ "Угроза несанкционированного использования привилегированных функций BIOS",
+ "Угроза несанкционированного копирования защищаемой информации",
+ "Угроза несанкционированного редактирования реестра",
+ "Угроза несанкционированного создания учётной записи пользователя",
+ "Угроза несанкционированного удаления защищаемой информации",
+ "Угроза несанкционированного удалённого внеполосного доступа к аппаратным средствам",
+ "Угроза несанкционированного управления буфером",
+ "Угроза несанкционированного управления синхронизацией и состоянием",
+ "Угроза несанкционированного управления указателями",
+ "Угроза несогласованности политик безопасности элементов облачной инфраструктуры",
+ "Угроза несогласованности правил доступа к большим данным",
+ "Угроза обнаружения открытых портов и идентификации привязанных к ним сетевых служб",
+ "Угроза обнаружения хостов",
+ "Угроза обхода некорректно настроенных механизмов аутентификации",
+ "Угроза общедоступности облачной инфраструктуры",
+ "Угроза опосредованного управления группой программ через совместно используемые данные",
+ "Угроза определения типов объектов защиты",
+ "Угроза определения топологии вычислительной сети",
+ "Угроза отказа в загрузке входных данных неизвестного формата хранилищем больших данных",
+ "Угроза отказа в обслуживании системой хранения данных суперкомпьютера",
+ "Угроза отключения контрольных датчиков",
+ "Угроза ошибки обновления гипервизора",
+ "Угроза перебора всех настроек и параметров приложения",
+ "Угроза перегрузки грид-системы вычислительными заданиями",
+ "Угроза передачи данных по скрытым каналам",
+ "Угроза передачи запрещённых команд на оборудование с числовым программным управлением",
+ "Угроза перезагрузки аппаратных и программно-аппаратных средств вычислительной техники",
+ "Угроза переполнения целочисленных переменных",
+ "Угроза перехвата вводимой и выводимой на периферийные устройства информации",
+ "Угроза перехвата данных, передаваемых по вычислительной сети",
+ "Угроза перехвата привилегированного потока",
+ "Угроза перехвата привилегированного процесса",
+ "Угроза перехвата управления гипервизором",
+ "Угроза перехвата управления средой виртуализации",
+ "Угроза повреждения системного реестра",
+ "Угроза повышения привилегий",
+ "Угроза подбора пароля BIOS",
+ "Угроза подделки записей журнала регистрации событий",
+ "Угроза подключения к беспроводной сети в обход процедуры аутентификации",
+ "Угроза подмены беспроводного клиента или точки доступа",
+ "Угроза подмены действия пользователя путём обмана",
+ "Угроза подмены доверенного пользователя",
+ "Угроза подмены резервной копии программного обеспечения BIOS",
+ "Угроза подмены содержимого сетевых ресурсов",
+ "Угроза подмены субъекта сетевого доступа",
+ "Угроза получения предварительной информации об объекте защиты",
+ "Угроза получения сведений о владельце беспроводного устройства",
+ "Угроза потери доверия к поставщику облачных услуг",
+ "Угроза потери и утечки данных, обрабатываемых в облаке",
+ "Угроза потери информации вследствие несогласованности работы узлов хранилища больших данных",
+ "Угроза потери управления облачными ресурсами",
+ "Угроза потери управления собственной инфраструктурой при переносе её в облако",
+ "Угроза преодоления физической защиты",
+ "Угроза приведения системы в состояние «отказ в обслуживании»",
+ "Угроза привязки к поставщику облачных услуг",
+ "Угроза приостановки оказания облачных услуг вследствие технических сбоев",
+ "Угроза программного выведения из строя средств хранения, обработки и (или) ввода/вывода/передачи информации",
+ "Угроза программного сброса пароля BIOS",
+ "Угроза пропуска проверки целостности программного обеспечения",
+ "Угроза прямого обращения к памяти вычислительного поля суперкомпьютера",
+ "Угроза распространения несанкционированно повышенных прав на всю грид-систему",
+ "Угроза сбоя автоматического управления системой разграничения доступа хранилища больших данных",
+ "Угроза сбоя обработки специальным образом изменённых файлов",
+ "Угроза сбоя процесса обновления BIOS",
+ "Угроза сканирования веб-сервисов, разработанных на основе языка описания WSDL",
+ "Угроза удаления аутентификационной информации",
+ "Угроза усиления воздействия на вычислительные ресурсы пользователей при помощи сторонних серверов",
+ "Угроза установки уязвимых версий обновления программного обеспечения BIOS",
+ "Угроза утраты вычислительных ресурсов",
+ "Угроза утраты носителей информации",
+ "Угроза физического выведения из строя средств хранения, обработки и (или) ввода/вывода/передачи информации",
+ "Угроза форматирования носителей информации",
+ "Угроза «форсированного веб-браузинга»",
+ "Угроза хищения средств хранения, обработки и (или) ввода/вывода/передачи информации",
+ "Угроза чрезмерного использования вычислительных ресурсов суперкомпьютера в ходе интенсивного обмена межпроцессорными сообщениями",
+ "Угроза эксплуатации цифровой подписи программного кода",
+ "Угроза перехвата исключения/сигнала из привилегированного блока функций",
+ "Угроза распространения состояния «отказ в обслуживании» в облачной инфраструктуре",
+ "Угроза включения в проект не достоверно испытанных компонентов",
+ "Угроза внедрения системной избыточности",
+ "Угроза заражения компьютера при посещении неблагонадёжных сайтов",
+ "Угроза «кражи» учётной записи доступа к сетевым сервисам",
+ "Угроза наличия механизмов разработчика",
+ "Угроза неправомерного шифрования информации",
+ "Угроза скрытного включения вычислительного устройства в состав бот-сети",
+ "Угроза распространения «почтовых червей»",
+ "Угроза «спама» веб-сервера",
+ "Угроза «фарминга»",
+ "Угроза «фишинга»",
+ "Угроза нарушения технологического/производственного процесса из-за временны́х задержек, вносимых средством защиты",
+ "Угроза неподтверждённого ввода данных оператором в систему, связанную с безопасностью",
+ "Угроза несанкционированного использования системных и сетевых утилит",
+ "Угроза несанкционированной модификации защищаемой информации",
+ "Угроза отказа подсистемы обеспечения температурного режима",
+ "Угроза перехвата одноразовых паролей в режиме реального времени",
+ "Угроза физического устаревания аппаратных компонентов",
+ "Угроза перехвата управления автоматизированной системой управления технологическими процессами",
+ "Угроза агрегирования данных, обрабатываемых с помощью мобильного устройства",
+ "Угроза несанкционированного изменения параметров настройки средств защиты информации",
+ "Угроза внедрения вредоносного кода через рекламу, сервисы и контент",
+ "Угроза несанкционированного воздействия на средство защиты информации",
+ "Угроза подмены программного обеспечения",
+ "Угроза маскирования действий вредоносного кода",
+ "Угроза внедрения вредоносного кода за счет посещения зараженных сайтов в сети Интернет",
+ "Угроза внедрения вредоносного кода в дистрибутив программного обеспечения",
+ "Угроза использования уязвимых версий программного обеспечения",
+ "Угроза утечки информации за счет применения вредоносным программным обеспечением алгоритмов шифрования трафика",
+ "Угроза несанкционированного использования привилегированных функций мобильного устройства",
+ "Угроза удаленного запуска вредоносного кода в обход механизмов защиты операционной системы",
+ "Угроза контроля вредоносной программой списка приложений, запущенных на мобильном устройстве",
+ "Угроза хищения аутентификационной информации из временных файлов cookie",
+ "Угроза скрытной регистрации вредоносной программой учетных записей администраторов",
+ "Угроза перехвата управления мобильного устройства при использовании виртуальных голосовых ассистентов",
+ "Угроза хищения информации с мобильного устройства при использовании виртуальных голосовых ассистентов",
+ "Угроза утечки пользовательских данных при использовании функций автоматического заполнения аутентификационной информации в браузере",
+ "Угроза несанкционированной установки приложений на мобильные устройства",
+ "Угроза утечки информации с неподключенных к сети Интернет компьютеров",
+ "Угроза несанкционированного изменения вредоносной программой значений параметров программируемых логических контроллеров",
+ "Угроза нарушения работы компьютера и блокирования доступа к его данным из-за некорректной работы установленных на нем средств защиты",
+ "Угроза отказа в работе оборудования из-за изменения геолокационной информации о нем",
+ "Угроза несанкционированного доступа к параметрам настройки оборудования за счет использования «мастер-кодов» (инженерных паролей)",
+ "Угроза нецелевого использования вычислительных ресурсов средства вычислительной техники",
+ "Угроза несанкционированного доступа к защищаемой памяти \r\nядра процессора",
+ "Угроза нарушения работы информационной системы, вызванного обновлением используемого в ней программного обеспечения",
+ "Угроза использования непроверенных пользовательских данных при формировании конфигурационного файла, используемого программным обеспечением администрирования информационных систем",
+ "Угроза перехвата управления информационной системой",
+ "Угроза обхода многофакторной аутентификации",
+ "Угроза несвоевременного выявления и реагирования компонентами информационной (автоматизированной) системы (в том числе средствами защиты информации) на события безопасности информации",
+ "Угроза несанкционированного доступа к системе при помощи сторонних сервисов",
+ "Угроза получения несанкционированного доступа к приложениям, установленным на Smart-картах",
+ "Угроза использования скомпрометированного доверенного источника обновлений программного обеспечения",
+ "Угроза раскрытия информации о модели машинного обучения",
+ "Угроза хищения обучающих данных",
+ "Угроза нарушения функционирования («обхода») средств, реализующих технологии искусственного интеллекта",
+ "Угроза модификации модели машинного обучения путем искажения («отравления») обучающих данных",
+ "Угроза подмены модели машинного обучения",
+ "Угроза несанкционированного доступа к контейнерам, предоставляющего пользователям расширенные привилегии",
+ "Угроза нарушения целостности (подмены) контейнеров",
+ "Угроза нарушения изоляции контейнеров",
+ "Угроза внедрения вредоносного программного обеспечения в контейнеры",
+ "Угроза модификации (подмены) образов контейнеров"
+]
\ No newline at end of file
diff --git a/dokogen/dictionaries/threats_fstec_date.txt b/dokogen/dictionaries/threats_fstec_date.txt
new file mode 100644
index 0000000..ed514eb
--- /dev/null
+++ b/dokogen/dictionaries/threats_fstec_date.txt
@@ -0,0 +1 @@
+2026-08-04
\ No newline at end of file
diff --git a/dokogen/fstec_threats.py b/dokogen/fstec_threats.py
new file mode 100644
index 0000000..2b39a70
--- /dev/null
+++ b/dokogen/fstec_threats.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+"""DokoGen — загрузка перечня угроз безопасности информации с сайта ФСТЭК (БДУ).
+
+Скачивает https://bdu.fstec.ru/threat (таблица угроз, с пагинацией),
+парсит названия угроз и сохраняет в dictionaries/threats_fstec.json.
+Рядом хранится дата актуализации (threats_fstec_date.txt).
+"""
+
+import os
+import re
+import json
+import ssl
+import urllib.request
+import urllib.parse
+from datetime import datetime
+
+BDU_URL = 'https://bdu.fstec.ru/threat'
+USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+
+
+def _ssl_ctx():
+ """SSL-контекст: сначала обычная проверка, при проблемах — без проверки."""
+ try:
+ ctx = ssl.create_default_context()
+ return ctx
+ except Exception:
+ pass
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ return ctx
+
+
+def _fetch(url, timeout=30):
+ """GET с User-Agent, возвращает текст или None."""
+ req = urllib.request.Request(url, headers={'User-Agent': USER_AGENT})
+ try:
+ with urllib.request.urlopen(req, timeout=timeout, context=_ssl_ctx()) as resp:
+ return resp.read().decode('utf-8', errors='ignore')
+ except Exception:
+ # повтор с отключённой проверкой сертификата (некоторые сети/прокси)
+ try:
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
+ return resp.read().decode('utf-8', errors='ignore')
+ except Exception:
+ return None
+
+
+def _parse_threats(html):
+ """Извлекает названия угроз из HTML-таблицы БДУ.
+
+ Строки вида:
...| УБИ. 001 | Угроза ... | ...
+ Возвращает список названий (без кода).
+ """
+ names = []
+ if not html:
+ return names
+ for row in re.findall(r']*>(.*?)
', html, re.S):
+ cells = re.findall(r']*>(.*?)', row, re.S)
+ cleaned = [re.sub(r'<[^>]+>', '', c).strip() for c in cells]
+ # формат: [код, название, ...] — берём ячейку, которая не похожа на код УБИ
+ for cell in cleaned:
+ if not cell:
+ continue
+ if re.fullmatch(r'УБИ\.?\s*[\d.]+', cell):
+ continue
+ if cell in ('№', 'Идентификатор', 'Наименование', 'Дата', 'Уровень опасности'):
+ continue
+ names.append(cell)
+ break
+ return names
+
+
+def download_fstec_threats(dict_dir=None, max_pages=8, page_size=100):
+ """Скачивает перечень угроз с bdu.fstec.ru.
+
+ Возвращает (ok, сообщение, список_угроз). При успехе сохраняет:
+ - dictionaries/threats_fstec.json — список названий угроз
+ - dictionaries/threats_fstec_date.txt — дата актуализации (ГГГГ-ММ-ДД)
+ """
+ if dict_dir is None:
+ dict_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries')
+
+ threats = []
+ for page in range(1, max_pages + 1):
+ url = f"{BDU_URL}?size={page_size}&page={page}"
+ html = _fetch(url)
+ if not html:
+ break
+ names = _parse_threats(html)
+ if not names:
+ break
+ for n in names:
+ if n not in threats:
+ threats.append(n)
+ # если на странице меньше page_size — это последняя
+ if len(names) < page_size:
+ break
+
+ if not threats:
+ return False, "Не удалось загрузить угрозы с сайта ФСТЭК (bdu.fstec.ru)", []
+
+ try:
+ os.makedirs(dict_dir, exist_ok=True)
+ with open(os.path.join(dict_dir, 'threats_fstec.json'), 'w', encoding='utf-8') as f:
+ json.dump(threats, f, ensure_ascii=False, indent=2)
+ today = datetime.now().strftime('%Y-%m-%d')
+ with open(os.path.join(dict_dir, 'threats_fstec_date.txt'), 'w', encoding='utf-8') as f:
+ f.write(today)
+ except Exception as e:
+ return False, f"Ошибка сохранения: {e}", threats
+
+ return True, f"Загружено угроз: {len(threats)}", threats
+
+
+def get_threats_date(dict_dir=None):
+ """Дата последней актуализации списка угроз (или None)."""
+ if dict_dir is None:
+ dict_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries')
+ path = os.path.join(dict_dir, 'threats_fstec_date.txt')
+ if os.path.exists(path):
+ try:
+ with open(path, encoding='utf-8') as f:
+ return f.read().strip()
+ except Exception:
+ pass
+ return None
+
+
+def load_threats(dict_dir=None):
+ """Загружает список угроз: сначала скачанный с ФСТЭК, иначе базовый threats.json."""
+ if dict_dir is None:
+ dict_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries')
+ fstec = os.path.join(dict_dir, 'threats_fstec.json')
+ if os.path.exists(fstec):
+ try:
+ with open(fstec, encoding='utf-8') as f:
+ return json.load(f)
+ except Exception:
+ pass
+ base = os.path.join(dict_dir, 'threats.json')
+ try:
+ with open(base, encoding='utf-8') as f:
+ return json.load(f)
+ except Exception:
+ return []
diff --git a/dokogen/security_tab_187_reference.py b/dokogen/security_tab_187_reference.py
new file mode 100644
index 0000000..77b1339
--- /dev/null
+++ b/dokogen/security_tab_187_reference.py
@@ -0,0 +1,480 @@
+# -*- coding: utf-8 -*-
+"""
+ВКЛАДКА «БЕЗОПАСНОСТЬ» из DokoGen 187-ФЗ (dokogen187, ветка source)
+=====================================================================
+Скопировано для переноса в DokoGen MU, чтобы интерфейс был одинаковым.
+
+ЧТО ВХОДИТ:
+ 1. Секция построения вкладки (вставить в метод _build_is_tab перед вкладкой «Средства защиты»)
+ 2. Методы-обработчики (вставить в класс приложения)
+ 3. Словари — скопированы в dokogen/dictionaries/:
+ attacker_categories.json, incidents.json, criteria_indicators.json,
+ org_measures.json, tech_measures.json, threats.json,
+ threats_fstec.json, threats_fstec_date.txt
+ 4. Модуль fstec_threats.py (скачивание перечня угроз с bdu.fstec.ru)
+ 5. check_security_ai() — ИИ-проверка (см. конец файла, добавить в verify.py)
+
+ЗАВИСИМОСТИ:
+ - tkinter/ttk (стандарт)
+ - dokogen/fstec_threats.py (загрузка угроз ФСТЭК)
+ - dokogen/verify.py: check_security_ai (ИИ-проверка)
+ - dokogen/api_settings.py: load_api_settings
+ - Поля модели InformationSystem (добавить в models.py, если нет):
+ attacker_category, attacker_characteristics, threats, incidents,
+ criteria_values, criteria_justification, org_measures, tech_measures
+ - Маппинги _collect_is/_on_is_select (см. комментарии в конце)
+
+ПРИМЕЧАНИЕ: в DokoGen MU (152-ФЗ) модель InformationSystem другая —
+ при переносе адаптировать имена полей или добавить новые.
+"""
+
+
+# ============================================================
+# 1. СЕКЦИЯ ПОСТРОЕНИЯ ВКЛАДКИ (вставить в _build_is_tab)
+# ============================================================
+
+ # ===== Вкладка «Безопасность» =====
+ 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)
+
+ # --- Категория нарушителя (словарик) ---
+ 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)
+
+
+
+# ============================================================
+# 2. МЕТОДЫ-ОБРАБОТЧИКИ (вставить в класс приложения)
+# ============================================================
+
+ 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 _refine_dict_item_dialog(self, base_item):
+ """Окно уточнения модели и количества для позиции из словаря.
+
+ Возвращает полную строку вида «Windows 10 Pro 5 шт.» или
+ «Сервер Supermicro SSG-2027R -E1R24N 2 шт.» или None.
+ """
+ import re
+ # Убираем из базы маркер «(уточнить …)»
+ base = re.sub(r'\s*\([^)]*уточн[^)]*\)', '', base_item, flags=re.I).strip() or base_item
+
+ dialog = tk.Toplevel(self.root)
+ dialog.title("Уточнить модель и количество")
+ dialog.geometry("500x180")
+ dialog.transient(self.root)
+ dialog.grab_set()
+
+ frame = ttk.Frame(dialog, padding=10)
+ frame.pack(fill=tk.BOTH, expand=True)
+
+ ttk.Label(frame, text=f"Позиция: {base}", wraplength=470,
+ font=('Arial', 9, 'bold')).pack(anchor=tk.W, pady=(0, 8))
+
+ ttk.Label(frame, text="Модель / версия / конфигурация (необязательно):").pack(anchor=tk.W)
+ model_entry = ttk.Entry(frame, width=60)
+ model_entry.pack(fill=tk.X, pady=(2, 6))
+ model_entry.focus_set()
+
+ ttk.Label(frame, text="Количество:").pack(anchor=tk.W)
+ count_entry = ttk.Entry(frame, width=20)
+ count_entry.pack(anchor=tk.W, pady=(2, 6))
+
+ result = {}
+
+ def _ok():
+ model = model_entry.get().strip()
+ count = count_entry.get().strip()
+ text = base
+ if model:
+ text = f"{text} {model}"
+ if count:
+ text = f"{text} {count} шт."
+ result['val'] = text
+ dialog.destroy()
+
+ def _cancel():
+ result['val'] = None
+ dialog.destroy()
+
+ btn_frame = ttk.Frame(frame)
+ btn_frame.pack(fill=tk.X, pady=(6, 0))
+ ttk.Button(btn_frame, text="✅ ОК", command=_ok).pack(side=tk.RIGHT, padx=2)
+ ttk.Button(btn_frame, text="Отмена", command=_cancel).pack(side=tk.RIGHT, padx=2)
+
+ dialog.bind('', lambda e: _ok())
+ dialog.bind('', lambda e: _cancel())
+ dialog.wait_window()
+ return result.get('val')
+
+ def _select_sw_from_dict(self, listbox, dict_name):
+ """Выбор позиций из словаря (JSON) — можно несколько."""
+ import json
+ dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', dict_name)
+ try:
+ with open(dict_path, 'r', encoding='utf-8') as f:
+ items = json.load(f)
+ title = "Выбор из словаря"
+ self._select_from_dialog(title, items, listbox)
+ except Exception as e:
+ messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
+
+ def _sw_dialog(self, title, initial=""):
+ """Большое окно для ввода/редактирования позиции ПО."""
+ dialog = tk.Toplevel(self.root)
+ dialog.title(title)
+ dialog.geometry("560x220")
+ dialog.transient(self.root)
+ dialog.grab_set()
+ dialog.resizable(True, True)
+
+ frame = ttk.Frame(dialog, padding=10)
+ frame.pack(fill=tk.BOTH, expand=True)
+
+ ttk.Label(frame, text="Текст позиции:").pack(anchor=tk.W)
+ txt = tk.Text(frame, width=70, height=6, wrap=tk.WORD)
+ txt.pack(fill=tk.BOTH, expand=True, pady=5)
+ txt.insert('1.0', initial)
+ txt.focus_set()
+
+ result = {}
+
+ def _ok():
+ result['val'] = txt.get('1.0', 'end-1c').strip()
+ dialog.destroy()
+
+ def _cancel():
+ result['val'] = None
+ dialog.destroy()
+
+ btn_frame = ttk.Frame(frame)
+ btn_frame.pack(fill=tk.X)
+ ttk.Button(btn_frame, text="✅ ОК", command=_ok).pack(side=tk.RIGHT, padx=2)
+ ttk.Button(btn_frame, text="Отмена", command=_cancel).pack(side=tk.RIGHT, padx=2)
+
+ dialog.bind('', lambda e: _ok())
+ dialog.bind('', lambda e: _cancel())
+ dialog.wait_window()
+ return result.get('val')
+
+ def _add_sw_item(self, listbox):
+ """Добавить строку в список ПО (большое окно)."""
+ item = self._sw_dialog("Добавить позицию")
+ if item:
+ listbox.insert(tk.END, item)
+
+ def _del_sw_item(self, listbox):
+ """Удалить выбранную строку из списка ПО."""
+ sel = listbox.curselection()
+ if sel:
+ listbox.delete(sel[0])
+
+ def _edit_sw_item(self, listbox):
+ """Редактировать выбранную строку списка ПО (большое окно)."""
+ sel = listbox.curselection()
+ if not sel:
+ messagebox.showinfo("Редактировать", "Сначала выберите позицию в списке")
+ return
+ current = listbox.get(sel[0])
+ item = self._sw_dialog("Редактировать позицию", initial=current)
+ if item is not None:
+ listbox.delete(sel[0])
+ listbox.insert(sel[0], item)
+
+# ============================================================
+# 3. ИИ-ПРОВЕРКА (добавить в verify.py)
+# ============================================================
+
+def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=None):
+ """ИИ-проверка заполненной вкладки «Безопасность» объекта КИИ (236 приказ).
+
+ Возвращает (результат, ошибка): результат — dict с 'оценка', 'замечания', 'предложения'.
+ """
+ if not api_key:
+ return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
+ system = (
+ "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, "
+ "приказ ФСТЭК России от 25.12.2017 № 239 «Об утверждении Требований по обеспечению "
+ "безопасности значимых объектов КИИ» и приказ № 236). Проверь заполненные сведения "
+ "об объекте КИИ и дай рекомендации. Верни ТОЛЬКО JSON: "
+ '{"оценка": "ok|warning|error", '
+ '"замечания": ["..."], '
+ '"предложения": {"attacker_category": "...", "threats": "...", "incidents": "...", '
+ '"criteria_values": "...", "criteria_justification": "...", '
+ '"org_measures": "...", "tech_measures": "..."}} '
+ "В «предложениях» укажи, что добавить/исправить по каждому разделу, если нужно, иначе пустые строки."
+ )
+ user = json.dumps(security_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': 'security',
+ 'data': {
+ 'rating': js.get('оценка', 'warning'),
+ 'notes': notes,
+ 'proposals': proposals,
+ },
+ }, None
+
+# ============================================================
+# 4. МАППИНГИ ПОЛЕЙ
+# ============================================================
+# В _collect_is() (список виджетов → модель):
+# ('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'),
+# В _on_is_select() — обратный порядок (combo → .set(), listbox → split('\n'))
+# После построения вкладок вызвать: self._update_threats_date_label()
diff --git a/dokogen/verify.py b/dokogen/verify.py
index f562aa2..afea6f6 100644
--- a/dokogen/verify.py
+++ b/dokogen/verify.py
@@ -389,6 +389,45 @@ def compare_company_values(old_data, new_data):
return diffs
+def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=None):
+ """ИИ-проверка заполненной вкладки «Безопасность» объекта КИИ (236 приказ)."""
+ if not api_key:
+ return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
+ system = (
+ "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, "
+ "приказ ФСТЭК России от 25.12.2017 № 239 «Об утверждении Требований по обеспечению "
+ "безопасности значимых объектов КИИ» и приказ № 236). Проверь заполненные сведения "
+ "об объекте КИИ и дай рекомендации. Верни ТОЛЬКО JSON: "
+ '{"оценка": "ok|warning|error", '
+ '"замечания": ["..."], '
+ '"предложения": {"attacker_category": "...", "threats": "...", "incidents": "...", '
+ '"criteria_values": "...", "criteria_justification": "...", '
+ '"org_measures": "...", "tech_measures": "..."}} '
+ "В «предложениях» укажи, что добавить/исправить по каждому разделу, если нужно, иначе пустые строки."
+ )
+ user = json.dumps(security_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': 'security',
+ 'data': {
+ 'rating': js.get('оценка', 'warning'),
+ 'notes': notes,
+ 'proposals': proposals,
+ },
+ }, None
+
+
def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-проверка реквизитов организации.