feat: переделка под ОКИИ (187-ФЗ) — объекты КИИ вместо ИС, категории 1-3, ОКВЭД, убраны ПДн/бумажные носители

- вкладка «Информационные системы» → «Объекты КИИ», убрана «Структура объекта»
- «Тип угроз»/«УЗ» → категория значимости 1/2/3/без категории
- удалены Категории ПДн, кол-во записей, перечень/субъекты/действия с ПДн + словари
- «Пользователи ИС» → «Пользователи ОКИИ», средства защиты оставлены
- вкладка «Бумажные документы» удалена полностью (GUI + логика + модели)
- ОКВЭД: поля во вкладке «Организация», словарь okved.json, выбор галочками, импорт
- нормализация регистра адреса DaData (КАПС → нормальный)
- importer: объекты КИИ (наименование объекта, ПО, СЗИ, пользователи)
This commit is contained in:
2026-08-04 13:58:23 +04:00
parent b115f9e2b4
commit ce1b56066a
11 changed files with 562 additions and 724 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""DokoGen — Пакет генератора документов 187-ФЗ""" """DokoGen — Пакет генератора документов 187-ФЗ"""
from .models import Company, CommissionMember, InformationSystem, PaperDocument from .models import Company, CommissionMember, InformationSystem
from .declension import inflect_name, decline, company_name_decline, get_short_fio, get_initials from .declension import inflect_name, decline, company_name_decline, get_short_fio, get_initials
from .variables import build_replacements, VARIABLE_DEFS from .variables import build_replacements, VARIABLE_DEFS
from .generator import process_template from .generator import process_template
+268
View File
@@ -0,0 +1,268 @@
[
"01.1 Выращивание однолетних культур",
"01.2 Выращивание многолетних культур",
"01.3 Выращивание рассады",
"01.4 Животноводство",
"01.5 Смешанное сельское хозяйство",
"01.6 Деятельность вспомогательная в области производства сельскохозяйственных культур и послеуборочной обработки сельхозпродукции",
"01.7 Охота, отлов и отстрел диких животных",
"01.8 Лесоводство и прочая деятельность в области лесозаготовок",
"02.1 Лесоводство и прочая деятельность в области лесозаготовок",
"02.2 Предоставление услуг в области лесоводства и лесозаготовок",
"02.3 Сбор и заготовка пищевых лесных ресурсов, недревесных лесных ресурсов и лекарственных растений",
"03.1 Рыболовство",
"03.2 Рыбоводство",
"05.1 Добыча угля",
"05.2 Добыча и обогащение угля",
"06.1 Добыча сырой нефти и природного газа",
"06.2 Добыча природного газа и газового конденсата",
"07.1 Добыча железных руд",
"07.2 Добыча руд цветных металлов",
"08.1 Добыча камня, песка и глины",
"08.9 Добыча полезных ископаемых, не включенных в другие группировки",
"09.1 Деятельность вспомогательная в сфере добычи нефти и природного газа",
"10.1 Переработка и консервирование мяса и мясной пищевой продукции",
"10.2 Переработка и консервирование рыбы, ракообразных и моллюсков",
"10.3 Переработка и консервирование фруктов и овощей",
"10.4 Производство растительных и животных масел и жиров",
"10.5 Производство молочной продукции",
"10.6 Производство мукомольной и крупяной промышленности, крахмала и крахмалосодержащих продуктов",
"10.7 Производство хлебобулочных и мучных кондитерских изделий",
"10.8 Производство прочих пищевых продуктов",
"10.9 Производство готовых кормов для животных",
"11.0 Производство напитков",
"12.0 Производство табачных изделий",
"13.1 Прядение текстильных волокон",
"13.2 Ткацкое производство",
"13.3 Отделка тканей и текстильных изделий",
"13.9 Производство прочих текстильных изделий",
"14.1 Производство одежды, кроме одежды из меха",
"14.2 Производство меховых изделий",
"14.3 Производство вязаных и трикотажных изделий одежды",
"15.1 Дубление и отделка кожи, производство чемоданов, сумок и т.п.",
"15.2 Производство обуви",
"16.1 Распиловка и строгание древесины",
"16.2 Производство изделий из дерева, пробки, соломки",
"17.1 Производство целлюлозы, древесной массы, бумаги и картона",
"17.2 Производство изделий из бумаги и картона",
"18.1 Деятельность полиграфическая и предоставление услуг в этой области",
"18.2 Копирование записанных носителей информации",
"19.1 Производство кокса",
"19.2 Производство нефтепродуктов",
"20.1 Производство основных химических веществ, удобрений, пластмасс и синтетического каучука",
"20.2 Производство пестицидов и прочей агрохимической продукции",
"20.3 Производство красок, лаков и аналогичных покрытий",
"20.4 Производство мыла и моющих, чистящих и полирующих средств",
"20.5 Производство прочих химических продуктов",
"21.1 Производство фармацевтических субстанций",
"21.2 Производство лекарственных препаратов и материалов",
"22.1 Производство резиновых изделий",
"22.2 Производство пластмассовых изделий",
"23.1 Производство стекла и изделий из стекла",
"23.2 Производство огнеупорных изделий",
"23.3 Производство строительных керамических материалов",
"23.4 Производство прочих фарфоровых и керамических изделий",
"23.5 Производство цемента, извести и гипса",
"23.6 Производство изделий из бетона, цемента и гипса",
"23.7 Резка, обработка и отделка камня",
"23.9 Производство абразивных и неметаллических минеральных изделий",
"24.1 Производство чугуна, стали и ферросплавов",
"24.2 Производство стальных труб, полых профилей и фитингов",
"24.3 Производство прочих стальных изделий первичной обработки",
"24.4 Производство основных драгоценных металлов и цветных металлов",
"24.5 Литье металлов",
"25.1 Производство строительных металлических конструкций и изделий",
"25.2 Производство металлических цистерн, резервуаров и емкостей",
"25.3 Производство паровых котлов",
"25.4 Производство оружия и боеприпасов",
"25.5 Ковка, прессование, штамповка и профилирование; порошковая металлургия",
"25.6 Обработка металлических изделий механическая",
"25.7 Производство ножевых изделий и столовых приборов",
"25.9 Производство прочих готовых металлических изделий",
"26.1 Производство элементов электронной аппаратуры и печатных схем",
"26.2 Производство компьютеров и периферийного оборудования",
"26.3 Производство коммуникационного оборудования",
"26.4 Производство бытовой электроники",
"26.5 Производство контрольно-измерительных приборов и навигационных приборов",
"26.6 Производство облучающего и электротерапевтического оборудования",
"26.7 Производство оптических приборов, фото- и кинооборудования",
"26.8 Производство незаписанных магнитных и оптических носителей",
"27.1 Производство электродвигателей, генераторов, трансформаторов",
"27.2 Производство электрических аккумуляторов и аккумуляторных батарей",
"27.3 Производство кабелей и кабельной арматуры",
"27.4 Производство электрического осветительного оборудования",
"27.5 Производство бытовых приборов",
"27.9 Производство прочего электрического оборудования",
"28.1 Производство машин и оборудования общего назначения",
"28.2 Производство прочих машин и оборудования общего назначения",
"28.3 Производство машин и оборудования для сельского и лесного хозяйства",
"28.4 Производство станков, машин и оборудования для обработки металлов",
"28.9 Производство прочих машин и оборудования специального назначения",
"29.1 Производство автотранспортных средств",
"29.2 Производство кузовов, прицепов и полуприцепов",
"29.3 Производство комплектующих и принадлежностей для автотранспортных средств",
"30.1 Строительство кораблей, судов и лодок",
"30.2 Производство железнодорожных локомотивов и подвижного состава",
"30.3 Производство летательных аппаратов и космических аппаратов",
"30.4 Производство военных боевых машин",
"30.9 Производство прочих транспортных средств и оборудования",
"31.0 Производство мебели",
"32.1 Производство ювелирных изделий, бижутерии",
"32.2 Производство музыкальных инструментов",
"32.3 Производство спортивных товаров",
"32.4 Производство игр и игрушек",
"32.5 Производство медицинских инструментов и оборудования",
"32.9 Производство изделий, не включенных в другие группировки",
"33.1 Ремонт и монтаж металлических изделий и машин",
"33.2 Монтаж промышленных машин и оборудования",
"35.1 Производство, передача и распределение электроэнергии",
"35.2 Производство и распределение газообразного топлива",
"35.3 Производство, передача и распределение пара и горячей воды",
"36.0 Забор, очистка и распределение воды",
"37.0 Сбор и обработка сточных вод",
"38.1 Сбор отходов",
"38.2 Обработка и утилизация отходов",
"38.3 Деятельность по обработке вторичного сырья",
"39.0 Предоставление услуг в области ликвидации последствий загрязнений",
"41.1 Разработка строительных проектов",
"41.2 Строительство жилых и нежилых зданий",
"42.1 Строительство автомобильных и железных дорог",
"42.2 Строительство инженерных коммуникаций",
"42.9 Строительство прочих инженерных сооружений",
"43.1 Разборка и снос зданий, подготовка строительного участка",
"43.2 Производство электромонтажных, санитарно-технических и прочих строительно-монтажных работ",
"43.3 Работы строительные отделочные",
"43.9 Работы строительные специализированные прочие",
"45.1 Торговля автотранспортными средствами",
"45.2 Техническое обслуживание и ремонт автотранспортных средств",
"45.3 Торговля автомобильными деталями и принадлежностями",
"45.4 Торговля мотоциклами и их обслуживание",
"46.1 Торговля оптовая за вознаграждение или на договорной основе",
"46.2 Торговля оптовая сельскохозяйственным сырьем и живыми животными",
"46.3 Торговля оптовая пищевыми продуктами, напитками и табачными изделиями",
"46.4 Торговля оптовая непродовольственными потребительскими товарами",
"46.5 Торговля оптовая информационным и коммуникационным оборудованием",
"46.6 Торговля оптовая прочими машинами и оборудованием",
"46.7 Торговля оптовая специализированная прочая",
"46.9 Торговля оптовая неспециализированная",
"47.1 Торговля розничная в неспециализированных магазинах",
"47.2 Торговля розничная пищевыми продуктами в специализированных магазинах",
"47.3 Торговля розничная моторным топливом",
"47.4 Торговля розничная информационным и коммуникационным оборудованием",
"47.5 Торговля розничная прочими бытовыми изделиями",
"47.6 Торговля розничная товарами культурно-развлекательного назначения",
"47.7 Торговля розничная прочими товарами в специализированных магазинах",
"47.8 Торговля розничная в палатках и на рынках",
"47.9 Торговля розничная вне магазинов, палаток, рынков",
"49.1 Деятельность железнодорожного пассажирского транспорта",
"49.2 Деятельность железнодорожного грузового транспорта",
"49.3 Деятельность прочего сухопутного пассажирского транспорта",
"49.4 Деятельность автомобильного грузового транспорта",
"49.5 Деятельность трубопроводного транспорта",
"50.1 Деятельность морского пассажирского транспорта",
"50.2 Деятельность морского грузового транспорта",
"50.3 Деятельность внутреннего водного пассажирского транспорта",
"50.4 Деятельность внутреннего водного грузового транспорта",
"51.1 Деятельность пассажирского воздушного транспорта",
"51.2 Деятельность грузового воздушного транспорта",
"52.1 Деятельность по складированию и хранению",
"52.2 Деятельность транспортная вспомогательная",
"53.1 Деятельность почтовой связи общего пользования",
"53.2 Деятельность почтовой связи прочая и курьерская",
"55.1 Деятельность гостиниц и прочих мест временного проживания",
"55.2 Деятельность по предоставлению мест для краткосрочного проживания",
"55.3 Деятельность по предоставлению мест для кемпингов и стоянок",
"56.1 Деятельность ресторанов и услуги по доставке продуктов питания",
"56.2 Деятельность предприятий общественного питания",
"56.3 Подача напитков",
"58.1 Издание книг, периодических изданий и другие виды издательской деятельности",
"58.2 Издание программного обеспечения",
"59.1 Производство кинофильмов, видеофильмов и телевизионных программ",
"59.2 Деятельность в области звукозаписи и издания музыкальных произведений",
"60.1 Деятельность в области радиовещания",
"60.2 Деятельность в области телевизионного вещания",
"61.1 Деятельность в области проводной связи",
"61.2 Деятельность в области беспроводной связи",
"61.3 Деятельность в области спутниковой связи",
"61.9 Деятельность в области телекоммуникаций прочая",
"62.0 Разработка компьютерного программного обеспечения, консультационные услуги в данной области",
"63.1 Деятельность по обработке данных, предоставление услуг по размещению информации",
"63.9 Деятельность в области информационных услуг прочая",
"64.1 Денежное посредничество",
"64.2 Деятельность холдинговых компаний",
"64.3 Деятельность инвестиционных фондов",
"64.9 Деятельность по финансовому посредничеству прочая",
"65.1 Страхование жизни",
"65.2 Перестрахование",
"65.3 Страхование и перестрахование, негосударственное пенсионное обеспечение",
"66.1 Деятельность вспомогательная в сфере финансовых услуг",
"66.2 Деятельность вспомогательная в сфере страхования",
"66.3 Деятельность по управлению фондами",
"68.1 Покупка и продажа собственного недвижимого имущества",
"68.2 Аренда и управление собственным или арендованным недвижимым имуществом",
"68.3 Операции с недвижимым имуществом за вознаграждение или на договорной основе",
"69.1 Деятельность в области права",
"69.2 Деятельность по оказанию услуг в области бухгалтерского учета и аудита",
"70.1 Деятельность головных офисов",
"70.2 Консультирование по вопросам управления",
"70.3 Деятельность по связям с общественностью",
"71.1 Деятельность в области архитектуры, инженерных изысканий",
"71.2 Технические испытания, исследования, анализ и сертификация",
"72.1 Научные исследования и разработки в области естественных и технических наук",
"72.2 Научные исследования и разработки в области общественных и гуманитарных наук",
"73.1 Деятельность рекламная",
"73.2 Исследование конъюнктуры рынка и опросы общественного мнения",
"74.1 Деятельность специализированная в области дизайна",
"74.2 Деятельность в области фотографии",
"74.3 Деятельность по письменному и устному переводу",
"74.9 Деятельность профессиональная, научная и техническая прочая",
"75.0 Деятельность ветеринарная",
"77.1 Аренда и лизинг автотранспортных средств",
"77.2 Прокат и аренда предметов личного пользования и хозяйственно-бытового назначения",
"77.3 Аренда и лизинг машин и оборудования",
"77.4 Аренда и лизинг интеллектуальной собственности",
"78.1 Деятельность агентств по подбору персонала",
"78.2 Деятельность агентств по временному трудоустройству",
"78.3 Деятельность по обеспечению безопасности в чрезвычайных ситуациях; деятельность по обеспечению безопасности в области информатизации",
"79.1 Деятельность туристических агентств и туроператоров",
"79.9 Услуги по бронированию прочие",
"80.1 Деятельность частных охранных служб",
"80.2 Деятельность систем обеспечения безопасности",
"80.3 Деятельность по расследованию",
"81.1 Деятельность по комплексному обслуживанию помещений",
"81.2 Деятельность по чистке и уборке",
"81.3 Деятельность по благоустройству ландшафта",
"82.1 Деятельность административно-хозяйственная комплексная",
"82.2 Деятельность центров обработки телефонных вызовов",
"82.3 Деятельность по организации конференций и выставок",
"82.9 Деятельность по предоставлению вспомогательных услуг для бизнеса",
"84.1 Деятельность органов государственного управления",
"84.2 Предоставление государственных услуг обществу",
"84.3 Деятельность в области обязательного социального обеспечения",
"85.1 Образование общее",
"85.2 Образование профессиональное",
"85.3 Обучение профессиональное",
"85.4 Образование дополнительное",
"86.1 Деятельность больничных организаций",
"86.2 Медицинская и стоматологическая практика",
"86.9 Деятельность в области медицины прочая",
"87.1 Деятельность по уходу с обеспечением проживания",
"87.2 Деятельность по уходу с обеспечением проживания для умственно отсталых и психически больных",
"87.9 Деятельность по уходу с обеспечением проживания прочая",
"88.1 Предоставление социальных услуг без обеспечения проживания престарелым и инвалидам",
"88.9 Предоставление прочих социальных услуг без обеспечения проживания",
"90.0 Деятельность творческая, деятельность в области искусства и организации развлечений",
"91.0 Деятельность библиотек, архивов, музеев и прочих объектов культуры",
"92.0 Деятельность по организации и проведению азартных игр и пари",
"93.1 Деятельность в области спорта",
"93.2 Деятельность в области отдыха и развлечений",
"94.1 Деятельность профессиональных союзов",
"94.2 Деятельность религиозных организаций",
"94.9 Деятельность общественных организаций прочая",
"95.1 Ремонт компьютеров и коммуникационного оборудования",
"95.2 Ремонт предметов личного потребления и хозяйственно-бытового назначения",
"96.0 Деятельность по предоставлению прочих персональных услуг",
"97.0 Деятельность домашних хозяйств с наемными работниками",
"98.0 Деятельность домашних хозяйств по производству товаров для собственного использования",
"99.0 Деятельность экстерриториальных организаций и органов"
]
-17
View File
@@ -1,17 +0,0 @@
[
"сбор",
"систематизация",
"хранение",
"извлечение",
"передача (предоставление доступа)",
"блокирование",
"уничтожение",
"запись",
"накопление",
"уточнение (обновление, изменение)",
"использование",
"обезличивание",
"удаление",
"распространение",
"иные действия"
]
-16
View File
@@ -1,16 +0,0 @@
[
"Сотрудникам организации",
"Соискателям",
"Родственникам сотрудников",
"Уволенным сотрудникам",
"Контрагентам",
"Представителям контрагентов",
"Клиентам",
"Посетителям сайта",
"Выгодоприобретателям по договорам",
"Субъект персональных данных, предоставивший согласие на трансграничную передачу персональных данных",
"Учащимя",
"Студентам",
"Законным представителям",
"Иным категориям субъектов персональных данных, персональные данные которых обрабатываются"
]
-44
View File
@@ -1,44 +0,0 @@
[
"Фамилия, Имя, Отчество",
"Год рождения",
"Месяц рождения",
"Дата рождения",
"Место рождения",
"Семейное положение",
"Социальное положение",
"Имущественное положение",
"Сведения о доходах",
"Пол",
"Адрес электронной почты",
"Адрес места жительства",
"Адрес регистрации",
"Номер телефона",
"Страховой номер индивидуального лицевого счета (СНИЛС)",
"Идентификационный номер налогоплательщика (ИНН);",
"Сведения о гражданстве",
"Данные документа, удостоверяющего личность",
"Данные водительского удостоверения",
"Данные документа, удостоверяющего личность за пределами Российской Федерации",
"Данные документа, содержащиеся в свидетельстве о рождении",
"Реквизиты банковских карт",
"Номер расчетного счета",
"Номер лицевого счета",
"Профессия",
"Должность",
"Сведения о трудовой деятельности (в том числе стаж работы, данные о трудовой занятости на текущее время с указанием наименования и расчетного счета организации)",
"Сведения об отношении к воинской обязанности, о воинском учете",
"Иные персональные данные",
"Сведения, собираемые посредством метрических программ (например, Яндекс.Метрика)",
"Сведения об образовании",
"Фото-видео изображение лица",
"Данные голоса человека",
"Сведения о состоянии здоровья",
"Национальная принадлежность",
"Политические взгляды",
"Религиозные или философские убеждения",
"Сведения о судимости",
"Данные изображения лица, полученные с помощью фото- видео устройств, на основании которых можно установить его личность и которые используются оператором для установления личности субъекта персональных данных (биометрия)",
"Данные голоса человека, на основании которых можно установить его личность и которые используются оператором для установления личности субъекта персональных данных (биометрия)",
"Изображение папиллярных узоров пальцев рук",
"Иные биометрические персональные данные"
]
+10 -10
View File
@@ -97,10 +97,10 @@ def _generate_variables_reference(path):
_bullet(doc, f'{var}{desc}') _bullet(doc, f'{var}{desc}')
_h(doc, 'Циклы', 12) _h(doc, 'Циклы', 12)
_bullet(doc, 'information_systems — ИС ({{item.name}}, {{item.description}}, {{item.structure}}, {{item.pd_actions}}, {{item.defense_tools_list}}, {{item.users}}, {{item.category}}, {{item.pd_count}} и др.).') _bullet(doc, 'information_systems — объекты КИИ ({{item.name}}, {{item.description}}, {{item.category}}, {{item.defense_tools_list}}, {{item.users}}, {{item.software}} и др.).')
_bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).') _bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).')
_bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).') _bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).')
_bullet(doc, 'paper_documents — бумажные документы ({{item.document}}, {{item.storage}}).')
doc.save(path) doc.save(path)
@@ -120,10 +120,10 @@ def _generate_user_manual(path):
_h(doc, '2. Основные вкладки', 12) _h(doc, '2. Основные вкладки', 12)
_bullet(doc, 'Организация — реквизиты, адреса, руководитель, склонения названий, проверка данных (Морфер / DaData / ИИ).') _bullet(doc, 'Организация — реквизиты, адреса, руководитель, склонения названий, проверка данных (Морфер / DaData / ИИ).')
_bullet(doc, 'Информационные системы — ИС, категории ПДн, действия с ПДн, средства защиты, пользователи.') _bullet(doc, 'Объекты КИИ — наименование, описание, категория значимости, средства защиты, пользователи ОКИИ.')
_bullet(doc, 'Комиссия — состав комиссии по классификации ИСПДн.') _bullet(doc, 'Комиссия — состав комиссии по категорированию объектов КИИ.')
_bullet(doc, 'Пользователи — сотрудники с доступом к ПДн.') _bullet(doc, 'Пользователи — сотрудники с доступом к объектам КИИ.')
_bullet(doc, 'Бумажные документы — бумажные носители ПДн и места хранения.')
_bullet(doc, 'Генерация — выбор шаблонов и запуск генерации.') _bullet(doc, 'Генерация — выбор шаблонов и запуск генерации.')
_bullet(doc, 'Лог — журнал действий, экспорт в файл.') _bullet(doc, 'Лог — журнал действий, экспорт в файл.')
@@ -137,13 +137,13 @@ def _generate_user_manual(path):
_h(doc, '4. Циклы (повторяющиеся блоки)', 12) _h(doc, '4. Циклы (повторяющиеся блоки)', 12)
_p(doc, 'Если в документе нужно повторить блок для каждой ИС, комиссии, ' _p(doc, 'Если в документе нужно повторить блок для каждой ИС, комиссии, '
'сотрудника или бумажного документа — используются маркеры цикла:') 'сотрудника или объекта КИИ — используются маркеры цикла:')
_p(doc, '<!-- loop:information_systems --> ... {{item.name}} ... <!-- loop_end -->', bold=True) _p(doc, '<!-- loop:information_systems --> ... {{item.name}} ... <!-- loop_end -->', bold=True)
_p(doc, 'Доступные циклы:') _p(doc, 'Доступные циклы:')
_bullet(doc, 'information_systems — ИС ({{item.name}}, {{item.description}}, {{item.structure}}, {{item.pd_actions}}, {{item.defense_tools_list}}, {{item.users}}, {{item.category}}, {{item.pd_count}} и др.).') _bullet(doc, 'information_systems — объекты КИИ ({{item.name}}, {{item.description}}, {{item.category}}, {{item.defense_tools_list}}, {{item.users}}, {{item.software}} и др.).')
_bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).') _bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).')
_bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).') _bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).')
_bullet(doc, 'paper_documents — бумажные документы ({{item.document}}, {{item.storage}}).')
_p(doc, 'Внутри цикла переменные пишутся через точку: {{item.поле}}. ' _p(doc, 'Внутри цикла переменные пишутся через точку: {{item.поле}}. '
'Допускается и синтаксис без точки: {{item_поле}}.') 'Допускается и синтаксис без точки: {{item_поле}}.')
@@ -179,7 +179,7 @@ def _generate_admin_manual(path):
_h(doc, '1. Файлы настроек рядом с программой', 12) _h(doc, '1. Файлы настроек рядом с программой', 12)
_bullet(doc, 'api_settings.json — API-ключи (DaData, ИИ).') _bullet(doc, 'api_settings.json — API-ключи (DaData, ИИ).')
_bullet(doc, 'import_settings.json — словарь импорта: что и откуда брать из опросника.') _bullet(doc, 'import_settings.json — словарь импорта: что и откуда брать из опросника.')
_bullet(doc, 'dictionaries/ — справочники: defense_tools.json, pd_actions.json, pd_items.json, pd_categories.json.') _bullet(doc, 'dictionaries/ — справочники: defense_tools.json, okved.json.')
_bullet(doc, 'dokogen.log — журнал работы (пишется автоматически).') _bullet(doc, 'dokogen.log — журнал работы (пишется автоматически).')
_bullet(doc, 'docs/ — инструкции и пустой опросник (создаются автоматически).') _bullet(doc, 'docs/ — инструкции и пустой опросник (создаются автоматически).')
+53 -150
View File
@@ -141,8 +141,8 @@ def import_187fz(filepath: str, log_fn: Optional[Callable] = None, settings_path
data = { data = {
'fullName': '', 'shortName': '', 'addressLegal': '', 'addressActual': '', 'fullName': '', 'shortName': '', 'addressLegal': '', 'addressActual': '',
'email': '', 'phone': '', 'inn': '', 'ogrn': '', 'kpp': '', 'email': '', 'phone': '', 'inn': '', 'ogrn': '', 'kpp': '',
'okved': '', 'chiefPosition': '', 'chiefFio': '', 'okved': '', 'okvedExtra': '', 'chiefPosition': '', 'chiefFio': '',
'ispdnFio': '', 'ispdnPosition': '', 'paperDocuments': '', 'paperStorage': '', 'ispdnFio': '', 'ispdnPosition': '',
'ispdnDepartment': '', 'ispdnEmail': '', 'ispdnPhone': '', 'ispdnDepartment': '', 'ispdnEmail': '', 'ispdnPhone': '',
'informationSystems': [], 'commission': [], 'employeesAccess': [], 'informationSystems': [], 'commission': [], 'employeesAccess': [],
} }
@@ -248,7 +248,7 @@ def _import_187_common(wb, data, settings, log_fn):
def _import_187_is_list(wb, data, settings, log_fn): def _import_187_is_list(wb, data, settings, log_fn):
"""Импорт информационных систем из остальных листов.""" """Импорт объектов КИИ из остальных листов (ОБЪЕКТ №1, №2, ...)."""
exclude_kws = settings.get('is_sheet_exclude_keywords', ['общие сведения', 'сотрудники']) exclude_kws = settings.get('is_sheet_exclude_keywords', ['общие сведения', 'сотрудники'])
is_sheets = [] is_sheets = []
for sn in wb.sheetnames: for sn in wb.sheetnames:
@@ -257,7 +257,7 @@ def _import_187_is_list(wb, data, settings, log_fn):
continue continue
is_sheets.append(sn) is_sheets.append(sn)
if not is_sheets: if not is_sheets:
log_fn("⚠ Листы ИС не найдены") log_fn("⚠ Листы объектов КИИ не найдены")
return return
is_list = [] is_list = []
@@ -275,16 +275,20 @@ def _import_187_is_list(wb, data, settings, log_fn):
continue continue
is_obj = { is_obj = {
'name': '', 'description': '', 'room': '', 'employee': '', 'position': '', 'name': '', 'description': '', 'usersList': [],
'personalDataList': [], 'pd_subjects_list': [], 'pd_actions': [], 'isInternet': False, 'defense_tools': [], 'software': '',
'usersList': [], 'isInternet': False, 'personalDataCount': 'менее 100 000',
'defense_tools': [], 'processing_mode': '',
} }
current_section = 'header' defense_keywords = {
pd_items_raw = [] 'антивирус': 'Антивирус',
subjects_raw = [] 'крипто': 'СКЗИ',
actions_raw = [] 'межсетев': 'Межсетевой экран',
'сзи.*от несанкционирован': 'СЗИ от НСД',
'обнаружен.*вторжен': 'СОВ',
'программные.*средств.*модул': 'Доверенная загрузка',
'доверен.*загрузк': 'Доверенная загрузка',
'гарантирован': 'Доверенная загрузка',
}
for vals in rows: for vals in rows:
field = (vals[0] or '').strip() field = (vals[0] or '').strip()
@@ -292,159 +296,58 @@ def _import_187_is_list(wb, data, settings, log_fn):
val_b = clean_value(vals[1]) if len(vals) > 1 else '' val_b = clean_value(vals[1]) if len(vals) > 1 else ''
is_checked = val_b.upper() in ('ДА', 'YES', '', '', '1', 'TRUE', 'ЕСТЬ') is_checked = val_b.upper() in ('ДА', 'YES', '', '', '1', 'TRUE', 'ЕСТЬ')
if 'цель обработки пд' in field_lower or ('цель обработки' in field_lower and ':' in field): if 'наименование объекта' in field_lower or 'название объекта' in field_lower:
current_section = 'purpose' is_obj['name'] = val_b
log_fn(f" Объект КИИ: {val_b}")
continue continue
if 'перечисленные пдн принадлежат' in field_lower or 'принадлеж' in field_lower: if 'описание объекта' in field_lower or 'описание окки' in field_lower:
current_section = 'subjects'
continue
if 'структура ис' in field_lower or 'структура информационной' in field_lower:
if val_b: if val_b:
is_obj['structure'] = val_b is_obj['description'] = val_b
elif ':' in field:
# Значение может быть записано в той же ячейке после двоеточия
tail = field.split(':', 1)[1].strip()
if tail and 'выбрать' not in tail.lower():
is_obj['structure'] = tail
continue continue
if 'названия программ' in field_lower or ('программ' in field_lower and 'ос взаимодейств' in field_lower): if 'операционная' in field_lower or 'прикладные программы' in field_lower or 'наименования программно' in field_lower:
if val_b: if val_b:
is_obj['software'] = val_b is_obj['software'] = (is_obj['software'] + '; ' + val_b) if is_obj['software'] else val_b
continue
if 'локальную сеть' in field_lower:
is_obj['is_has_lan'] = is_checked
continue
if 'количество записей' in field_lower or 'количество субъект' in field_lower:
if val_b:
is_obj['personalDataCount'] = val_b
continue
if 'перечень действий с пд' in field_lower or ('действи' in field_lower and 'пд' in field_lower):
current_section = 'actions'
continue
if 'способ обработки пд' in field_lower:
if val_b:
is_obj['processing_mode'] = val_b
continue continue
if 'интернет' in field_lower: if 'интернет' in field_lower:
is_obj['isInternet'] = is_checked is_obj['isInternet'] = is_checked
continue continue
if 'сотрудник' in field_lower and 'должност' not in field_lower:
if current_section == 'header': if val_b:
if 'перечень пд' in field_lower: parts = [p.strip() for p in val_b.replace('\n', ';').split(';') if p.strip()]
current_section = 'pd_items' for part in parts:
elif 'название ис' in field_lower or 'наименование ис' in field_lower or 'наименование информационной' in field_lower or 'наименование объекта' in field_lower or 'название объекта' in field_lower: cleaned = part.strip()
is_obj['name'] = val_b if ':' in cleaned and len(cleaned.split(':')[0]) < 20:
log_fn(f" ИС: {val_b}") cleaned = cleaned.split(':', 1)[1].strip()
elif 'описание ис' in field_lower or 'описание информационной' in field_lower: if cleaned and cleaned not in is_obj['usersList']:
if val_b: is_obj['usersList'].append(cleaned)
is_obj['description'] = val_b continue
log_fn(f" Описание: {val_b[:60]}") # Средства защиты
continue matched = False
elif 'сотрудник' in field_lower and 'должност' not in field_lower: for kw, label in defense_keywords.items():
is_obj['employee'] = val_b if re.search(kw, field_lower):
if val_b: if val_b and 'выбрать' not in field_lower:
parts = [p.strip() for p in val_b.replace('\n', ';').split(';') if p.strip()] is_obj['defense_tools'].append(val_b)
for part in parts: matched = True
cleaned = part.strip() break
if ':' in cleaned and len(cleaned.split(':')[0]) < 20: if matched:
cleaned = cleaned.split(':', 1)[1].strip() continue
if cleaned and cleaned not in is_obj['usersList']: if val_b and 'выбрать' not in field_lower and field_lower not in ('значение', 'примечание'):
is_obj['usersList'].append(cleaned) # Остальные заполненные поля — в описание (адрес, сфера, тип и т.д.)
elif 'должность сотрудника' in field_lower: if not is_obj['description']:
is_obj['position'] = val_b is_obj['description'] = f"{field}: {val_b}"
elif 'наименование отдела' in field_lower: else:
pass is_obj['description'] += f"\n{field}: {val_b}"
elif 'номер кабинета' in field_lower:
is_obj['room'] = val_b
elif 'значение' in field_lower or 'примечание' in field_lower:
continue
elif val_b and 'выбрать' not in field_lower:
if not is_obj['description']:
is_obj['description'] = f"{field}: {val_b}"
else:
is_obj['description'] += f"\n{field}: {val_b}"
elif current_section == 'pd_items':
if is_checked and 'выбрать' not in field_lower and field_lower != 'значение':
pd_items_raw.append(field)
if 'цель обработки' in field_lower:
current_section = 'purpose'
elif current_section == 'purpose':
purpose_val = val_b or field or ''
if purpose_val and 'выбрать' not in purpose_val.lower() and ':' not in purpose_val[:40]:
is_obj['purpose'] = purpose_val
log_fn(f" Цель: {purpose_val[:80]}")
current_section = 'header'
elif current_section == 'subjects':
if is_checked and 'выбрать' not in field_lower:
subjects_raw.append(field)
if 'количество записей' in field_lower or 'действи' in field_lower:
continue
elif current_section == 'actions':
if is_checked and 'выбрать' not in field_lower and 'защита' not in field_lower[:20]:
actions_raw.append(field)
if 'защита' in field_lower and 'действи' not in field_lower:
current_section = 'defense'
elif current_section == 'defense':
defense_keywords = {
'антивирус': 'Антивирус',
'крипто': 'СКЗИ',
'межсетев': 'Межсетевой экран',
'сзи.*от несанкционирован': 'СЗИ от НСД',
'обнаружен.*вторжен': 'СОВ',
'программные.*средств.*модул': 'Доверенная загрузка',
'доверен.*загрузк': 'Доверенная загрузка',
'гарантирован': 'Доверенная загрузка',
}
matched_label = None
for kw, label in defense_keywords.items():
if re.search(kw, field_lower):
matched_label = label
break
if matched_label and val_b and 'выбрать' not in field_lower:
# Без префикса категории: просто название средства (Kaspersky, КриптоПро)
is_obj['defense_tools'].append(val_b)
elif matched_label:
pass
elif 'куда' in field_lower or 'передаются' in field_lower:
current_section = 'header'
elif 'способ обработки' in field_lower or 'локальную сеть' in field_lower or 'интернет' in field_lower:
current_section = 'header'
if pd_items_raw:
is_obj['personalDataList'] = pd_items_raw
log_fn(f" ПДн: {len(pd_items_raw)}")
if subjects_raw:
is_obj['pd_subjects_list'] = subjects_raw
log_fn(f" Субъектов: {len(subjects_raw)}")
if actions_raw:
is_obj['pd_actions'] = actions_raw
log_fn(f" Действий: {len(actions_raw)}")
if not is_obj['name']: if not is_obj['name']:
is_obj['name'] = sheet_name.strip() is_obj['name'] = sheet_name.strip()
log_fn(f"ИС без названия — использовано имя листа: {is_obj['name']}") log_fn(f"Объект без названия — использовано имя листа: {is_obj['name']}")
# Автоопределение категорий ПДн
pd_cats = []
all_pd_text = ' '.join(p.lower() for p in pd_items_raw)
special_kw = ['состоян.*здоров', 'национальн', 'политическ', 'религиозн', 'философ', 'судимост', 'интимн']
if any(re.search(kw, all_pd_text) for kw in special_kw):
pd_cats.append('специальные')
bio_kw = ['биометрическ', 'изображен.*лиц', 'голос.*человек', 'папилляр', 'дактилоскоп', 'фото.*изображен']
if any(re.search(kw, all_pd_text) for kw in bio_kw):
pd_cats.append('биометрические')
if pd_items_raw:
pd_cats.append('иные')
if pd_cats:
is_obj['personalDataCategory'] = pd_cats
log_fn(f" Категории ПДн: {', '.join(pd_cats)}")
is_list.append(is_obj) is_list.append(is_obj)
log_fn(f"{is_obj['name']} — импортирована") log_fn(f"{is_obj['name']} — импортирован")
if is_list: if is_list:
data['informationSystems'] = is_list data['informationSystems'] = is_list
log_fn(f"\n✅ Всего импортировано ИС: {len(is_list)}") log_fn(f"\n✅ Всего импортировано объектов КИИ: {len(is_list)}")
def _import_187_employees(wb, data, settings, log_fn): def _import_187_employees(wb, data, settings, log_fn):
@@ -497,7 +400,7 @@ def _import_187_employees(wb, data, settings, log_fn):
if 'перечень сотрудников' in a_lower: if 'перечень сотрудников' in a_lower:
in_commission = False in_commission = False
in_employees = True in_employees = True
log_fn(" Секция: сотрудники с доступом к ПДн") log_fn(" Секция: сотрудники с доступом к объектам КИИ")
continue continue
if a_lower in ('№ п/п', '', 'номер', ''): if a_lower in ('№ п/п', '', 'номер', ''):
continue continue
+10 -31
View File
@@ -7,7 +7,7 @@ from typing import List, Dict, Any
@dataclass @dataclass
class CommissionMember: class CommissionMember:
"""Член комиссии по ПДн""" """Член комиссии по категорированию объектов КИИ"""
role: str = "член комиссии" role: str = "член комиссии"
position: str = "" position: str = ""
fio: str = "" fio: str = ""
@@ -15,38 +15,19 @@ class CommissionMember:
@dataclass @dataclass
class InformationSystem: class InformationSystem:
"""Информационная система (обработка ПДн)""" """Объект КИИ (вместо информационной системы 152-ФЗ)"""
name: str = "" name: str = ""
description: str = "" description: str = ""
is_local_network: bool = False is_local_network: bool = False
is_internet: bool = False is_internet: bool = False
defence_level: str = "" category: str = "" # Категория значимости объекта КИИ: 1 / 2 / 3 / без категории
threat_type: str = "3" users_list: List[str] = field(default_factory=list) # Пользователи ОКИИ
category: str = "" defense_tools_list: List[str] = field(default_factory=list) # Средства защиты
pd_list: str = ""
pd_count: str = "менее 100 000"
users: str = ""
defense_tools_list: List[str] = field(default_factory=list)
processing_modes: str = ""
subject_type: str = "работники организации"
pd_actions_list: List[str] = field(default_factory=list)
pd_subjects_list: List[str] = field(default_factory=list)
personal_data_list: List[str] = field(default_factory=list)
users_list: List[str] = field(default_factory=list)
personal_data_category: List[str] = field(default_factory=list)
purpose: str = "" purpose: str = ""
room: str = "" structure: str = "" # Структура объекта КИИ
structure: str = "" # Структура информационной системы
software: str = "" # Программное обеспечение software: str = "" # Программное обеспечение
@dataclass
class PaperDocument:
"""Бумажный документ с ПДн"""
name: str = ""
storage: str = ""
@dataclass @dataclass
class Company: class Company:
"""Организация — основной объект (187-ФЗ)""" """Организация — основной объект (187-ФЗ)"""
@@ -75,6 +56,8 @@ class Company:
ogrn: str = "" ogrn: str = ""
ogrn_date: str = "" ogrn_date: str = ""
kpp: str = "" kpp: str = ""
okved: str = "" # Основной ОКВЭД
okved_extra: List[str] = field(default_factory=list) # Дополнительные ОКВЭД
# Должностные лица # Должностные лица
chief_position: str = "" chief_position: str = ""
@@ -91,7 +74,6 @@ class Company:
# Списки # Списки
commission: List[CommissionMember] = field(default_factory=list) commission: List[CommissionMember] = field(default_factory=list)
information_systems: List[InformationSystem] = field(default_factory=list) information_systems: List[InformationSystem] = field(default_factory=list)
paper_documents_list: List[PaperDocument] = field(default_factory=list)
employees_access: List[Dict[str, str]] = field(default_factory=list) employees_access: List[Dict[str, str]] = field(default_factory=list)
# Наименование структурного подразделения по безопасности # Наименование структурного подразделения по безопасности
@@ -135,13 +117,10 @@ class Company:
InformationSystem(**isys) if isinstance(isys, dict) else isys InformationSystem(**isys) if isinstance(isys, dict) else isys
for isys in d["information_systems"] for isys in d["information_systems"]
] ]
if "paper_documents_list" in d and isinstance(d["paper_documents_list"], list):
d["paper_documents_list"] = [
PaperDocument(**p) if isinstance(p, dict) else p
for p in d["paper_documents_list"]
]
if "employees_access" in d and not isinstance(d["employees_access"], list): if "employees_access" in d and not isinstance(d["employees_access"], list):
d["employees_access"] = [] d["employees_access"] = []
if "okved_extra" in d and not isinstance(d.get("okved_extra"), list):
d["okved_extra"] = []
valid = {f.name for f in cls.__dataclass_fields__.values()} valid = {f.name for f in cls.__dataclass_fields__.values()}
return cls(**{k: v for k, v in d.items() if k in valid}) return cls(**{k: v for k, v in d.items() if k in valid})
+158 -362
View File
@@ -11,7 +11,7 @@ from datetime import datetime
from typing import List, Optional from typing import List, Optional
from . import ( from . import (
Company, CommissionMember, InformationSystem, PaperDocument, Company, CommissionMember, InformationSystem,
inflect_name, inflect_name,
build_replacements, VARIABLE_DEFS, build_replacements, VARIABLE_DEFS,
process_template, process_template,
@@ -157,7 +157,6 @@ class DokoGenApp:
self._build_is_tab() self._build_is_tab()
self._build_commission_tab() self._build_commission_tab()
self._build_users_tab() self._build_users_tab()
self._build_paper_tab()
self._build_generation_tab() self._build_generation_tab()
self._build_log_tab() self._build_log_tab()
@@ -270,6 +269,33 @@ class DokoGenApp:
f2.columnconfigure(1, weight=1) f2.columnconfigure(1, weight=1)
# --- ОКВЭД ---
f_okved = ttk.LabelFrame(left, text="ОКВЭД", padding=10)
f_okved.pack(fill=tk.X, pady=5)
# Основной ОКВЭД — один, из словаря или вручную
ttk.Label(f_okved, text="Основной ОКВЭД:").grid(row=0, column=0, sticky=tk.W, pady=2)
okved_entry = ttk.Entry(f_okved, width=50)
okved_entry.grid(row=0, column=1, padx=10, sticky=tk.EW, pady=1)
self._company_entries['okved'] = okved_entry
ttk.Button(f_okved, text="Выбрать из словаря",
command=self._select_main_okved).grid(row=0, column=2, padx=5)
# Дополнительные ОКВЭД — сколько угодно
ttk.Label(f_okved, text="Дополнительные ОКВЭД:").grid(row=1, column=0, sticky=tk.NW, pady=2)
self.okved_extra_listbox = tk.Listbox(f_okved, height=4, width=50)
self.okved_extra_listbox.grid(row=1, column=1, padx=10, sticky=tk.EW, pady=1)
okved_btn = ttk.Frame(f_okved)
okved_btn.grid(row=1, column=2, padx=5, sticky=tk.N)
ttk.Button(okved_btn, text="Выбрать из словаря",
command=self._select_extra_okved).pack(fill=tk.X, pady=1)
ttk.Button(okved_btn, text="+ Добавить",
command=self._add_okved_manual).pack(fill=tk.X, pady=1)
ttk.Button(okved_btn, text="- Удалить",
command=self._delete_okved_extra).pack(fill=tk.X, pady=1)
f_okved.columnconfigure(1, weight=1)
# --- Должностные лица --- # --- Должностные лица ---
f3 = ttk.LabelFrame(left, text="Должностные лица", padding=10) f3 = ttk.LabelFrame(left, text="Должностные лица", padding=10)
f3.pack(fill=tk.X, pady=5) f3.pack(fill=tk.X, pady=5)
@@ -686,12 +712,12 @@ class DokoGenApp:
# ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ---------- # ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ----------
def _build_is_tab(self): def _build_is_tab(self):
tab = ttk.Frame(self.notebook) tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Информационные системы") self.notebook.add(tab, text="Объекты КИИ")
# Левая панель — список ИС # Левая панель — список объектов КИИ
left = ttk.Frame(tab) left = ttk.Frame(tab)
left.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5) left.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
ttk.Label(left, text="Информационные системы:").pack() ttk.Label(left, text="Объекты КИИ:").pack()
self.is_listbox = tk.Listbox(left, width=35, height=18, exportselection=False) self.is_listbox = tk.Listbox(left, width=35, height=18, exportselection=False)
self.is_listbox.pack(fill=tk.BOTH, expand=True, pady=5) self.is_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
self.is_listbox.bind('<<ListboxSelect>>', self._on_is_select) self.is_listbox.bind('<<ListboxSelect>>', self._on_is_select)
@@ -703,7 +729,7 @@ class DokoGenApp:
ttk.Button(btn_frame, text="- Удалить", command=self._delete_is).pack(side=tk.LEFT, padx=2) ttk.Button(btn_frame, text="- Удалить", command=self._delete_is).pack(side=tk.LEFT, padx=2)
# Правая панель — редактирование # Правая панель — редактирование
self._is_edit_panel = ttk.LabelFrame(tab, text="Редактирование ИС") self._is_edit_panel = ttk.LabelFrame(tab, text="Редактирование объекта КИИ")
self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
fields_frame = ttk.Frame(self._is_edit_panel, padding=10) fields_frame = ttk.Frame(self._is_edit_panel, padding=10)
@@ -726,51 +752,19 @@ class DokoGenApp:
self.is_internet_var = tk.BooleanVar() self.is_internet_var = tk.BooleanVar()
ttk.Checkbutton(chk_frame, text="Интернет", variable=self.is_internet_var).pack(side=tk.LEFT, padx=5) ttk.Checkbutton(chk_frame, text="Интернет", variable=self.is_internet_var).pack(side=tk.LEFT, padx=5)
# Тип угроз # Категория значимости объекта КИИ
self.is_threat_var = tk.StringVar(value="3") self.is_category_var = tk.StringVar(value="без категории")
self.is_threat_var.trace_add("write", lambda *_: self._on_categories_or_count_changed()) ttk.Label(fields_frame, text="Категория значимости:").grid(row=3, column=0, sticky=tk.W, pady=2)
ttk.Label(fields_frame, text="Тип угроз:").grid(row=3, column=0, sticky=tk.W, pady=2) cat_frame = ttk.Frame(fields_frame)
threat_frame = ttk.Frame(fields_frame) cat_frame.grid(row=3, column=1, sticky=tk.W, padx=10)
threat_frame.grid(row=3, column=1, sticky=tk.W, padx=10) for val in ["1", "2", "3", "без категории"]:
for val, text in [("1", "1 (НДВ в СПО)"), ("2", "2 (НДВ в ППО)"), ("3", "3 (НДВ нет)")]: ttk.Radiobutton(cat_frame, text=val, variable=self.is_category_var,
ttk.Radiobutton(threat_frame, text=text, variable=self.is_threat_var,
value=val).pack(side=tk.LEFT, padx=3) value=val).pack(side=tk.LEFT, padx=3)
# Уровень защищённости # Программное обеспечение
self.is_defence_var = tk.StringVar(value="4") ttk.Label(fields_frame, text="Программное обеспечение:").grid(row=4, column=0, sticky=tk.W, pady=2)
ttk.Label(fields_frame, text="Ур. защищённости:").grid(row=4, column=0, sticky=tk.W, pady=2) self.is_software_entry = ttk.Entry(fields_frame, width=55)
def_frame = ttk.Frame(fields_frame) self.is_software_entry.grid(row=4, column=1, padx=10, sticky=tk.EW)
def_frame.grid(row=4, column=1, sticky=tk.W, padx=10)
for lvl in ["1", "2", "3", "4"]:
ttk.Radiobutton(def_frame, text=f"{lvl} ур.",
variable=self.is_defence_var, value=lvl).pack(side=tk.LEFT, padx=3)
# Категории ПДн
ttk.Label(fields_frame, text="Категории ПДн:").grid(row=5, column=0, sticky=tk.NW, pady=2)
cat_frame = ttk.Frame(fields_frame)
cat_frame.grid(row=5, column=1, sticky=tk.W, padx=10)
self.category_vars = []
for cat in ["специальные", "биометрические", "общедоступные", "иные"]:
var = tk.BooleanVar()
var.trace_add('write', lambda *_: self._on_categories_or_count_changed())
cb = ttk.Checkbutton(cat_frame, text=cat, variable=var)
cb.pack(anchor=tk.W)
self.category_vars.append((cat, var))
# Количество записей ПДн (выпадающий список)
ttk.Label(fields_frame, text="Кол-во записей:").grid(row=6, column=0, sticky=tk.W, pady=2)
self.is_pd_count_var = tk.StringVar(value="менее 100 000")
self.is_pd_count_combo = ttk.Combobox(fields_frame, textvariable=self.is_pd_count_var,
values=["менее 100 000", "более 100 000"],
state='readonly', width=28)
self.is_pd_count_combo.grid(row=6, column=1, padx=10, sticky=tk.W)
self.is_pd_count_combo.bind('<<ComboboxSelected>>', lambda e: self._on_categories_or_count_changed())
# Структура ИС
ttk.Label(fields_frame, text="Структура ИС:").grid(row=7, column=0, sticky=tk.W, pady=2)
self.is_structure_entry = ttk.Entry(fields_frame, width=55)
self.is_structure_entry.grid(row=7, column=1, padx=10, sticky=tk.EW)
fields_frame.columnconfigure(1, weight=1) fields_frame.columnconfigure(1, weight=1)
@@ -778,39 +772,6 @@ class DokoGenApp:
nb2 = ttk.Notebook(self._is_edit_panel) nb2 = ttk.Notebook(self._is_edit_panel)
nb2.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) nb2.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Список ПДн
pd_tab = ttk.Frame(nb2)
nb2.add(pd_tab, text="Перечень ПДн")
pd_btn_frame = ttk.Frame(pd_tab)
pd_btn_frame.pack(fill=tk.X)
ttk.Button(pd_btn_frame, text="+ Добавить", command=self._add_pd_item).pack(side=tk.LEFT, padx=2)
ttk.Button(pd_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_listbox)).pack(side=tk.LEFT, padx=2)
ttk.Button(pd_btn_frame, text="Выбрать из списка", command=self._select_from_pd_list).pack(side=tk.LEFT, padx=2)
self.pd_listbox = tk.Listbox(pd_tab, height=6)
self.pd_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Субъекты ПДн
subj_tab = ttk.Frame(nb2)
nb2.add(subj_tab, text="Субъекты ПДн")
subj_btn_frame = ttk.Frame(subj_tab)
subj_btn_frame.pack(fill=tk.X)
ttk.Button(subj_btn_frame, text="+ Добавить", command=self._add_pd_subjects_item).pack(side=tk.LEFT, padx=2)
ttk.Button(subj_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_subjects_listbox)).pack(side=tk.LEFT, padx=2)
ttk.Button(subj_btn_frame, text="Выбрать из списка", command=self._select_from_subjects_list).pack(side=tk.LEFT, padx=2)
self.pd_subjects_listbox = tk.Listbox(subj_tab, height=6)
self.pd_subjects_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Действия с ПДн
act_tab = ttk.Frame(nb2)
nb2.add(act_tab, text="Действия с ПДн")
act_btn_frame = ttk.Frame(act_tab)
act_btn_frame.pack(fill=tk.X)
ttk.Button(act_btn_frame, text="+ Добавить", command=self._add_pd_action_item).pack(side=tk.LEFT, padx=2)
ttk.Button(act_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_actions_listbox)).pack(side=tk.LEFT, padx=2)
ttk.Button(act_btn_frame, text="Выбрать из списка", command=self._select_from_actions_list).pack(side=tk.LEFT, padx=2)
self.pd_actions_listbox = tk.Listbox(act_tab, height=6)
self.pd_actions_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# СЗИ # СЗИ
def_tab = ttk.Frame(nb2) def_tab = ttk.Frame(nb2)
nb2.add(def_tab, text="Средства защиты") nb2.add(def_tab, text="Средства защиты")
@@ -822,9 +783,9 @@ class DokoGenApp:
self.defense_tools_listbox = tk.Listbox(def_tab, height=6) self.defense_tools_listbox = tk.Listbox(def_tab, height=6)
self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Пользователи # Пользователи ОКИИ
user_tab = ttk.Frame(nb2) user_tab = ttk.Frame(nb2)
nb2.add(user_tab, text="Пользователи ИС") nb2.add(user_tab, text="Пользователи ОКИИ")
self.users_listbox = tk.Listbox(user_tab, height=6) self.users_listbox = tk.Listbox(user_tab, height=6)
self.users_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.users_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
btnf = ttk.Frame(user_tab) btnf = ttk.Frame(user_tab)
@@ -886,7 +847,7 @@ class DokoGenApp:
frame = ttk.Frame(scrollable, padding=10) frame = ttk.Frame(scrollable, padding=10)
frame.pack(fill=tk.BOTH, expand=True) frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="Сотрудники, имеющие доступ к ПДн", ttk.Label(frame, text="Сотрудники, имеющие доступ к объектам КИИ",
font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10)) font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10))
# Заголовки # Заголовки
@@ -971,102 +932,6 @@ class DokoGenApp:
if emp['position'] or emp['fio']: if emp['position'] or emp['fio']:
self.model.employees_access.append(emp) self.model.employees_access.append(emp)
# ---------- ВКЛАДКА "БУМАЖНЫЕ ДОКУМЕНТЫ" ----------
def _build_paper_tab(self):
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Бумажные документы")
canvas = tk.Canvas(tab, highlightthickness=0)
scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview)
scrollable = ttk.Frame(canvas)
scrollable.bind("<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=scrollable, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
frame = ttk.Frame(scrollable, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="Бумажные носители ПДн (документы)",
font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10))
# Заголовки
header = ttk.Frame(frame)
header.pack(fill=tk.X)
for w, col in [(45, "Название документа"), (45, "Место хранения")]:
ttk.Label(header, text=col, font=('', 9, 'bold'),
width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2)
self._paper_frame = ttk.Frame(frame)
self._paper_frame.pack(fill=tk.X, pady=5)
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X, pady=5)
ttk.Button(btn_frame, text="+ Добавить документ",
command=self._add_paper_row_ui).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="- Удалить выбранный",
command=self._delete_paper_selected).pack(side=tk.LEFT, padx=2)
self._paper_count_label = ttk.Label(frame, text="Добавлено: 0")
self._paper_count_label.pack(anchor=tk.W, pady=5)
self._paper_entries = []
# Загрузка существующих
for p in self.model.paper_documents_list:
self._add_paper_row(p.name, p.storage)
self._update_paper_count()
def _add_paper_row(self, name="", storage=""):
"""Добавляет одну строку документа."""
row = ttk.Frame(self._paper_frame)
row.pack(fill=tk.X, pady=2)
name_entry = ttk.Entry(row, width=45)
name_entry.insert(0, name)
name_entry.pack(side=tk.LEFT, padx=2)
storage_entry = ttk.Entry(row, width=45)
storage_entry.insert(0, storage)
storage_entry.pack(side=tk.LEFT, padx=2)
del_btn = ttk.Button(row, text="", width=3,
command=lambda: self._delete_paper_row(row))
del_btn.pack(side=tk.LEFT, padx=5)
self._paper_entries.append((row, name_entry, storage_entry))
self._update_paper_count()
def _add_paper_row_ui(self):
self._add_paper_row()
def _delete_paper_row(self, row_frame):
self._paper_entries = [e for e in self._paper_entries if e[0] != row_frame]
row_frame.destroy()
self._update_paper_count()
def _delete_paper_selected(self):
if self._paper_entries:
row_frame = self._paper_entries[-1][0]
self._delete_paper_row(row_frame)
def _update_paper_count(self):
count = len(self._paper_entries)
if hasattr(self, '_paper_count_label'):
self._paper_count_label.config(text=f"Добавлено: {count}")
def _collect_paper_documents(self):
"""Собирает данные бумажных документов из GUI в модель."""
self.model.paper_documents_list.clear()
for row, name_e, storage_e in self._paper_entries:
name = name_e.get().strip()
storage = storage_e.get().strip()
if name or storage:
self.model.paper_documents_list.append(
PaperDocument(name=name, storage=storage)
)
# ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ---------- # ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
def _build_generation_tab(self): def _build_generation_tab(self):
tab = ttk.Frame(self.notebook) tab = ttk.Frame(self.notebook)
@@ -1142,29 +1007,21 @@ class DokoGenApp:
setattr(c, key, entry.get().strip()) setattr(c, key, entry.get().strip())
c.contract_number = self.doc_number_var.get() c.contract_number = self.doc_number_var.get()
c.contract_date = self.doc_date_var.get() c.contract_date = self.doc_date_var.get()
if hasattr(self, 'okved_extra_listbox'):
c.okved_extra = list(self.okved_extra_listbox.get(0, tk.END))
def _collect_is(self): def _collect_is(self):
"""Собирает данные ИС из GUI в модель.""" """Собирает данные объекта КИИ из GUI в модель."""
if self._current_is_idx is not None and self._current_is_idx < len(self.model.information_systems): if self._current_is_idx is not None and self._current_is_idx < len(self.model.information_systems):
isys = self.model.information_systems[self._current_is_idx] isys = self.model.information_systems[self._current_is_idx]
isys.name = self.is_name_entry.get().strip() isys.name = self.is_name_entry.get().strip()
isys.description = self.is_description_entry.get().strip() isys.description = self.is_description_entry.get().strip()
if hasattr(self, 'is_structure_entry'):
isys.structure = self.is_structure_entry.get().strip()
if hasattr(self, 'is_software_entry'): if hasattr(self, 'is_software_entry'):
isys.software = self.is_software_entry.get().strip() isys.software = self.is_software_entry.get().strip()
isys.is_local_network = self.is_lan_var.get() isys.is_local_network = self.is_lan_var.get()
isys.is_internet = self.is_internet_var.get() isys.is_internet = self.is_internet_var.get()
isys.threat_type = self.is_threat_var.get() isys.category = self.is_category_var.get()
isys.defence_level = self.is_defence_var.get()
cats = [c for c, v in self.category_vars if v.get()]
isys.personal_data_category = cats
if hasattr(self, 'is_pd_count_entry'):
isys.pd_count = self.is_pd_count_entry.get().strip() or 'менее 100 000'
# Списки # Списки
isys.personal_data_list = list(self.pd_listbox.get(0, tk.END))
isys.pd_subjects_list = list(self.pd_subjects_listbox.get(0, tk.END))
isys.pd_actions_list = list(self.pd_actions_listbox.get(0, tk.END))
isys.defense_tools_list = list(self.defense_tools_listbox.get(0, tk.END)) isys.defense_tools_list = list(self.defense_tools_listbox.get(0, tk.END))
isys.users_list = list(self.users_listbox.get(0, tk.END)) isys.users_list = list(self.users_listbox.get(0, tk.END))
@@ -1199,6 +1056,12 @@ class DokoGenApp:
self.doc_number_var.set(c.contract_number or "") self.doc_number_var.set(c.contract_number or "")
self.doc_date_var.set(c.contract_date or "") self.doc_date_var.set(c.contract_date or "")
# Дополнительные ОКВЭД
if hasattr(self, 'okved_extra_listbox'):
self.okved_extra_listbox.delete(0, tk.END)
for item in (c.okved_extra or []):
self.okved_extra_listbox.insert(tk.END, item)
# Склонения # Склонения
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6} idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
for code, entry in self.decl_entries.items(): for code, entry in self.decl_entries.items():
@@ -1236,15 +1099,6 @@ class DokoGenApp:
) )
self._update_emp_count() self._update_emp_count()
# Бумажные документы
if hasattr(self, '_paper_entries'):
for row_frame, _, _ in self._paper_entries:
row_frame.destroy()
self._paper_entries.clear()
for p in c.paper_documents_list:
self._add_paper_row(p.name, p.storage)
self._update_paper_count()
# Шаблоны # Шаблоны
self._on_templates_changed() self._on_templates_changed()
@@ -1296,64 +1150,6 @@ class DokoGenApp:
self.log(f"🏙 Город определён: {city}") self.log(f"🏙 Город определён: {city}")
# ==================== АВТОРАСЧЁТ УРОВНЯ ЗАЩИЩЁННОСТИ ==================== # ==================== АВТОРАСЧЁТ УРОВНЯ ЗАЩИЩЁННОСТИ ====================
def _auto_calc_defence_level(self):
"""Автоматический расчёт УЗ по ПП №1119: категории ПДн, тип субъектов,
кол-во записей, тип угроз."""
if self._current_is_idx is None:
return
categories = [cat for cat, var in self.category_vars if var.get()]
pd_count = self.is_pd_count_var.get() if hasattr(self, 'is_pd_count_var') else 'менее 100 000'
threat_type = self.is_threat_var.get() if hasattr(self, 'is_threat_var') else '3'
is_large = 'более' in pd_count.lower()
# Автоопределение типа субъектов: если в Субъектах ПДн выбраны только "Сотрудники" → работники, иначе → иные
subj_items = self.pd_subjects_listbox.get(0, tk.END) if hasattr(self, 'pd_subjects_listbox') else []
has_only_employees = len(subj_items) > 0 and all('сотрудник' in s.lower() for s in subj_items)
is_employees = has_only_employees or len(subj_items) == 0
# Таблица ПП №1119: (категория, тип_субъектов, объём, тип_угроз) → УЗ
def lookup(cat_group, employees, large, threat):
# Общедоступные → всегда 4
if cat_group == 'public':
return 4
# Специальные/биометрические
if cat_group == 'special_bio':
tbl = {
('emp', False, '1'): 2, ('emp', True, '1'): 1,
('other', False, '1'): 1, ('other', True, '1'): 1,
('emp', False, '2'): 2, ('emp', True, '2'): 1,
('other', False, '2'): 2, ('other', True, '2'): 1,
('emp', False, '3'): 3, ('emp', True, '3'): 2,
('other', False, '3'): 3, ('other', True, '3'): 2,
}
else: # иные
tbl = {
('emp', False, '1'): 3, ('emp', True, '1'): 2,
('other', False, '1'): 2, ('other', True, '1'): 1,
('emp', False, '2'): 4, ('emp', True, '2'): 3,
('other', False, '2'): 3, ('other', True, '2'): 2,
('emp', False, '3'): 4, ('emp', True, '3'): 4,
('other', False, '3'): 4, ('other', True, '3'): 3,
}
key = ('emp' if employees else 'other', large, threat)
return tbl.get(key, 4)
# Определяем группу категорий
if 'общедоступные' in categories and not any(c in categories for c in ['специальные', 'биометрические', 'иные']):
cat_group = 'public'
elif any(c in categories for c in ['специальные', 'биометрические']):
cat_group = 'special_bio'
else:
cat_group = 'other'
level = lookup(cat_group, is_employees, is_large, threat_type)
self.is_defence_var.set(str(level))
def _on_categories_or_count_changed(self):
"""Обработчик изменения категорий ПДн или количества записей."""
if self._current_is_idx is not None:
self._auto_calc_defence_level()
# ==================== КОМИССИЯ ==================== # ==================== КОМИССИЯ ====================
def _add_commission_row(self, role="Член комиссии", position="", fio=""): def _add_commission_row(self, role="Член комиссии", position="", fio=""):
row = ttk.Frame(self._comm_members_frame) row = ttk.Frame(self._comm_members_frame)
@@ -1421,28 +1217,15 @@ class DokoGenApp:
self.is_name_entry.insert(0, isys.name or "") self.is_name_entry.insert(0, isys.name or "")
self.is_description_entry.delete(0, tk.END) self.is_description_entry.delete(0, tk.END)
self.is_description_entry.insert(0, isys.description or "") self.is_description_entry.insert(0, isys.description or "")
if hasattr(self, 'is_structure_entry'):
self.is_structure_entry.delete(0, tk.END)
self.is_structure_entry.insert(0, isys.structure or "")
if hasattr(self, 'is_software_entry'): if hasattr(self, 'is_software_entry'):
self.is_software_entry.delete(0, tk.END) self.is_software_entry.delete(0, tk.END)
self.is_software_entry.insert(0, isys.software or "") self.is_software_entry.insert(0, isys.software or "")
self.is_lan_var.set(isys.is_local_network) self.is_lan_var.set(isys.is_local_network)
self.is_internet_var.set(isys.is_internet) self.is_internet_var.set(isys.is_internet)
self.is_threat_var.set(isys.threat_type or "3") self.is_category_var.set(isys.category or "без категории")
self.is_defence_var.set(isys.defence_level or "4")
for cat, var in self.category_vars:
var.set(cat in isys.personal_data_category)
# Заполняем кол-во записей ПДн
if hasattr(self, 'is_pd_count_entry'):
self.is_pd_count_entry.delete(0, tk.END)
self.is_pd_count_entry.insert(0, isys.pd_count or 'менее 100 000')
# Списки # Списки
for lb, items in [ for lb, items in [
(self.pd_listbox, isys.personal_data_list or []),
(self.pd_subjects_listbox, isys.pd_subjects_list or []),
(self.pd_actions_listbox, isys.pd_actions_list or []),
(self.defense_tools_listbox, isys.defense_tools_list or []), (self.defense_tools_listbox, isys.defense_tools_list or []),
(self.users_listbox, isys.users_list or []), (self.users_listbox, isys.users_list or []),
]: ]:
@@ -1471,13 +1254,7 @@ class DokoGenApp:
description=src.description, description=src.description,
is_local_network=src.is_local_network, is_local_network=src.is_local_network,
is_internet=src.is_internet, is_internet=src.is_internet,
defence_level=src.defence_level, category=src.category,
subject_type=src.subject_type,
threat_type=src.threat_type,
personal_data_category=list(src.personal_data_category),
personal_data_list=list(src.personal_data_list),
pd_actions_list=list(src.pd_actions_list),
pd_subjects_list=list(src.pd_subjects_list),
defense_tools_list=list(src.defense_tools_list), defense_tools_list=list(src.defense_tools_list),
users_list=list(src.users_list), users_list=list(src.users_list),
) )
@@ -1497,16 +1274,6 @@ class DokoGenApp:
self.is_listbox.selection_set(0) self.is_listbox.selection_set(0)
self._on_is_select() self._on_is_select()
def _add_pd_item(self):
item = simpledialog.askstring("Добавить ПДн", "Введите персональные данные:")
if item:
self.pd_listbox.insert(tk.END, item)
def _add_pd_subjects_item(self):
item = simpledialog.askstring("Добавить категорию", "Введите категорию субъектов:")
if item:
self.pd_subjects_listbox.insert(tk.END, item)
def _add_defense_item(self): def _add_defense_item(self):
item = simpledialog.askstring("Добавить СЗИ", "Введите средство защиты:") item = simpledialog.askstring("Добавить СЗИ", "Введите средство защиты:")
if item: if item:
@@ -1533,7 +1300,7 @@ class DokoGenApp:
if not names: if not names:
messagebox.showinfo( messagebox.showinfo(
"Выбор пользователей", "Выбор пользователей",
"Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к ПДн) " "Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к объектам КИИ) "
"или добавьте пользователей в другой ИС." "или добавьте пользователей в другой ИС."
) )
return return
@@ -1607,44 +1374,6 @@ class DokoGenApp:
dialog.wait_window() dialog.wait_window()
def _add_pd_action_item(self):
item = simpledialog.askstring("Добавить действие", "Введите действие с ПДн:")
if item:
self.pd_actions_listbox.insert(tk.END, item)
def _select_from_actions_list(self):
"""Выбор действий с ПДн из JSON словаря."""
import json
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_actions.json')
try:
with open(dict_path, 'r', encoding='utf-8') as f:
items = json.load(f)
self._select_from_dialog("Выбор действий с персональными данными", items, self.pd_actions_listbox)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
def _select_from_pd_list(self):
"""Выбор ПДн из JSON словаря."""
import json
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_items.json')
try:
with open(dict_path, 'r', encoding='utf-8') as f:
items = json.load(f)
self._select_from_dialog("Выбор персональных данных", items, self.pd_listbox)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
def _select_from_subjects_list(self):
"""Выбор категорий субъектов из JSON словаря."""
import json
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_categories.json')
try:
with open(dict_path, 'r', encoding='utf-8') as f:
items = json.load(f)
self._select_from_dialog("Выбор категорий субъектов ПДн", items, self.pd_subjects_listbox)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
def _select_from_defense_list(self): def _select_from_defense_list(self):
"""Выбор СЗИ из JSON словаря.""" """Выбор СЗИ из JSON словаря."""
import json import json
@@ -1656,6 +1385,84 @@ class DokoGenApp:
except Exception as e: except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}") messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
# ==================== ОКВЭД ====================
def _load_okved_dict(self):
"""Загружает словарь ОКВЭД (dictionaries/okved.json)."""
import json
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'okved.json')
try:
with open(dict_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь ОКВЭД:\n{e}")
return []
def _select_main_okved(self):
"""Выбор основного ОКВЭД из словаря (всегда один)."""
items = self._load_okved_dict()
if not items:
return
current = self._company_entries.get('okved').get().strip() if 'okved' in self._company_entries else ''
dialog = tk.Toplevel(self.root)
dialog.title("Основной ОКВЭД")
dialog.geometry("500x500")
dialog.transient(self.root)
dialog.grab_set()
frame = ttk.Frame(dialog, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="Выберите основной ОКВЭД (один):",
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
canvas = tk.Canvas(frame, highlightthickness=0)
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
check_frame = ttk.Frame(canvas)
check_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=check_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Радио-кнопки: основной всегда один
var = tk.StringVar(value=current)
for item in items:
ttk.Radiobutton(check_frame, text=item, variable=var,
value=item).pack(anchor=tk.W, pady=1)
btn_frame = ttk.Frame(dialog, padding=10)
btn_frame.pack(fill=tk.X)
def _apply():
val = var.get()
if val:
entry = self._company_entries.get('okved')
if entry:
entry.delete(0, tk.END)
entry.insert(0, val)
dialog.destroy()
ttk.Button(btn_frame, text="✅ Применить", command=_apply).pack(side=tk.RIGHT, padx=2)
ttk.Button(btn_frame, text="Отмена", command=dialog.destroy).pack(side=tk.RIGHT, padx=2)
dialog.wait_window()
def _select_extra_okved(self):
"""Выбор дополнительных ОКВЭД из словаря (можно несколько)."""
items = self._load_okved_dict()
if not items:
return
self._select_from_dialog("Выбор дополнительных ОКВЭД", items, self.okved_extra_listbox)
def _add_okved_manual(self):
"""Ручное добавление дополнительного ОКВЭД."""
item = simpledialog.askstring("Добавить ОКВЭД", "Введите код ОКВЭД:")
if item:
self.okved_extra_listbox.insert(tk.END, item.strip())
def _delete_okved_extra(self):
sel = self.okved_extra_listbox.curselection()
if sel:
self.okved_extra_listbox.delete(sel[0])
# ==================== ШАБЛОНЫ ==================== # ==================== ШАБЛОНЫ ====================
def select_templates(self): def select_templates(self):
folder = filedialog.askdirectory(title="Папка шаблонов") folder = filedialog.askdirectory(title="Папка шаблонов")
@@ -1720,7 +1527,6 @@ class DokoGenApp:
self._collect_commission() self._collect_commission()
self._collect_is() self._collect_is()
self._collect_employees() self._collect_employees()
self._collect_paper_documents()
# Проверка # Проверка
if not self._validate_before_generate(): if not self._validate_before_generate():
@@ -1939,7 +1745,24 @@ class DokoGenApp:
if data.get('ispdnDepartment'): if data.get('ispdnDepartment'):
self.model.security_department = data['ispdnDepartment'] self.model.security_department = data['ispdnDepartment']
# Информационные системы # ОКВЭД (основной и дополнительные)
if data.get('okved'):
entry = self._company_entries.get('okved')
if entry:
entry.delete(0, tk.END)
entry.insert(0, data['okved'])
if data.get('okvedExtra'):
if hasattr(self, 'okved_extra_listbox'):
self.okved_extra_listbox.delete(0, tk.END)
extra = data['okvedExtra']
if isinstance(extra, str):
parts = [p.strip() for p in extra.replace(';', '\n').split('\n') if p.strip()]
else:
parts = [str(p).strip() for p in extra if str(p).strip()]
for p in parts:
self.okved_extra_listbox.insert(tk.END, p)
# Объекты КИИ
if data.get('informationSystems'): if data.get('informationSystems'):
self.model.information_systems.clear() self.model.information_systems.clear()
for is_data in data['informationSystems']: for is_data in data['informationSystems']:
@@ -1948,17 +1771,12 @@ class DokoGenApp:
description=is_data.get('description', ''), description=is_data.get('description', ''),
is_local_network=is_data.get('is_has_lan', is_data.get('is_local_network', False)), is_local_network=is_data.get('is_has_lan', is_data.get('is_local_network', False)),
is_internet=is_data.get('isInternet', False), is_internet=is_data.get('isInternet', False),
pd_count=is_data.get('personalDataCount', 'менее 100 000'), category=is_data.get('category', ''),
personal_data_list=is_data.get('personalDataList', []),
pd_subjects_list=is_data.get('pd_subjects_list', []),
defense_tools_list=is_data.get('defense_tools', []), defense_tools_list=is_data.get('defense_tools', []),
users_list=is_data.get('usersList', []), users_list=is_data.get('usersList', []),
personal_data_category=is_data.get('personalDataCategory', []), purpose=is_data.get('purpose', is_data.get('description', '')),
processing_modes=is_data.get('processing_mode', ''),
purpose=is_data.get('description', ''),
room=is_data.get('room', ''),
structure=is_data.get('structure', ''), structure=is_data.get('structure', ''),
pd_actions_list=is_data.get('pd_actions', []), software=is_data.get('software', ''),
) )
self.model.information_systems.append(isys) self.model.information_systems.append(isys)
self._refresh_is_listbox() self._refresh_is_listbox()
@@ -2005,26 +1823,6 @@ class DokoGenApp:
) )
self._update_emp_count() self._update_emp_count()
# Бумажные документы (из «Названия документов» / «Место хранения»)
if data.get('paperDocuments'):
self.model.paper_documents_list.clear()
names = [n.strip() for n in data['paperDocuments'].replace('\n', ';').split(';') if n.strip()]
storages = [s.strip() for s in (data.get('paperStorage') or '').replace('\n', ';').split(';') if s.strip()]
for i, name in enumerate(names):
storage = storages[i] if i < len(storages) else (storages[0] if storages else '')
self.model.paper_documents_list.append(
PaperDocument(name=name, storage=storage)
)
# Обновляем GUI
if hasattr(self, '_paper_entries'):
for row_frame, _, _ in self._paper_entries:
row_frame.destroy()
self._paper_entries.clear()
for p in self.model.paper_documents_list:
self._add_paper_row(p.name, p.storage)
self._update_paper_count()
self.log(f" Бумажные документы: {len(self.model.paper_documents_list)} шт.")
self.log("✅ Импорт применён") self.log("✅ Импорт применён")
self.root.after(0, _apply) self.root.after(0, _apply)
@@ -2069,7 +1867,6 @@ class DokoGenApp:
self._collect_commission() self._collect_commission()
self._collect_is() self._collect_is()
self._collect_employees() self._collect_employees()
self._collect_paper_documents()
try: try:
with open(filename, 'w', encoding='utf-8') as f: with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2) json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
@@ -2095,7 +1892,6 @@ class DokoGenApp:
self._collect_commission() self._collect_commission()
self._collect_is() self._collect_is()
self._collect_employees() self._collect_employees()
self._collect_paper_documents()
try: try:
os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True) os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True)
with open(self._autosave_path, 'w', encoding='utf-8') as f: with open(self._autosave_path, 'w', encoding='utf-8') as f:
+30 -91
View File
@@ -57,7 +57,8 @@ VARIABLE_DEFS = [
('{{company_ogrn_date}}', 'A', 'Дата ОГРН', 'company:ogrn_date'), ('{{company_ogrn_date}}', 'A', 'Дата ОГРН', 'company:ogrn_date'),
('{{company_kpp}}', 'A', 'КПП', 'company:kpp'), ('{{company_kpp}}', 'A', 'КПП', 'company:kpp'),
('{{security_department}}', 'A', 'Наименование структурного подразделения по безопасности', 'company:security_department'), ('{{security_department}}', 'A', 'Наименование структурного подразделения по безопасности', 'company:security_department'),
('{{item.pd_actions}}', 'B', 'Действия с ПДн (в цикле ИС)', 'item:pd_actions'), ('{{company_okved}}', 'A', 'Основной ОКВЭД', 'company:okved'),
('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'),
# === Комиссия === # === Комиссия ===
('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'), ('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'),
@@ -69,22 +70,15 @@ VARIABLE_DEFS = [
('{{document_number}}', 'A', 'Номер документа', 'doc_number'), ('{{document_number}}', 'A', 'Номер документа', 'doc_number'),
# === 187-ФЗ === # === 187-ФЗ ===
('{{is_security_requirements}}', 'B', 'Требования уровня защищённости', 'security_reqs'), ('{{is_security_requirements}}', 'B', 'Требования к защите объекта КИИ', 'security_reqs'),
('{{is_list}}', 'B', 'Список всех ИС (нумерованный)', 'is_list'), ('{{is_list}}', 'B', 'Список всех объектов КИИ (нумерованный)', 'is_list'),
('{{is_name}}', 'B', 'Наименование первой ИС', 'is:name'), ('{{is_name}}', 'B', 'Наименование первого объекта КИИ', 'is:name'),
('{{name}}', 'B', 'Название ИС (алиас)', 'is:name'), ('{{name}}', 'B', 'Название объекта КИИ (алиас)', 'is:name'),
('{{description}}', 'B', 'Описание ИС (алиас)', 'is:description'), ('{{description}}', 'B', 'Описание объекта КИИ (алиас)', 'is:description'),
('{{category}}', 'B', 'Категория ИС (алиас)', 'is:category'), ('{{category}}', 'B', 'Категория значимости объекта КИИ (алиас)', 'is:category'),
('{{number}}', 'B', 'Номер (алиас)', 'doc_number'), ('{{number}}', 'B', 'Номер (алиас)', 'doc_number'),
('{{pd_count}}', 'B', 'Количество записей ПДн (алиас)', 'is:pd_count'), ('{{users}}', 'B', 'Пользователи ОКИИ (алиас)', 'is:users'),
('{{pd_list}}', 'B', 'Список ПДн (алиас)', 'is:pd_list'),
('{{pd_subjects}}', 'B', 'Субъекты ПДн (алиас)', 'is:pd_subjects'),
('{{threat_type}}', 'B', 'Тип угроз (алиас)', 'is:threat_type'),
('{{users}}', 'B', 'Пользователи ИС (алиас)', 'is:users'),
('{{defence_level}}', 'B', 'Уровень защищённости (алиас)', 'is:defence_level'),
('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'), ('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'),
('{{document}}', 'A', 'Название бумажного документа (алиас)', 'company:paper_documents'),
('{{storage}}', 'A', 'Место хранения (алиас)', 'company:paper_storage'),
('{{responsible_fio_accs}}', 'A', 'ФИО ответственного (вин.п.)', 'fio_acl:responsible'), ('{{responsible_fio_accs}}', 'A', 'ФИО ответственного (вин.п.)', 'fio_acl:responsible'),
('{{commission1}}', 'A', 'Должность члена комиссии 1', 'commission_member:0:position'), ('{{commission1}}', 'A', 'Должность члена комиссии 1', 'commission_member:0:position'),
('{{commission1_fio}}', 'A', 'ФИО члена комиссии 1', 'commission_member:0:fio'), ('{{commission1_fio}}', 'A', 'ФИО члена комиссии 1', 'commission_member:0:fio'),
@@ -221,13 +215,11 @@ def _add_is_indexed_vars(replacements: Dict[str, str], isys, idx: int):
'is_address': getattr(isys, 'address', '') or '', 'is_address': getattr(isys, 'address', '') or '',
'is_description': isys.description or '', 'is_description': isys.description or '',
'is_has_internet': 'да' if isys.is_internet else 'нет', 'is_has_internet': 'да' if isys.is_internet else 'нет',
'is_defence_level': str(isys.defence_level or ''), 'is_category': isys.category or '',
'is_category': ', '.join(isys.personal_data_category) or isys.category or '',
'is_pd_list': ', '.join(isys.personal_data_list or []) or '',
'is_users': ', '.join(isys.users_list or []) or '', 'is_users': ', '.join(isys.users_list or []) or '',
'is_pd_count': str(isys.pd_count or ''),
'is_purpose': isys.purpose or '', 'is_purpose': isys.purpose or '',
'is_structure': getattr(isys, 'structure', '') or '', 'is_structure': getattr(isys, 'structure', '') or '',
'is_software': getattr(isys, 'software', '') or '',
} }
for f, v in fields.items(): for f, v in fields.items():
replacements[f'{{{{{f}_{idx}}}}}'] = str(v) replacements[f'{{{{{f}_{idx}}}}}'] = str(v)
@@ -295,68 +287,16 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
'address': getattr(isys, 'address', '') or '', 'address': getattr(isys, 'address', '') or '',
'is_has_lan': 'Да' if isys.is_local_network else 'Нет', 'is_has_lan': 'Да' if isys.is_local_network else 'Нет',
'has_internet': 'да' if isys.is_internet else 'нет', 'has_internet': 'да' if isys.is_internet else 'нет',
'defence_level': str(isys.defence_level or ''), 'category': isys.category or '',
'threat_type': str(isys.threat_type or '3'),
'category': ', '.join(isys.personal_data_category) or isys.category or '',
'pd_list': ', '.join(isys.personal_data_list or []) or '',
'pd_count': str(isys.pd_count or ''),
'users': ', '.join(isys.users_list or []) or '', 'users': ', '.join(isys.users_list or []) or '',
'users_loop': users_loop, 'users_loop': users_loop,
'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '', 'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '',
'processing_modes': isys.processing_modes or '',
'pd_subjects': '; '.join(isys.pd_subjects_list or []) or '',
'is_purpose': isys.purpose or '', 'is_purpose': isys.purpose or '',
'structure': getattr(isys, 'structure', '') or '', 'structure': getattr(isys, 'structure', '') or '',
'software': getattr(isys, 'software', '') or '', 'software': getattr(isys, 'software', '') or '',
'pd_actions': '; '.join(isys.pd_actions_list or []) or '',
}) })
loops['information_systems'] = items loops['information_systems'] = items
# Бумажные документы
def _storage_with_prep(storage: str) -> str:
"""«сейф» → «в сейфе» (предлог + предложный падеж).
Если уже начинается с предлога оставить как есть."""
if not storage:
return ''
s = storage.strip()
low = s.lower()
if low.startswith(('в ', 'на ', 'под ', 'за ', 'у ', 'при ', 'из ', 'со ', 'во ', 'над ', 'перед ')):
return s
words = s.split()
out = []
# Исключения предложного падежа: «в шкафу», «в углу» (не «в шкафе»)
loct_exceptions = {'шкаф': 'шкафу', 'угол': 'углу', 'край': 'краю', 'рот': 'рту', 'мост': 'мосту'}
for w in words:
core = w.strip('.,;')
if not core or core.isdigit() or core.startswith(''):
out.append(w)
continue
if core.lower() in loct_exceptions:
out.append(loct_exceptions[core.lower()])
continue
try:
p = dc.morph.parse(core)[0]
inf = p.inflect({'loct'})
if inf:
out.append(inf.word)
continue
except Exception:
pass
out.append(w)
return 'в ' + ' '.join(out)
paper_list = getattr(company, 'paper_documents_list', None) or []
if paper_list:
items = []
for p in paper_list:
storage = p.storage or ''
items.append({
'document': p.name or '',
'storage': _storage_with_prep(storage), # «в сейфе»
'storage_raw': storage, # «сейф» (как введено)
})
loops['paper_documents'] = items
# Сотрудники с доступом # Сотрудники с доступом
employees = getattr(company, 'employees_access', None) or [] employees = getattr(company, 'employees_access', None) or []
if employees: if employees:
@@ -448,11 +388,6 @@ def _resolve(source: str, ctx: Dict) -> str:
'name': 'name', 'name': 'name',
'description': 'description', 'description': 'description',
'category': 'category', 'category': 'category',
'defence_level': 'defence_level',
'threat_type': 'threat_type',
'pd_list': lambda: ', '.join(first.personal_data_list or []),
'pd_count': 'pd_count',
'pd_subjects': lambda: '; '.join(first.pd_subjects_list or []),
'users': lambda: ', '.join(first.users_list or []), 'users': lambda: ', '.join(first.users_list or []),
} }
if field in field_map: if field in field_map:
@@ -501,21 +436,25 @@ def _get_fio(company: Company, who: str) -> str:
def _get_security_text(ctx: Dict) -> str: def _get_security_text(ctx: Dict) -> str:
"""Возвращает текст требований для уровня защищённости.""" """Возвращает текст требований к защите объекта КИИ по категории значимости."""
defence_level = ctx.get('extended_fields', {}).get('defence_level', '4')
is_list = ctx.get('is_list', []) is_list = ctx.get('is_list', [])
if not defence_level and is_list: category = ''
defence_level = str(getattr(is_list[0], 'defence_level', '4') or '4') if is_list:
dl = str(defence_level).strip()[0] category = str(getattr(is_list[0], 'category', '') or '').strip()
TEXTS = { TEXTS = {
'1': """Для обеспечения 1-го уровня защищённости...""", '1': """Для значимого объекта КИИ 1-й категории значимости необходимо:
'2': """Для обеспечения 2-го уровня защищённости...""", создание системы безопасности значимого объекта КИИ;
'3': """Для обеспечения 3-го уровня защищённости...""", обеспечение защиты информации от компьютерных атак;
'4': """Для обеспечения 4-го уровня защищённости персональных данных необходимо: категорирование объектов КИИ и контроль его результатов.""",
режим безопасности помещений; '2': """Для значимого объекта КИИ 2-й категории значимости необходимо:
сохранность носителей ПДн; создание системы безопасности значимого объекта КИИ;
утверждение перечня лиц с доступом к ПДн; обеспечение защиты информации от компьютерных атак.""",
использование сертифицированных СЗИ.""", '3': """Для значимого объекта КИИ 3-й категории значимости необходимо:
обеспечение защиты информации от компьютерных атак.""",
} }
return TEXTS.get(dl, TEXTS['4']) if category in TEXTS:
return TEXTS[category]
return """Для объекта КИИ, не являющегося значимым, необходимо:
принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ."""
+32 -2
View File
@@ -65,6 +65,17 @@ _ADDRESS_ABBR = {
'СТ': 'ст.', 'Ш': 'ш.', 'Р-Н': 'р-н.', 'Б-Р': 'б-р.', 'МКР': 'мкр.', 'СТ': 'ст.', 'Ш': 'ш.', 'Р-Н': 'р-н.', 'Б-Р': 'б-р.', 'МКР': 'мкр.',
'НАБ': 'наб.', 'ТУП': 'туп.', 'ПР-Д': 'пр-д.', 'КРП': 'крп.', 'НАБ': 'наб.', 'ТУП': 'туп.', 'ПР-Д': 'пр-д.', 'КРП': 'крп.',
'Г.': 'г.', 'УЛ.': 'ул.', 'Д.': 'д.', 'Р-Н.': 'р-н.', 'Г.': 'г.', 'УЛ.': 'ул.', 'Д.': 'д.', 'Р-Н.': 'р-н.',
'Г.О': 'г.о.', 'Г.О.': 'г.о.', 'ГО': 'г.о.', 'М.О': 'м.о.', 'М.О.': 'м.о.',
}
# Слова, которые в адресах пишутся со строчной буквы (кроме случаев,
# когда стоят первым словом после точки — «г. Электроугли» и т.п.)
_ADDRESS_LOWERCASE = {
'область', 'край', 'республика', 'округ', 'район', 'город', 'поселок',
'посёлок', 'деревня', 'село', 'станица', 'хутор', 'аул', 'улица',
'проспект', 'переулок', 'бульвар', 'шоссе', 'набережная', 'площадь',
'проезд', 'тупик', 'аллея', 'линия', 'дом', 'строение', 'корпус',
'квартира', 'офис', 'помещение', 'этаж', 'комната', 'участок',
} }
@@ -105,7 +116,12 @@ def _normalize_text(text):
def normalize_address(text): def normalize_address(text):
"""«Г Крымск» → «г. Крымск», «Р-Н» → «р-н.» и т.п.""" """«Г Крымск» → «г. Крымск», «Р-Н» → «р-н.», «МОСКОВСКАЯ ОБЛАСТЬ» → «Московская область».
Приводит адрес к нормальному регистру: каждое слово с заглавной буквы,
адресные сокращения (г., ул., д., обл., г.о.) и административные термины
(область, район, город, дом и т.п.) со строчной.
"""
if not text: if not text:
return '' return ''
parts = re.split(r'(\s+|[,\;])', text) parts = re.split(r'(\s+|[,\;])', text)
@@ -115,7 +131,21 @@ def normalize_address(text):
out.append(_ADDRESS_ABBR[p.strip().upper()]) out.append(_ADDRESS_ABBR[p.strip().upper()])
else: else:
out.append(p) out.append(p)
return ''.join(out) res = ''.join(out)
# КАПС → нормальный регистр по словам
res = _normalize_text(res)
# Адресные термины — со строчной буквы (сохраняя пунктуацию)
words = res.split(' ')
fixed = []
for w in words:
m = re.match(r'^([^\w]*)([\w\-]+)([^\w]*)$', w, re.UNICODE)
if m:
pre, core, post = m.group(1), m.group(2), m.group(3)
if core.lower() in _ADDRESS_LOWERCASE:
fixed.append(pre + core.lower() + post)
continue
fixed.append(w)
return ' '.join(fixed)
def _fmt_date(value): def _fmt_date(value):