Compare commits
20
Commits
614044a6ca
...
source
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
691b167725 | ||
|
|
a7878804e4 | ||
|
|
8e97e10714 | ||
|
|
dbf8c613dd | ||
|
|
b2b68258fd | ||
|
|
1840f1e2d4 | ||
|
|
e15610ba29 | ||
|
|
36d2d5a87b | ||
|
|
c90660a0f8 | ||
|
|
5bb6a2f5e9 | ||
|
|
829a1a98e3 | ||
|
|
ad54d3e0e2 | ||
|
|
9f8bf3999d | ||
|
|
07bf25ebc3 | ||
|
|
0b3ef66125 | ||
|
|
1793182735 | ||
|
|
52b4ee6f1a | ||
|
|
c9dffdd4ca | ||
|
|
136c58c948 | ||
|
|
28fa7edaa8 |
@@ -16,20 +16,27 @@
|
||||
],
|
||||
"Наука": [
|
||||
"1",
|
||||
"11",
|
||||
"13",
|
||||
"7",
|
||||
"8",
|
||||
"9"
|
||||
"9",
|
||||
"11",
|
||||
"13"
|
||||
],
|
||||
"Транспорт": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"11"
|
||||
"8",
|
||||
"9",
|
||||
"11",
|
||||
"13",
|
||||
"14"
|
||||
],
|
||||
"Связь": [
|
||||
"4"
|
||||
"4",
|
||||
"6",
|
||||
"8",
|
||||
"9"
|
||||
],
|
||||
"Энергетика": [
|
||||
"1",
|
||||
@@ -85,20 +92,26 @@
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"13",
|
||||
"14"
|
||||
"13"
|
||||
],
|
||||
"Оборонная промышленность": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"11",
|
||||
"12",
|
||||
"13"
|
||||
],
|
||||
"Ракетно-космическая промышленность": [
|
||||
|
||||
+2770
-267
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,17 @@
|
||||
[
|
||||
"Установление контролируемой зоны",
|
||||
"Контроль физического доступа к объекту",
|
||||
"Разработка документов (регламентов, инструкций, руководств) по обеспечению безопасности объекта",
|
||||
"Назначение ответственных за обеспечение безопасности объекта",
|
||||
"Обучение и инструктаж персонала",
|
||||
"Регламентация действий при компьютерных инцидентах",
|
||||
"Утверждение перечня лиц, имеющих доступ к объекту"
|
||||
]
|
||||
"Установлены границы контролируемой зоны.",
|
||||
"Физическая охрана периметра обеспечена бетонным забором с колючей проволокой, исключающим прямой доступ к зданиям, и турникетом на проходной; въезд и вход осуществляются по пропускам с применением СКУД, ведется учет посетителей.",
|
||||
"Имеется система видеонаблюдения.",
|
||||
"Здания не имеют окон, выходящих во вне, входная группа оснащена металлической дверью.",
|
||||
"Установлены охранно-пожарная сигнализация и тревожная кнопка.",
|
||||
"Исключена возможность неконтролируемого пребывания посторонних лиц в помещениях, где ведется работа с объектами критической информационной инфраструктуры.",
|
||||
"Назначены: ответственный за организацию обеспечения безопасности объектов критической информационной инфраструктуры, администратор и администратор безопасности объектов критической информационной инфраструктуры, утверждены инструкции, регламентирующие работу с данными и объектами критической информационной инфраструктуры.",
|
||||
"Осуществляется внутренний контроль соответствия обеспечения безопасности объектов критической информационной инфраструктуры установленным требованиям.",
|
||||
"Работники, непосредственно осуществляющие работу с объектами критической информационной инфраструктуры, ознакомлены с положениями законодательства Российской Федерации об объектах критической информационной инфраструктуры, документами, определяющими политику организации в отношении работы с объектами критической информационной инфраструктуры.",
|
||||
"Обеспечивается учет машинных носителей объектов критической информационной инфраструктуры.",
|
||||
"Обеспечивается резервирование и восстановление данных, модифицированных или уничтоженных вследствие несанкционированного доступа к ним.",
|
||||
"Разработаны правила доступа к данным, обрабатываемым в объектах критической информационной инфраструктуры.",
|
||||
"Определён перечень сотрудников, осуществляющих работу с объектами критической информационной инфраструктуры.",
|
||||
"Ведётся учёт всех защищаемых носителей информации с помощью их маркировки и занесения учетных данных в журнал учета с отметкой об их выдаче (приеме).",
|
||||
"Обеспечено раздельное хранение материальных носителей, обработка которых осуществляется в различных целях."
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). По категории значимости объекта КИИ, актуальным угрозам и уже выбранным средствам защиты информации (СЗИ) определи, каких средств не хватает, и дай рекомендации. Учитывай требования по категориям: для I категории — максимальный набор (антивирус, СКЗИ, SIEM, межсетевые экраны, средства анти-APT и т.д.), для II — расширенный набор, для III — базовый набор. Выбирай СЗИ ТОЛЬКО из реально существующих классов средств защиты (антивирусная защита, СКЗИ, межсетевой экран, SIEM, DLP, IDS/IPS, средства доверенной загрузки, контроля целостности, управления доступом и т.п.).",
|
||||
"system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). По категории значимости объекта КИИ, характеристикам объекта, актуальным угрозам и уже выбранным средствам защиты информации (СЗИ) определи, каких средств не хватает, и дай рекомендации. Обязательно учитывай следующее:\n1) КАТЕГОРИЯ ЗНАЧИМОСТИ: если категория «без категории», «—», пусто или не присвоена — объект НЕ является значимым объектом КИИ, полный набор СЗИ по приказу № 239 для него НЕ требуется. В этом случае рекомендую только минимальный базовый набор (антивирусная защита, резервное копирование, управление доступом) и главный совет — провести категорирование объекта. НЕ перечисляй средства для I категории (СКЗИ, SIEM, анти-APT, доверенная загрузка и т.п.), если категория не присвоена. Для I категории — максимальный набор, для II — расширенный, для III — базовый набор по приказу № 239.\n2) ХАРАКТЕРИСТИКИ ОБЪЕКТА: внимательно читай переданные характеристики (наличие интернета, архитектура, ПО, тип объекта). НЕ рекомендуй СЗИ для технологий, которых нет у объекта: нет интернета/внешних сетей → не рекомендуй DDoS-защиту, VPN, веб-защиту, защиту от удалённых атак из интернета; нет виртуализации/контейнеров → не рекомендуй защиту виртуальной инфраструктуры и контейнеров; нет мобильных устройств → не рекомендуй защиту мобильных; объект не АСУ ТП → не рекомендуй защиту АСУ ТП; нет гостайны → не рекомендуй средства для гостайны. Отмечай только реально применимые классы СЗИ.\n3) ОБЪЁМ: максимум 5–7 рекомендаций, только самые важные для данной категории и объекта. Если текущий набор СЗИ достаточен — верни пустой список рекомендаций и «достаточно»: true.\nВыбирай СЗИ ТОЛЬКО из реально существующих классов средств защиты (антивирусная защита, СКЗИ, межсетевой экран, SIEM, DLP, IDS/IPS, средства доверенной загрузки, контроля целостности, управления доступом и т.п.).",
|
||||
"response_format": "Верни ТОЛЬКО JSON: {\"рекомендации\": [{\"сзи\": \"название класса/средства\", \"зачем\": \"для чего нужно\"}], \"общие_советы\": [\"...\"], \"достаточно\": true|false}. Поле «достаточно» — true, если текущий набор СЗИ покрывает требования категории. Не придумывай конкретные торговые марки, если их нет в списке — указывай класс средства."
|
||||
}
|
||||
|
||||
+10
-4
@@ -310,10 +310,14 @@ def _subst_var_xml(xml: str, var: str, val: str) -> str:
|
||||
+ '</w:r><w:r><w:t xml:space="preserve">')
|
||||
|
||||
|
||||
def _expand_nested_loops(template: str, item: Dict) -> str:
|
||||
def _expand_nested_loops(template: str, item: Dict, loops: Dict = None) -> str:
|
||||
"""Разворачивает вложенные циклы вида <!-- loop:users_loop --> ... <!-- loop_end -->
|
||||
данными из item (список словарей). Используется для циклов внутри цикла ИС,
|
||||
например пользователи ИС. Переменные внутри: {{user_fio}}, {{user_position}}."""
|
||||
данными из item (список словарей) или из общего словаря циклов loops.
|
||||
Используется для циклов внутри цикла ИС, например пользователи ИС,
|
||||
а также для вложенных таблиц (показатели значимости внутри объекта КИИ).
|
||||
Переменные внутри: {{user_fio}}, {{user_position}}, {{item.*}}."""
|
||||
if loops is None:
|
||||
loops = {}
|
||||
result = template
|
||||
for _ in range(20):
|
||||
m_start = LOOP_START.search(result)
|
||||
@@ -321,6 +325,8 @@ def _expand_nested_loops(template: str, item: Dict) -> str:
|
||||
break
|
||||
key = m_start.group(1)
|
||||
data = item.get(key)
|
||||
if not isinstance(data, list):
|
||||
data = loops.get(key)
|
||||
if not isinstance(data, list):
|
||||
# Нет данных для вложенного цикла — удаляем блок целиком
|
||||
depth = 1
|
||||
@@ -478,7 +484,7 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
|
||||
clone = template_clean
|
||||
# Вложенные циклы (users_loop и т.п.) — разворачиваем ДО подстановки
|
||||
# обычных полей, чтобы {{user_fio}} внутри них не затёрлись алиасами
|
||||
clone = _expand_nested_loops(clone, item)
|
||||
clone = _expand_nested_loops(clone, item, loops)
|
||||
for field, value in item.items():
|
||||
if isinstance(value, list):
|
||||
continue # списки — данные вложенных циклов, не подставляем
|
||||
|
||||
+435
-44
@@ -7,48 +7,439 @@
|
||||
],
|
||||
"value_column": 1,
|
||||
"fields": [
|
||||
{"name": "Полное наименование организации", "var": "fullName", "cell": "", "keywords": "полное наименование организации"},
|
||||
{"name": "Сокращённое наименование организации", "var": "shortName", "cell": "", "keywords": "сокращенное наименование организации"},
|
||||
{"name": "Юридический адрес", "var": "addressLegal", "cell": "", "keywords": "адрес организации \\(юридический\\)"},
|
||||
{"name": "Фактический адрес", "var": "addressActual", "cell": "", "keywords": "адрес организации \\(фактический\\)"},
|
||||
{"name": "Электронная почта", "var": "email", "cell": "", "keywords": "электронная почта"},
|
||||
{"name": "Телефон", "var": "phone", "cell": "", "keywords": "телефон"},
|
||||
{"name": "ИНН", "var": "inn", "cell": "", "keywords": "инн"},
|
||||
{"name": "ОГРН", "var": "ogrn", "cell": "", "keywords": "огрн"},
|
||||
{"name": "Дата получения ОГРН", "var": "ogrnDate", "cell": "", "keywords": "дата получения огрн|дата.*огрн"},
|
||||
{"name": "КПП", "var": "kpp", "cell": "", "keywords": "кпп"},
|
||||
{"name": "Основной ОКВЭД", "var": "okved", "cell": "", "keywords": "основной оквэд"},
|
||||
{"name": "Дополнительные ОКВЭД", "var": "okvedExtra", "cell": "", "keywords": "дополнительные оквэд"},
|
||||
{"name": "Должность руководителя", "var": "chiefPosition", "cell": "", "keywords": "должность руководителя"},
|
||||
{"name": "ФИО руководителя", "var": "chiefFio", "cell": "", "keywords": "фио руководителя"},
|
||||
{"name": "Должность ответственного лица (безопасность ЗОКИИ)", "var": "responsiblePosition", "cell": "", "keywords": "должность ответственного лица"},
|
||||
{"name": "ФИО ответственного лица (безопасность ЗОКИИ)", "var": "responsibleFio", "cell": "", "keywords": "фио ответственного лица"},
|
||||
{"name": "Подразделение по безопасности ОКИИ", "var": "ispdnDepartment", "cell": "", "keywords": "структур.*подраздел"},
|
||||
{"name": "Рабочий телефон ответственного", "var": "ispdnPhone", "cell": "", "keywords": "рабочий телефон ответственного|мобильный телефон ответственного"},
|
||||
{"name": "Электронная почта ответственного", "var": "ispdnEmail", "cell": "", "keywords": "электронная почта ответственного"},
|
||||
{"name": "Должность специалиста по ЗИ/администратора", "var": "administratorPosition", "cell": "", "keywords": "должность специалиста по защите информации|должность.*администрат"},
|
||||
{"name": "ФИО специалиста по ЗИ/администратора", "var": "administratorFio", "cell": "", "keywords": "фио специалиста по защите информации|фио.*администрат"},
|
||||
{"name": "Право владения зданием", "var": "buildingRight", "cell": "", "keywords": "право владения зданием"},
|
||||
{"name": "Сумма прибыли за год", "var": "profit", "cell": "", "keywords": "сумма прибыли"},
|
||||
{"name": "Сумма налога с прибыли", "var": "profitTax", "cell": "", "keywords": "сумма налога с прибыли"},
|
||||
{"name": "Доменное имя и внешний IP", "var": "domainName", "cell": "", "keywords": "доменное имя и внешний сетевой адрес"},
|
||||
{"name": "Сайт организации", "var": "site_name", "cell": "", "keywords": "сайт организации"},
|
||||
{"name": "Должность ответственного за сайт", "var": "site_responsible_position", "cell": "", "keywords": "должность ответственного за сайт"},
|
||||
{"name": "ФИО ответственного за сайт", "var": "site_responsible_fio", "cell": "", "keywords": "фио ответственного за сайт"},
|
||||
{"name": "Разработчик сайта", "var": "site_service_provider", "cell": "", "keywords": "организации, которая разрабатывала и размещала корпоративный сайт"},
|
||||
{"name": "ИНН разработчика сайта", "var": "site_service_provider_inn", "cell": "", "keywords": "инн организации, которая разрабатывала"},
|
||||
{"name": "Размещение сайта", "var": "site_hosting", "cell": "", "keywords": "размещение сайта"},
|
||||
{"name": "Адрес расположения серверов", "var": "site_hosting_address", "cell": "", "keywords": "адрес расположения серверов"},
|
||||
{"name": "Облачная платформа", "var": "cloud_platform", "cell": "", "keywords": "облачную платформу"},
|
||||
{"name": "Аренда выделенного сервера", "var": "cloud_dedicated_server", "cell": "", "keywords": "выделенный сервер"},
|
||||
{"name": "Аренда виртуального ЦОД", "var": "cloud_vdc", "cell": "", "keywords": "виртуальный дата-центр"},
|
||||
{"name": "Серверы в дата-центре", "var": "cloud_datacenter", "cell": "", "keywords": "сервера в дата-центре"},
|
||||
{"name": "Средства контейнеризации", "var": "cloud_container", "cell": "", "keywords": "контейнеризации"},
|
||||
{"name": "Собственная серверная (ЦОД)", "var": "cloud_own_server", "cell": "", "keywords": "собственная серверная"},
|
||||
{"name": "Общее количество компьютеров", "var": "computers_total", "cell": "", "keywords": "общее количество компьютеров"},
|
||||
{"name": "Компьютеры, подключённые к объектам КИИ", "var": "computers_kii", "cell": "", "keywords": "компьютеров подключенных к объектам кии"},
|
||||
{"name": "Центр мониторинга", "var": "monitoring_center", "cell": "", "keywords": "к какому центру подключены"},
|
||||
{"name": "Детали подключения к центру мониторинга", "var": "monitoring_details", "cell": "", "keywords": "детали подключения"},
|
||||
{"name": "Дополнительная информация", "var": "additionalInfo", "cell": "", "keywords": "дополнительная информация"}
|
||||
{
|
||||
"name": "Полное наименование организации",
|
||||
"var": "fullName",
|
||||
"cell": "",
|
||||
"keywords": "полное наименование организации"
|
||||
},
|
||||
{
|
||||
"name": "Сокращённое наименование организации",
|
||||
"var": "shortName",
|
||||
"cell": "",
|
||||
"keywords": "сокращенное наименование организации"
|
||||
},
|
||||
{
|
||||
"name": "Юридический адрес",
|
||||
"var": "addressLegal",
|
||||
"cell": "",
|
||||
"keywords": "адрес организации \\(юридический\\)"
|
||||
},
|
||||
{
|
||||
"name": "Фактический адрес",
|
||||
"var": "addressActual",
|
||||
"cell": "",
|
||||
"keywords": "адрес организации \\(фактический\\)"
|
||||
},
|
||||
{
|
||||
"name": "Электронная почта",
|
||||
"var": "email",
|
||||
"cell": "",
|
||||
"keywords": "электронная почта"
|
||||
},
|
||||
{
|
||||
"name": "Телефон",
|
||||
"var": "phone",
|
||||
"cell": "",
|
||||
"keywords": "телефон"
|
||||
},
|
||||
{
|
||||
"name": "ИНН",
|
||||
"var": "inn",
|
||||
"cell": "",
|
||||
"keywords": "инн"
|
||||
},
|
||||
{
|
||||
"name": "ОГРН",
|
||||
"var": "ogrn",
|
||||
"cell": "",
|
||||
"keywords": "огрн"
|
||||
},
|
||||
{
|
||||
"name": "Дата получения ОГРН",
|
||||
"var": "ogrnDate",
|
||||
"cell": "",
|
||||
"keywords": "дата получения огрн|дата.*огрн"
|
||||
},
|
||||
{
|
||||
"name": "КПП",
|
||||
"var": "kpp",
|
||||
"cell": "",
|
||||
"keywords": "кпп"
|
||||
},
|
||||
{
|
||||
"name": "Основной ОКВЭД",
|
||||
"var": "okved",
|
||||
"cell": "",
|
||||
"keywords": "основной оквэд"
|
||||
},
|
||||
{
|
||||
"name": "Дополнительные ОКВЭД",
|
||||
"var": "okvedExtra",
|
||||
"cell": "",
|
||||
"keywords": "дополнительные оквэд"
|
||||
},
|
||||
{
|
||||
"name": "Должность руководителя",
|
||||
"var": "chiefPosition",
|
||||
"cell": "",
|
||||
"keywords": "должность руководителя"
|
||||
},
|
||||
{
|
||||
"name": "ФИО руководителя",
|
||||
"var": "chiefFio",
|
||||
"cell": "",
|
||||
"keywords": "фио руководителя"
|
||||
},
|
||||
{
|
||||
"name": "Должность ответственного лица (безопасность ЗОКИИ)",
|
||||
"var": "responsiblePosition",
|
||||
"cell": "",
|
||||
"keywords": "должность ответственного лица"
|
||||
},
|
||||
{
|
||||
"name": "ФИО ответственного лица (безопасность ЗОКИИ)",
|
||||
"var": "responsibleFio",
|
||||
"cell": "",
|
||||
"keywords": "фио ответственного лица"
|
||||
},
|
||||
{
|
||||
"name": "Подразделение по безопасности ОКИИ",
|
||||
"var": "ispdnDepartment",
|
||||
"cell": "",
|
||||
"keywords": "структур.*подраздел"
|
||||
},
|
||||
{
|
||||
"name": "Рабочий телефон ответственного",
|
||||
"var": "ispdnPhone",
|
||||
"cell": "",
|
||||
"keywords": "рабочий телефон ответственного|мобильный телефон ответственного"
|
||||
},
|
||||
{
|
||||
"name": "Электронная почта ответственного",
|
||||
"var": "ispdnEmail",
|
||||
"cell": "",
|
||||
"keywords": "электронная почта ответственного"
|
||||
},
|
||||
{
|
||||
"name": "Должность специалиста по ЗИ/администратора",
|
||||
"var": "administratorPosition",
|
||||
"cell": "",
|
||||
"keywords": "должность специалиста по защите информации|должность.*администрат"
|
||||
},
|
||||
{
|
||||
"name": "ФИО специалиста по ЗИ/администратора",
|
||||
"var": "administratorFio",
|
||||
"cell": "",
|
||||
"keywords": "фио специалиста по защите информации|фио.*администрат"
|
||||
},
|
||||
{
|
||||
"name": "Право владения зданием",
|
||||
"var": "buildingRight",
|
||||
"cell": "",
|
||||
"keywords": "право владения зданием"
|
||||
},
|
||||
{
|
||||
"name": "Сумма прибыли за год",
|
||||
"var": "profit",
|
||||
"cell": "",
|
||||
"keywords": "сумма прибыли"
|
||||
},
|
||||
{
|
||||
"name": "Сумма налога с прибыли",
|
||||
"var": "profitTax",
|
||||
"cell": "",
|
||||
"keywords": "сумма налога с прибыли"
|
||||
},
|
||||
{
|
||||
"name": "Доменное имя и внешний IP",
|
||||
"var": "domainName",
|
||||
"cell": "",
|
||||
"keywords": "доменное имя и внешний сетевой адрес"
|
||||
},
|
||||
{
|
||||
"name": "Сайт организации",
|
||||
"var": "site_name",
|
||||
"cell": "",
|
||||
"keywords": "сайт организации"
|
||||
},
|
||||
{
|
||||
"name": "Должность ответственного за сайт",
|
||||
"var": "site_responsible_position",
|
||||
"cell": "",
|
||||
"keywords": "должность ответственного за сайт"
|
||||
},
|
||||
{
|
||||
"name": "ФИО ответственного за сайт",
|
||||
"var": "site_responsible_fio",
|
||||
"cell": "",
|
||||
"keywords": "фио ответственного за сайт"
|
||||
},
|
||||
{
|
||||
"name": "Разработчик сайта",
|
||||
"var": "site_service_provider",
|
||||
"cell": "",
|
||||
"keywords": "организации, которая разрабатывала и размещала корпоративный сайт"
|
||||
},
|
||||
{
|
||||
"name": "ИНН разработчика сайта",
|
||||
"var": "site_service_provider_inn",
|
||||
"cell": "",
|
||||
"keywords": "инн организации, которая разрабатывала"
|
||||
},
|
||||
{
|
||||
"name": "Размещение сайта",
|
||||
"var": "site_hosting",
|
||||
"cell": "",
|
||||
"keywords": "размещение сайта"
|
||||
},
|
||||
{
|
||||
"name": "Адрес расположения серверов",
|
||||
"var": "site_hosting_address",
|
||||
"cell": "",
|
||||
"keywords": "адрес расположения серверов"
|
||||
},
|
||||
{
|
||||
"name": "Облачная платформа",
|
||||
"var": "cloud_platform",
|
||||
"cell": "",
|
||||
"keywords": "облачную платформу"
|
||||
},
|
||||
{
|
||||
"name": "Аренда выделенного сервера",
|
||||
"var": "cloud_dedicated_server",
|
||||
"cell": "",
|
||||
"keywords": "выделенный сервер"
|
||||
},
|
||||
{
|
||||
"name": "Аренда виртуального ЦОД",
|
||||
"var": "cloud_vdc",
|
||||
"cell": "",
|
||||
"keywords": "виртуальный дата-центр"
|
||||
},
|
||||
{
|
||||
"name": "Серверы в дата-центре",
|
||||
"var": "cloud_datacenter",
|
||||
"cell": "",
|
||||
"keywords": "сервера в дата-центре"
|
||||
},
|
||||
{
|
||||
"name": "Средства контейнеризации",
|
||||
"var": "cloud_container",
|
||||
"cell": "",
|
||||
"keywords": "контейнеризации"
|
||||
},
|
||||
{
|
||||
"name": "Собственная серверная (ЦОД)",
|
||||
"var": "cloud_own_server",
|
||||
"cell": "",
|
||||
"keywords": "собственная серверная"
|
||||
},
|
||||
{
|
||||
"name": "Общее количество компьютеров",
|
||||
"var": "computers_total",
|
||||
"cell": "",
|
||||
"keywords": "общее количество компьютеров"
|
||||
},
|
||||
{
|
||||
"name": "Компьютеры, подключённые к объектам КИИ",
|
||||
"var": "computers_kii",
|
||||
"cell": "",
|
||||
"keywords": "компьютеров подключенных к объектам кии"
|
||||
},
|
||||
{
|
||||
"name": "Центр мониторинга",
|
||||
"var": "monitoring_center",
|
||||
"cell": "",
|
||||
"keywords": "к какому центру подключены"
|
||||
},
|
||||
{
|
||||
"name": "Детали подключения к центру мониторинга",
|
||||
"var": "monitoring_details",
|
||||
"cell": "",
|
||||
"keywords": "детали подключения"
|
||||
},
|
||||
{
|
||||
"name": "Дополнительная информация",
|
||||
"var": "additionalInfo",
|
||||
"cell": "",
|
||||
"keywords": "дополнительная информация"
|
||||
},
|
||||
{
|
||||
"name": "Физическая охрана (ЧОП, сторожа, охранники)",
|
||||
"var": "physicalGuard",
|
||||
"cell": "",
|
||||
"keywords": "физическая охрана"
|
||||
},
|
||||
{
|
||||
"name": "Пожарная сигнализация",
|
||||
"var": "physicalFireAlarm",
|
||||
"cell": "",
|
||||
"keywords": "пожарная сигнализация"
|
||||
},
|
||||
{
|
||||
"name": "Тревожная кнопка",
|
||||
"var": "physicalPanicButton",
|
||||
"cell": "",
|
||||
"keywords": "тревожная кнопка"
|
||||
},
|
||||
{
|
||||
"name": "Решётки на окнах",
|
||||
"var": "physicalBars",
|
||||
"cell": "",
|
||||
"keywords": "решетки на окнах"
|
||||
},
|
||||
{
|
||||
"name": "Металлические двери",
|
||||
"var": "physicalMetalDoors",
|
||||
"cell": "",
|
||||
"keywords": "металлические двери"
|
||||
},
|
||||
{
|
||||
"name": "Система видеонаблюдения",
|
||||
"var": "physicalVideoSurveillance",
|
||||
"cell": "",
|
||||
"keywords": "система видеонаблюдения"
|
||||
},
|
||||
{
|
||||
"name": "Видеодомофон",
|
||||
"var": "physicalVideoIntercom",
|
||||
"cell": "",
|
||||
"keywords": "видеодомофон"
|
||||
},
|
||||
{
|
||||
"name": "Турникет на входе",
|
||||
"var": "physicalTurnstile",
|
||||
"cell": "",
|
||||
"keywords": "турникет на входе"
|
||||
},
|
||||
{
|
||||
"name": "Журнал учёта посетителей",
|
||||
"var": "physicalVisitorLog",
|
||||
"cell": "",
|
||||
"keywords": "журнал учета посетителей|журнал учёта посетителей"
|
||||
}
|
||||
],
|
||||
"criterion_fields": [
|
||||
{
|
||||
"num": "1",
|
||||
"name": "Максимальное число пострадавших людей",
|
||||
"keywords": "пострадавш",
|
||||
"unit": "человек"
|
||||
},
|
||||
{
|
||||
"num": "2б",
|
||||
"name": "Люди с нарушенными условиями жизнедеятельности",
|
||||
"keywords": "условия жизнедеятельности",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "3б",
|
||||
"name": "Люди, для которых недоступны транспортные услуги",
|
||||
"keywords": "транспортн",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "4а",
|
||||
"name": "Абоненты, для которых недоступны услуги связи",
|
||||
"keywords": "абонент",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "5а",
|
||||
"name": "Максимальное время недоступности государственной услуги",
|
||||
"keywords": "недоступна государственная услуга|недоступности государственной",
|
||||
"unit": "часов"
|
||||
},
|
||||
{
|
||||
"num": "5б",
|
||||
"name": "Время, в течение которого госуслуга не может быть оказана",
|
||||
"keywords": "регламентом|административным регламентом",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"num": "11б",
|
||||
"name": "Люди, подверженные вредным воздействиям на окружающую среду",
|
||||
"keywords": "вредным воздействиям",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "13а",
|
||||
"name": "Снижение объёма продукции ГОЗ",
|
||||
"keywords": "объем продукции|объём продукции",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"num": "13б",
|
||||
"name": "Увеличение времени изготовления единицы продукции ГОЗ",
|
||||
"keywords": "время изготовления",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"num": "14",
|
||||
"name": "Максимальное время недоступности ИС (оборона)",
|
||||
"keywords": "недоступна пользователю",
|
||||
"unit": "часов"
|
||||
},
|
||||
{
|
||||
"num": "2а",
|
||||
"name": "Территория нарушения жизнеобеспечения населения",
|
||||
"keywords": "территория.*жизнедеятельности|жизнедеятельности населения",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "3а",
|
||||
"name": "Территория нарушения транспортного сообщения",
|
||||
"keywords": "территория.*транспорт",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "3в",
|
||||
"name": "Типы нарушенных перевозок и грузов",
|
||||
"keywords": "типам нарушенных перевозок|типы нарушенных перевозок",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "3г",
|
||||
"name": "Категория объекта транспортной инфраструктуры",
|
||||
"keywords": "категорированию объектов транспортной|категорию объекта транспортной",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "4б",
|
||||
"name": "Уровень органа власти, для которого недоступны услуги связи",
|
||||
"keywords": "уровню органа государственной власти",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "6",
|
||||
"name": "Уровень государственного органа (невыполнение функции)",
|
||||
"keywords": "функционирования государственного органа|государственного органа в части",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "7",
|
||||
"name": "Уровень международного договора",
|
||||
"keywords": "международного договора",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "11а",
|
||||
"name": "Территория вредных воздействий на окружающую среду",
|
||||
"keywords": "территория.*окружающую среду|окружающая среда может",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "12",
|
||||
"name": "Уровень пункта управления (ситуационного центра)",
|
||||
"keywords": "пункта управления|ситуационного центра",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "13.1",
|
||||
"name": "Статус в кооперации головного исполнителя ГОЗ",
|
||||
"keywords": "кооперации головного исполнителя|статусу в кооперации",
|
||||
"unit": "вариант"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,21 @@ def import_187fz(filepath: str, log_fn: Optional[Callable] = None, settings_path
|
||||
finally:
|
||||
wb.close()
|
||||
|
||||
# Ответы раздела «Физическая безопасность» (да/нет) — собираем в словарь,
|
||||
# чтобы текст организационных мер генерировался с учётом реальных средств.
|
||||
ps_keys = [
|
||||
'physicalGuard', 'physicalFireAlarm', 'physicalPanicButton',
|
||||
'physicalBars', 'physicalMetalDoors', 'physicalVideoSurveillance',
|
||||
'physicalVideoIntercom', 'physicalTurnstile', 'physicalVisitorLog',
|
||||
]
|
||||
ps = {}
|
||||
for k in ps_keys:
|
||||
v = str(data.get(k) or '').strip()
|
||||
if v:
|
||||
ps[k] = v
|
||||
if ps:
|
||||
data['physicalSecurity'] = ps
|
||||
|
||||
# Копирование полей ИСПДн в админа, если админ не заполнен
|
||||
for src, dst in [('ispdnFio', 'administratorFio'), ('ispdnFio', 'adminFio'),
|
||||
('ispdnPosition', 'administratorPosition'), ('ispdnPosition', 'adminPosition'),
|
||||
@@ -541,6 +556,17 @@ def _import_187_is_list(wb, data, settings, log_fn):
|
||||
if _set(field_lower, 'tech_measures', val_b):
|
||||
log_fn(f" Тех. меры: {val_b[:60]}")
|
||||
continue
|
||||
# --- Показатели критериев значимости (ПП-127) → criterion_input ---
|
||||
# Строки вида «Показатель 1. …» / «Показатель 2а. …» / «Показатель 13.1. …»
|
||||
m_crit = re.match(r'^показател[ья]?\s*(\d+(?:\.\d+)?)([а-яА-Я]?)', field_lower)
|
||||
if m_crit:
|
||||
_num_crit = m_crit.group(1) + m_crit.group(2)
|
||||
# нормализация: «10.1» → «10.1», «13.1» → «13.1», «2а» → «2а»
|
||||
_num_crit = _num_crit.replace(' .', '.').strip()
|
||||
if val_b and 'выбрать' not in field_lower and not _is_hint(val_b):
|
||||
is_obj.setdefault('criterion_input', {})[_num_crit] = str(val_b).strip()
|
||||
log_fn(f" Показатель {_num_crit}: {str(val_b)[:60]}")
|
||||
continue
|
||||
if 'интернет' in field_lower:
|
||||
is_obj['isInternet'] = is_checked
|
||||
continue
|
||||
|
||||
+33
-1
@@ -80,6 +80,12 @@ class InformationSystem:
|
||||
org_measures: str = "" # Организационные меры (\n — разделитель)
|
||||
tech_measures: str = "" # Технические меры (\n — разделитель)
|
||||
|
||||
# Введённые значения показателей ПП-127 для авторасчёта: {num: значение}
|
||||
# num — «1», «2а», «2б», …, «14»; значение — число (количественные)
|
||||
# или выбранный вариант (качественные). Заполняется окном авторасчёта,
|
||||
# импортом из опросника или вручную; используется в _build_categorization_indicators.
|
||||
criterion_input: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Company:
|
||||
@@ -130,7 +136,9 @@ class Company:
|
||||
profit_tax: str = "" # Сумма налога с прибыли за предыдущий год
|
||||
|
||||
# Показатель 8 (ущерб субъекту КИИ): доходы за 5 лет и суммарный ущерб
|
||||
income_year_1: str = "" # Доход за 5 лет назад (руб.)
|
||||
# В форме — одна строка «Доход за 5 лет (суммарно)»; по годам разбивает сам оператор.
|
||||
income_5y: str = "" # Доход за 5 лет суммарно (руб.)
|
||||
income_year_1: str = "" # Доход за 5 лет назад (руб.) — обратная совместимость
|
||||
income_year_2: str = "" # Доход за 4 года назад (руб.)
|
||||
income_year_3: str = "" # Доход за 3 года назад (руб.)
|
||||
income_year_4: str = "" # Доход за 2 года назад (руб.)
|
||||
@@ -144,6 +152,11 @@ class Company:
|
||||
vso: str = "" # Снижение выплат сторонним организациям ΔВСО (руб.)
|
||||
ns: str = "" # Снижение сборов ΔНС (руб.)
|
||||
|
||||
# Показатель 10 (прекращение финансовых операций): операции за год (млн ед.)
|
||||
ops_count: str = "" # Количество операций за прошедший год (млн ед.)
|
||||
calendar_days: str = "" # Количество календарных дней в году (по умолчанию 365)
|
||||
criterion10_value: str = "" # Значение показателя 10.1–10.7 (в своих единицах)
|
||||
|
||||
# Списки
|
||||
commission: List[CommissionMember] = field(default_factory=list)
|
||||
information_systems: List[InformationSystem] = field(default_factory=list)
|
||||
@@ -152,6 +165,11 @@ class Company:
|
||||
# Наименование структурного подразделения по безопасности
|
||||
security_department: str = ""
|
||||
|
||||
# Ответы раздела «Физическая безопасность» опросника (да/нет):
|
||||
# охрана, пожарная сигнализация, тревожная кнопка, решётки, металлические двери,
|
||||
# видеонаблюдение, видеодомофон, турникет, журнал посетителей.
|
||||
physical_security: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def update_declensions(self, inflect_func):
|
||||
"""Обновить склонения полного названия"""
|
||||
if self.full_name:
|
||||
@@ -214,6 +232,20 @@ class Company:
|
||||
d["okved_extra"] = []
|
||||
valid = {f.name for f in cls.__dataclass_fields__.values()}
|
||||
obj = cls(**{k: v for k, v in d.items() if k in valid})
|
||||
# Миграция: старые файлы с доходами по годам → суммарный доход за 5 лет
|
||||
if not (getattr(obj, 'income_5y', '') or '').strip():
|
||||
_sum = 0.0
|
||||
_has = False
|
||||
for i in range(1, 6):
|
||||
v = str(getattr(obj, f'income_year_{i}', '') or '').strip()
|
||||
if v:
|
||||
try:
|
||||
_sum += float(v.replace(' ', '').replace(',', '.').replace('\u00a0', ''))
|
||||
_has = True
|
||||
except Exception:
|
||||
pass
|
||||
if _has:
|
||||
obj.income_5y = f"{_sum:,.0f}".replace(',', ' ')
|
||||
# Если в поле ФИО несколько человек — оставить одного (старые опросники)
|
||||
for fio_field in ('chief_fio', 'responsible_fio', 'admin_fio'):
|
||||
val = getattr(obj, fio_field, '')
|
||||
|
||||
+102
-16
@@ -11,6 +11,24 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def _ubi_num(text):
|
||||
"""Код УБИ из строки → int: «УБИ.069 Название» → 69. None, если кода нет."""
|
||||
m = re.search(r'уби\.?\s*(\d+)', str(text), re.IGNORECASE)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
def sort_threats_by_ubi(items):
|
||||
"""Сортировка угроз по возрастанию кода УБИ (УБИ.001, УБИ.002, …).
|
||||
|
||||
Строки без кода УБИ уходят в конец (по алфавиту).
|
||||
"""
|
||||
def _key(t):
|
||||
n = _ubi_num(t)
|
||||
return (0, n) if n is not None else (1, str(t).lower())
|
||||
return sorted(items, key=_key)
|
||||
|
||||
|
||||
def load_threats(dict_dir=None):
|
||||
@@ -42,8 +60,26 @@ BASE_KEYWORDS = [
|
||||
'вредоносн', 'несанкционированн', 'утечк', 'перехват', 'модификац',
|
||||
'уничтожен', 'отказ в обслуживании', 'нсд', 'биос', 'пароль',
|
||||
'криптографическ', 'шифрован', 'аутентификац',
|
||||
# базовые угрозы локальных сетей / клиент-серверных архитектур
|
||||
'доступ', 'данн', 'систем', 'конфигурац', 'целостност', 'информац',
|
||||
'программн', 'пользовател', 'авторизац', 'сеанс', 'журнал', 'реестр',
|
||||
'файл', 'память', 'буфер', 'ресурс', 'сервер', 'клиент', 'учётн',
|
||||
'учетн', 'идентификац', 'привилег', 'средств', 'сетев', 'локальн',
|
||||
]
|
||||
|
||||
# Технологии, которые по умолчанию НЕ используются на объекте.
|
||||
# Если в характеристиках объекта есть явные упоминания — группа возвращается.
|
||||
TECH_GROUPS = {
|
||||
'виртуализац': ['виртуал', 'гипервизор', 'контейнер'],
|
||||
'облако': ['облачн', 'облак'],
|
||||
'мобильн': ['мобильн', 'смартфон', 'планшет'],
|
||||
'грид': ['грид', 'суперкомпьютер'],
|
||||
'большие данные': ['больших данных', 'хранилищ'],
|
||||
'беспроводн': ['беспроводн', 'wi-fi', 'wifi'],
|
||||
'smart-карты': ['smart-карт', 'смарт-карт'],
|
||||
'машинное обучение': ['машинного обучения', 'искусственного интеллекта', 'нейросет'],
|
||||
}
|
||||
|
||||
# Объект имеет подключение к сети Интернет / сеть электросвязи
|
||||
INTERNET_KEYWORDS = [
|
||||
'удалённ', 'удаленн', 'сетев', 'фишинг', 'ddos', 'отказ в обслуживании',
|
||||
@@ -153,6 +189,21 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
||||
is_asu = _is_asu(isys)
|
||||
is_network = _is_network(isys)
|
||||
|
||||
# --- Технологии, которые явно НЕ используются на объекте ---
|
||||
# Если в характеристиках объекта (описание, архитектура, ПО, сфера) нет
|
||||
# упоминаний технологии — угрозы этой группы исключаем.
|
||||
tech_context = ' '.join([
|
||||
str(getattr(isys, 'description', '') or ''),
|
||||
str(getattr(isys, 'architecture', '') or ''),
|
||||
str(getattr(isys, 'hw_sw', '') or ''),
|
||||
str(getattr(isys, 'os_sw', '') or ''),
|
||||
str(getattr(isys, 'app_sw', '') or ''),
|
||||
str(getattr(isys, 'object_type', '') or ''),
|
||||
str(getattr(isys, 'sphere', '') or ''),
|
||||
str(getattr(isys, 'net_category', '') or ''),
|
||||
str(getattr(isys, 'typical_object', '') or ''),
|
||||
]).lower()
|
||||
|
||||
# --- Слова-исключения (если встретились в названии угрозы — она неактуальна) ---
|
||||
exclude = []
|
||||
|
||||
@@ -167,6 +218,19 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
||||
if is_asu:
|
||||
# АСУ: нет BIOS/виртуализации/браузера в привычном понимании
|
||||
exclude += ['биос', 'грид', 'гипервизор', 'виртуальн', 'браузер']
|
||||
else:
|
||||
# Не АСУ — исключаем угрозы АСУ ТП / технологических процессов
|
||||
exclude += ['технологическ', 'датчик', 'исполнительн', 'асутп', 'scada',
|
||||
'промышленн', 'контроллер', 'числовым программным управлением',
|
||||
'программируемых логических']
|
||||
|
||||
# Технологии, не упомянутые в характеристиках объекта → исключаем
|
||||
for tech_name, kws in TECH_GROUPS.items():
|
||||
if any(kw in tech_context for kw in kws):
|
||||
continue # технология используется — угрозы оставляем
|
||||
# Технология не упомянута — исключаем её угрозы
|
||||
for kw in kws:
|
||||
exclude.append(kw)
|
||||
|
||||
if not is_network and not is_asu:
|
||||
# Обычная ИС: неактуальны чисто сетевые/телеком-угрозы без указания
|
||||
@@ -198,9 +262,9 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
||||
sc = sum(1 for kw in pos if kw in tl)
|
||||
scored.append((sc, t))
|
||||
|
||||
# Сортируем по релевантности (сначала — с наибольшим совпадением)
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
result = [t for sc, t in scored if sc > 0]
|
||||
# Сортируем по возрастанию кода УБИ (УБИ.001 → УБИ.211…)
|
||||
scored = [t for sc, t in scored if sc > 0]
|
||||
result = sort_threats_by_ubi(scored)
|
||||
|
||||
if limit and len(result) > limit:
|
||||
result = result[:limit]
|
||||
@@ -234,19 +298,18 @@ BASE_ORG_MEASURES = [
|
||||
'Утверждение перечня лиц, имеющих доступ к объекту',
|
||||
]
|
||||
|
||||
# Базовый набор тех. мер
|
||||
# Базовый набор тех. мер — только универсальные, не требующие конкретных СЗИ.
|
||||
# Меры, зависящие от средств защиты (антивирус, СЗИ от НСД, регистрация событий
|
||||
# и т.п.), добавляются отдельно по факту наличия СЗИ (DEFENSE_TOOL_MEASURES).
|
||||
BASE_TECH_MEASURES = [
|
||||
'Идентификация и аутентификация пользователей',
|
||||
'Управление доступом',
|
||||
'Ограничение программной среды',
|
||||
'Антивирусная защита',
|
||||
'Защита информации от несанкционированного доступа (СЗИ от НСД)',
|
||||
'Регистрация и мониторинг событий безопасности',
|
||||
'Резервное копирование и восстановление',
|
||||
'Защита технических средств и линий связи',
|
||||
]
|
||||
|
||||
# Средства защиты → доп. тех. меры
|
||||
# Средства защиты → доп. тех. меры (по факту наличия СЗИ у объекта)
|
||||
DEFENSE_TOOL_MEASURES = [
|
||||
('межсетев', 'Межсетевое экранирование'),
|
||||
('firewall', 'Межсетевое экранирование'),
|
||||
@@ -260,6 +323,23 @@ DEFENSE_TOOL_MEASURES = [
|
||||
('виртуализ', 'Защита среды виртуализации'),
|
||||
('vmware', 'Защита среды виртуализации'),
|
||||
('hyper-v', 'Защита среды виртуализации'),
|
||||
('антивирус', 'Антивирусная защита'),
|
||||
('kaspersky', 'Антивирусная защита'),
|
||||
('eset', 'Антивирусная защита'),
|
||||
('dr.web', 'Антивирусная защита'),
|
||||
('аваст', 'Антивирусная защита'),
|
||||
('avast', 'Антивирусная защита'),
|
||||
('сзи', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('от нсд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('dallas', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('secret net', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('аккорд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('siem', 'Регистрация и мониторинг событий безопасности'),
|
||||
('dlp', 'Регистрация и мониторинг событий безопасности'),
|
||||
('мониторинг', 'Регистрация и мониторинг событий безопасности'),
|
||||
('регистрац', 'Регистрация и мониторинг событий безопасности'),
|
||||
('доверенная загрузка', 'Контроль (доверенная) загрузки'),
|
||||
('резервн', 'Резервное копирование и восстановление'),
|
||||
]
|
||||
|
||||
|
||||
@@ -331,15 +411,21 @@ def auto_fill_security(isys, threats=None):
|
||||
if not (getattr(isys, 'org_measures', '') or '').strip():
|
||||
res['org_measures'] = '\n'.join(BASE_ORG_MEASURES)
|
||||
|
||||
# 7. Тех. меры — базовые + по СЗИ объекта
|
||||
# 7. Тех. меры — базовые + по СЗИ объекта.
|
||||
# Только если объекту присвоена категория значимости (1/2/3):
|
||||
# для незначимого объекта технические меры не требуются.
|
||||
if not (getattr(isys, 'tech_measures', '') or '').strip():
|
||||
tech = list(BASE_TECH_MEASURES)
|
||||
defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
|
||||
dlow = defense.lower()
|
||||
for kw, measure in DEFENSE_TOOL_MEASURES:
|
||||
if kw in dlow and measure not in tech:
|
||||
tech.append(measure)
|
||||
res['tech_measures'] = '\n'.join(tech)
|
||||
cat = (getattr(isys, 'category', '') or '').strip()
|
||||
has_category = cat in ('1', '2', '3')
|
||||
if has_category:
|
||||
tech = list(BASE_TECH_MEASURES)
|
||||
defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
|
||||
dlow = defense.lower()
|
||||
for kw, measure in DEFENSE_TOOL_MEASURES:
|
||||
if kw in dlow and measure not in tech:
|
||||
tech.append(measure)
|
||||
res['tech_measures'] = '\n'.join(tech)
|
||||
# else: категория не присвоена — технические меры не заполняем
|
||||
|
||||
return res
|
||||
|
||||
|
||||
+485
-71
@@ -22,7 +22,7 @@ from . import (
|
||||
from .verify import (
|
||||
check_declension_morpher, check_company_dadata,
|
||||
check_declension_ai, check_company_ai,
|
||||
COMPANY_FIELDS, compare_company_values,
|
||||
COMPANY_FIELDS, compare_company_values, normalize_threats,
|
||||
)
|
||||
from .api_settings import load_api_settings, save_api_settings
|
||||
|
||||
@@ -345,19 +345,12 @@ class DokoGenApp:
|
||||
entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
|
||||
self._company_entries[key] = entry
|
||||
|
||||
# Показатель 8 (ущерб субъекту КИИ): доходы за 5 лет + суммарный ущерб
|
||||
ttk.Label(f_fin, text="Доходы за 5 лет (показатель 8):",
|
||||
font=('', 9, 'bold')).grid(row=len(fin_fields), column=0, columnspan=2,
|
||||
sticky=tk.W, pady=(8, 2))
|
||||
# Показатель 8 (ущерб субъекту КИИ): доход за 5 лет (суммарно) + суммарный ущерб
|
||||
ttk.Button(f_fin, text="🌐 Загрузить доходы из DaData (по ИНН)",
|
||||
command=self._load_incomes_from_dadata).grid(
|
||||
row=len(fin_fields), column=2, padx=5, sticky=tk.W)
|
||||
income_fields = [
|
||||
("Доход за 5 лет назад (руб.):", "income_year_1"),
|
||||
("Доход за 4 года назад (руб.):", "income_year_2"),
|
||||
("Доход за 3 года назад (руб.):", "income_year_3"),
|
||||
("Доход за 2 года назад (руб.):", "income_year_4"),
|
||||
("Доход за прошлый год (руб.):", "income_year_5"),
|
||||
("Доход за 5 лет (суммарно, руб.):", "income_5y"),
|
||||
("Снижение уровня дохода от нарушения критических процессов (руб.):", "income_loss"),
|
||||
]
|
||||
for j, (label, key) in enumerate(income_fields):
|
||||
@@ -761,8 +754,9 @@ class DokoGenApp:
|
||||
self._company_entries['okved'].insert(0, resolve_okved(comp['okved'], _tok3))
|
||||
if want('okved_extra') and comp.get('okved_extra') and hasattr(self, 'okved_extra_listbox'):
|
||||
self.okved_extra_listbox.delete(0, tk.END)
|
||||
from .verify import split_okved_extra
|
||||
_applied = 0
|
||||
for code in comp['okved_extra']:
|
||||
for code in split_okved_extra(comp['okved_extra']):
|
||||
if str(code).strip():
|
||||
self.okved_extra_listbox.insert(tk.END, resolve_okved(str(code).strip(), _tok3))
|
||||
_applied += 1
|
||||
@@ -1482,11 +1476,18 @@ class DokoGenApp:
|
||||
self.doc_number_var.set(c.contract_number 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)
|
||||
try:
|
||||
from .verify import split_okved_extra, resolve_okved
|
||||
from .api_settings import load_api_settings as _las_g
|
||||
_tok_g = (_las_g().get('dadata_token') or '')
|
||||
for item in split_okved_extra(c.okved_extra or []):
|
||||
self.okved_extra_listbox.insert(tk.END, resolve_okved(item, _tok_g))
|
||||
except Exception:
|
||||
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}
|
||||
@@ -1908,9 +1909,15 @@ class DokoGenApp:
|
||||
w.set(val)
|
||||
elif isinstance(w, tk.Listbox):
|
||||
w.delete(0, tk.END)
|
||||
for line in str(val).split('\n'):
|
||||
if line.strip():
|
||||
w.insert(tk.END, line.strip())
|
||||
val = str(val)
|
||||
if model_field in ('threats', 'incidents'):
|
||||
val = normalize_threats(val)
|
||||
lines = [ln.strip() for ln in val.split('\n') if ln.strip()]
|
||||
if model_field == 'threats':
|
||||
from .threat_rules import sort_threats_by_ubi
|
||||
lines = sort_threats_by_ubi(lines)
|
||||
for line in lines:
|
||||
w.insert(tk.END, line)
|
||||
elif isinstance(w, tk.Text):
|
||||
w.delete('1.0', tk.END)
|
||||
w.insert('1.0', val)
|
||||
@@ -2197,6 +2204,16 @@ class DokoGenApp:
|
||||
критериев экономической значимости» ФСТЭК (2021). Показывает расчёт
|
||||
и предлагает категорию как рекомендацию."""
|
||||
import re as _re2
|
||||
from .variables import (CRITERION_QUANT_THRESHOLDS, CRITERION_CHOICE_INDICATORS,
|
||||
CRITERION_QUANT_UNITS, _calc_criterion_other,
|
||||
_calc_criterion_13_opk, _criterion_10_cat)
|
||||
|
||||
# Сфера ОПК — для показателей 8/13а/13б используются формулы ПП РФ № 796
|
||||
_sphere_opk = False
|
||||
try:
|
||||
_sphere_opk = (self.is_sphere_combo.get().strip() == 'Оборонная промышленность')
|
||||
except Exception:
|
||||
_sphere_opk = False
|
||||
|
||||
def _num(s):
|
||||
# «121 118 270 руб», «1 000», «906 779 руб», «2 212.5» → число
|
||||
@@ -2218,7 +2235,9 @@ class DokoGenApp:
|
||||
# grab_set + lift — чтобы окно не уходило на задний план
|
||||
w = tk.Toplevel(dialog)
|
||||
w.title(f"🧮 Авторасчёт показателя {num} — {name[:60]}")
|
||||
w.geometry("980x760")
|
||||
w.geometry("980x820")
|
||||
w.minsize(760, 560)
|
||||
w.resizable(True, True)
|
||||
w.transient(dialog)
|
||||
w.lift()
|
||||
w.focus_force()
|
||||
@@ -2236,7 +2255,141 @@ class DokoGenApp:
|
||||
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
if num == '9':
|
||||
if num in ('13а', '13б') and _sphere_opk:
|
||||
# ОПК: 13а — V = (1−(V_max−V_инц)/V_ГОЗ)×100%, 13б — T = (1−(t_норм/t_ненорм))×100%
|
||||
# (ПП РФ № 796 от 27.06.2026, п. 34, 38)
|
||||
ttk.Label(frm, text=f"Показатель {num} — по отраслевым особенностям ОПК (ПП РФ № 796)",
|
||||
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
|
||||
if num == '13а':
|
||||
ttk.Label(frm, text="Формула: V = (1 − (V_max − V_инц) / V_ГОЗ) × 100%, где\n"
|
||||
"V_max — максимальный объём продукции ГОЗ при максимальной скорости выпуска;\n"
|
||||
"V_инц — объём, поставленный во время устранения компьютерного инцидента;\n"
|
||||
"V_ГОЗ — полный объём продукции по ГОЗ.",
|
||||
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
|
||||
|
||||
def _row(label, key, default=''):
|
||||
ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0))
|
||||
e = ttk.Entry(frm, width=80)
|
||||
e.insert(0, str(default or ''))
|
||||
e.pack(fill=tk.X)
|
||||
return e
|
||||
|
||||
_ci13a = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
|
||||
e_vmax = _row("V_max — максимальный объём продукции ГОЗ (ед.):", 'vmax', _ci13a.get('13a_vmax', ''))
|
||||
e_vinc = _row("V_инц — объём продукции, поставленный во время устранения инцидента (ед.):", 'vinc', _ci13a.get('13a_vinc', ''))
|
||||
e_vgoz = _row("V_ГОЗ — полный объём продукции по ГОЗ (ед.):", 'vgoz', _ci13a.get('13a_vgoz', ''))
|
||||
else:
|
||||
ttk.Label(frm, text="Формула: T = (1 − (t_норм / t_ненорм)) × 100%, где\n"
|
||||
"t_норм — сумма времени операций по производству единицы продукции ГОЗ\n"
|
||||
"при штатном функционировании объекта КИИ;\n"
|
||||
"t_ненорм — сумма времени тех же операций при ненормальном функционировании.",
|
||||
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
|
||||
|
||||
def _row(label, key, default=''):
|
||||
ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0))
|
||||
e = ttk.Entry(frm, width=80)
|
||||
e.insert(0, str(default or ''))
|
||||
e.pack(fill=tk.X)
|
||||
return e
|
||||
|
||||
_ci13b = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
|
||||
e_tnorm = _row("t_норм — время операций при штатном функционировании (час):", 'tnorm', _ci13b.get('13b_tnorm', ''))
|
||||
e_tnon = _row("t_ненорм — время операций при ненормальном функционировании (час):", 'tnon', _ci13b.get('13b_tnonorm', ''))
|
||||
result_frame = ttk.Frame(frm)
|
||||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result = tk.Text(result_frame, height=10, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview)
|
||||
result.configure(yscrollcommand=result_scroll.set)
|
||||
result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
result_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
elif (num in CRITERION_QUANT_THRESHOLDS or num in CRITERION_CHOICE_INDICATORS):
|
||||
# Остальные показатели (1–7, 11–14): количественные — ввод значения,
|
||||
# качественные — выбор варианта (III/II/I) из справочника ПП-127
|
||||
is_choice = num in CRITERION_CHOICE_INDICATORS
|
||||
ttk.Label(frm, text=f"Показатель {num} — по порогам ПП-127",
|
||||
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
|
||||
if is_choice:
|
||||
ttk.Label(frm, text="Выберите один из вариантов значений (соответствует III/II/I категории):",
|
||||
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
|
||||
else:
|
||||
_unit = CRITERION_QUANT_UNITS.get(num, '')
|
||||
ttk.Label(frm, text=f"Введите значение показателя ({_unit}): категория определяется "
|
||||
"по пороговым значениям ПП-127.",
|
||||
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
|
||||
|
||||
def _row(label, key, default=''):
|
||||
ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0))
|
||||
e = ttk.Entry(frm, width=80)
|
||||
e.insert(0, str(default or ''))
|
||||
e.pack(fill=tk.X)
|
||||
return e
|
||||
|
||||
if is_choice:
|
||||
# Варианты значений для качественного показателя (из criteria_127.json)
|
||||
_opts = []
|
||||
for _g in (criteria if isinstance(criteria, list) else []):
|
||||
for _ind in _g.get('indicators', []):
|
||||
if str(_ind.get('num', '')) == num:
|
||||
_vals = _ind.get('values', {}) or {}
|
||||
_opts = [str(v) for v in _vals.values() if str(v).strip()]
|
||||
break
|
||||
e_choice = ttk.Combobox(frm, width=90, state='readonly', values=_opts)
|
||||
e_choice.pack(fill=tk.X, pady=4)
|
||||
else:
|
||||
e_value = _row(f"Значение показателя ({_unit}):", 'value', '')
|
||||
result_frame = ttk.Frame(frm)
|
||||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result = tk.Text(result_frame, height=10, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview)
|
||||
result.configure(yscrollcommand=result_scroll.set)
|
||||
result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
result_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
elif num == '10' or str(num).startswith('10.'):
|
||||
# Показатель 10 — прекращение финансовых операций (банковская сфера)
|
||||
is_base = str(num) == '10'
|
||||
if is_base:
|
||||
ttk.Label(frm, text="Показатель 10 — прекращение (нарушение) проведения клиентами операций "
|
||||
"по переводу денежных средств",
|
||||
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
|
||||
ttk.Label(frm, text="Применим для: системно значимых кредитных организаций, операторов услуг "
|
||||
"платёжной инфраструктуры значимых платёжных систем, операторов платформы "
|
||||
"цифрового рубля и др.\n"
|
||||
"Формула: №10 = операции за год (млн ед.) / календарные дни в году "
|
||||
"(среднедневное количество операций).\n"
|
||||
"Пороги: ≤ 70 млн/день → III; > 70 и ≤ 120 → II; > 120 → I.",
|
||||
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
|
||||
else:
|
||||
ttk.Label(frm, text=f"Показатель {num} — прекращение (нарушение) проведения операций "
|
||||
"(подпоказатель 10.x)",
|
||||
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
|
||||
ttk.Label(frm, text="Значение показателя вводится в единицах, установленных ПП-127 "
|
||||
"(трлн руб., тыс. шт., млрд руб., количество договоров, млн ед.); "
|
||||
"категория определяется по порогам ПП-127.",
|
||||
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
|
||||
|
||||
def _row(label, key, default=''):
|
||||
ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0))
|
||||
e = ttk.Entry(frm, width=80)
|
||||
e.insert(0, str(default or ''))
|
||||
e.pack(fill=tk.X)
|
||||
return e
|
||||
|
||||
_ops = getattr(self.model, 'ops_count', '') or ''
|
||||
_days = getattr(self.model, 'calendar_days', '') or '365'
|
||||
_v10 = getattr(self.model, 'criterion10_value', '') or ''
|
||||
if is_base:
|
||||
e_ops = _row("Количество операций за прошедший год (млн ед.):", 'ops', _ops)
|
||||
e_days = _row("Количество календарных дней в году (по умолчанию 365):", 'days', _days)
|
||||
else:
|
||||
e_val10 = _row("Значение показателя (в единицах ПП-127):", 'val10', _v10)
|
||||
result_frame = ttk.Frame(frm)
|
||||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result = tk.Text(result_frame, height=12, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview)
|
||||
result.configure(yscrollcommand=result_scroll.set)
|
||||
result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
result_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
elif num == '9':
|
||||
ttk.Label(frm, text="Показатель 9 — ущерб бюджетам РФ (применим ко всем субъектам КИИ)",
|
||||
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
|
||||
ttk.Label(frm, text="Формула: №9 = ΔБ / Бусредн × 100%, где ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС\n"
|
||||
@@ -2277,8 +2430,67 @@ class DokoGenApp:
|
||||
e_div = _row("Снижение выплат от дивидендов ΔДИВ (руб.):", 'div', '0')
|
||||
e_vso = _row("Снижение выплат сторонних организаций ΔВСО (руб.):", 'vso', '0')
|
||||
e_ns = _row("Снижение сборов ΔНС (руб.):", 'ns', '0')
|
||||
result = tk.Text(frm, height=8, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result_frame = ttk.Frame(frm)
|
||||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result = tk.Text(result_frame, height=12, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview)
|
||||
result.configure(yscrollcommand=result_scroll.set)
|
||||
result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
result_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
elif num == '8' and _sphere_opk:
|
||||
# ОПК: U_8 = U_у8 / R_год × 100%, U_у8 = C + A + X (ПП РФ № 796, п. 18–20)
|
||||
ttk.Label(frm, text="Показатель 8 — ущерб субъекту КИИ (отраслевые особенности ОПК, ПП РФ № 796)",
|
||||
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
|
||||
ttk.Label(frm, text="Формула: U_8 = U_у8 / R_год × 100%, где U_у8 = C + A + X\n"
|
||||
"C — суммарные затраты на объект КИИ;\n"
|
||||
"A — затраты на возмещение ущерба потерпевшим (если ущерба нет, A = 0);\n"
|
||||
"X — ущерб от ранее произошедших компьютерных инцидентов (если не было, X = 0);\n"
|
||||
"R_год — усреднённый объём годового дохода за прошедший 5-летний период.",
|
||||
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
|
||||
|
||||
def _row(label, key, default=''):
|
||||
ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0))
|
||||
e = ttk.Entry(frm, width=80)
|
||||
e.insert(0, str(default or ''))
|
||||
e.pack(fill=tk.X)
|
||||
return e
|
||||
|
||||
_ci8 = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
|
||||
_bv8 = getattr(isys, 'balance_value', '') or ''
|
||||
_bvn8 = _num(_bv8)
|
||||
_c_def = _num(_ci8.get('8_c', '') or '') or _bvn8
|
||||
e_c = _row("C — суммарные затраты на объект КИИ (руб.):", 'c',
|
||||
f"{_c_def:,.0f}" if _c_def else '')
|
||||
if _bvn8 and not _num(_ci8.get('8_c', '') or ''):
|
||||
ttk.Label(frm, text="⚠️ C — подставлена балансовая стоимость объекта КИИ "
|
||||
f"({_bvn8:,.0f} руб.); можно поправить.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
e_a = _row("A — затраты на возмещение ущерба потерпевшим (руб., по умолчанию 0):", 'a',
|
||||
_ci8.get('8_a', ''))
|
||||
e_x = _row("X — ущерб от ранее произошедших компьютерных инцидентов (руб., по умолчанию 0):", 'x',
|
||||
_ci8.get('8_x', ''))
|
||||
_incs_raw8 = [getattr(self.model, f'income_year_{i}', '') or '' for i in range(1, 6)]
|
||||
_incs8 = [_num(x) for x in _incs_raw8]
|
||||
_incs8_f = [x for x in _incs8 if x is not None]
|
||||
_inc58 = _num(getattr(self.model, 'income_5y', '') or '')
|
||||
if _inc58:
|
||||
_d_avg8 = _inc58 / 5.0
|
||||
elif _incs8_f:
|
||||
_d_avg8 = sum(_incs8_f) / len(_incs8_f)
|
||||
else:
|
||||
_d_avg8 = None
|
||||
e_r = _row("R_год — усреднённый годовой доход за 5 лет (руб.):", 'r',
|
||||
f"{_d_avg8:,.0f}" if _d_avg8 else '')
|
||||
ttk.Label(frm, text="⚠️ R_год — среднее по заполненным годам; ПП-127 требует усреднение "
|
||||
"за прошедший пятилетний период.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
result_frame = ttk.Frame(frm)
|
||||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result = tk.Text(result_frame, height=12, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview)
|
||||
result.configure(yscrollcommand=result_scroll.set)
|
||||
result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
result_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
else:
|
||||
ttk.Label(frm, text="Показатель 8 — ущерб субъекту КИИ",
|
||||
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
|
||||
@@ -2294,15 +2506,30 @@ class DokoGenApp:
|
||||
return e
|
||||
|
||||
e_profit = e_tax = None
|
||||
# Доходы за 5 лет — из данных организации (поля «Доходы за 5 лет»)
|
||||
# Доходы за 5 лет — из данных организации (поле «Доход за 5 лет (суммарно)»,
|
||||
# иначе — обратная совместимость по годам)
|
||||
_incs_raw = [getattr(self.model, f'income_year_{i}', '') or '' for i in range(1, 6)]
|
||||
_incs = [_num(x) for x in _incs_raw]
|
||||
_incs_filled = [x for x in _incs if x is not None]
|
||||
_d_avg = (sum(_incs_filled) / len(_incs_filled)) if _incs_filled else None
|
||||
_inc5 = _num(getattr(self.model, 'income_5y', '') or '')
|
||||
if _inc5:
|
||||
_d_avg = _inc5 / 5.0
|
||||
_n_years = 5
|
||||
elif _incs_filled:
|
||||
_d_avg = sum(_incs_filled) / len(_incs_filled)
|
||||
_n_years = len(_incs_filled)
|
||||
else:
|
||||
_d_avg = None
|
||||
_n_years = 0
|
||||
_d_loss = _num(getattr(self.model, 'income_loss', '') or '')
|
||||
_bv8 = getattr(isys, 'balance_value', '') or ''
|
||||
_bvn8 = _num(_bv8)
|
||||
_dincome_def = _d_loss or _bvn8
|
||||
_dincome_est = False
|
||||
if not _dincome_def and _d_avg:
|
||||
# Фолбэк: ущерб не заполнен → оценка «доход за год × 5» (наихудший сценарий)
|
||||
_dincome_def = _d_avg * 5
|
||||
_dincome_est = True
|
||||
e_dincome = _row("Снижение уровня дохода от нарушения критических процессов (руб.):",
|
||||
'dincome', f"{_dincome_def:,.0f}" if _dincome_def else '')
|
||||
if _d_loss:
|
||||
@@ -2313,6 +2540,10 @@ class DokoGenApp:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — подставлена балансовая стоимость объекта КИИ "
|
||||
f"({_bvn8:,.0f} руб.); можно поправить.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
elif _dincome_est:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — не заполнено, принята оценка «доход за год × 5» "
|
||||
f"({_d_avg:,.0f} × 5 = {_dincome_def:,.0f} руб., наихудший сценарий); можно поправить.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
else:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — не заполнено: введите вручную или заполните "
|
||||
"«Снижение уровня дохода» во вкладке «Организация».",
|
||||
@@ -2324,12 +2555,121 @@ class DokoGenApp:
|
||||
"ПП-127 требует усреднение за прошедший пятилетний период: "
|
||||
"при неполных данных результат методологически некорректен — заполните все 5 лет.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
result = tk.Text(frm, height=8, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result_frame = ttk.Frame(frm)
|
||||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
result = tk.Text(result_frame, height=12, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview)
|
||||
result.configure(yscrollcommand=result_scroll.set)
|
||||
result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
result_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
def _calc():
|
||||
lines = []
|
||||
if num == '8':
|
||||
if num in ('13а', '13б') and _sphere_opk:
|
||||
# Сохраняем введённые составляющие формулы в criterion_input (для шаблона)
|
||||
if isys:
|
||||
_ci = dict(getattr(isys, 'criterion_input', None) or {})
|
||||
if num == '13а':
|
||||
_ci['13a_vmax'] = e_vmax.get().strip()
|
||||
_ci['13a_vinc'] = e_vinc.get().strip()
|
||||
_ci['13a_vgoz'] = e_vgoz.get().strip()
|
||||
else:
|
||||
_ci['13b_tnorm'] = e_tnorm.get().strip()
|
||||
_ci['13b_tnonorm'] = e_tnon.get().strip()
|
||||
isys.criterion_input = _ci
|
||||
text, cat = _calc_criterion_13_opk(self.model, isys, num)
|
||||
if text.startswith('Заполните'):
|
||||
result.delete('1.0', tk.END)
|
||||
result.insert('1.0', text)
|
||||
return
|
||||
lines.append(text)
|
||||
if cat:
|
||||
lines.append(f"Категория значимости: {cat} (по порогам ПП-127)")
|
||||
else:
|
||||
lines.append("Категория значимости: — (значение ниже порога III)")
|
||||
elif (num in CRITERION_QUANT_THRESHOLDS or num in CRITERION_CHOICE_INDICATORS):
|
||||
is_choice = num in CRITERION_CHOICE_INDICATORS
|
||||
if is_choice:
|
||||
choice = e_choice.get().strip()
|
||||
if not choice:
|
||||
result.delete('1.0', tk.END)
|
||||
result.insert('1.0', "Выберите вариант значения показателя.")
|
||||
return
|
||||
text, cat = _calc_criterion_other(self.model, isys, num, choice=choice)
|
||||
else:
|
||||
val = _num(e_value.get())
|
||||
if val is None:
|
||||
result.delete('1.0', tk.END)
|
||||
result.insert('1.0', "Введите числовое значение показателя.")
|
||||
return
|
||||
text, cat = _calc_criterion_other(self.model, isys, num, value=val)
|
||||
lines.append(text)
|
||||
if cat:
|
||||
lines.append(f"Категория значимости: {cat} (по порогам ПП-127)")
|
||||
else:
|
||||
lines.append("Категория значимости: — (значение ниже порога III)")
|
||||
elif num == '10' or str(num).startswith('10.'):
|
||||
if str(num) == '10':
|
||||
ops = _num(e_ops.get())
|
||||
days = _num(e_days.get())
|
||||
if days is None or days <= 0:
|
||||
days = 365
|
||||
if ops is None:
|
||||
result.delete('1.0', tk.END)
|
||||
result.insert('1.0', "Заполните количество операций за год (млн ед.).")
|
||||
return
|
||||
val = ops / days
|
||||
lines.append(f"Количество операций за прошедший год: {ops:,.0f} млн ед.")
|
||||
lines.append(f"Количество календарных дней в году: {days:.0f}")
|
||||
lines.append(f"№10 = операции / дни = {ops:,.0f} / {days:.0f} = {val:.6f} млн ед./день")
|
||||
else:
|
||||
val = _num(e_val10.get())
|
||||
if val is None:
|
||||
result.delete('1.0', tk.END)
|
||||
result.insert('1.0', "Заполните значение показателя.")
|
||||
return
|
||||
lines.append(f"Значение показателя {num}: {val:,.6f}")
|
||||
cat = _criterion_10_cat(num, val)
|
||||
if cat:
|
||||
lines.append(f"Категория значимости: {cat} (по порогам ПП-127)")
|
||||
else:
|
||||
lines.append("Категория значимости: — (значение ниже порога III)")
|
||||
elif num == '8' and _sphere_opk:
|
||||
c = _num(e_c.get())
|
||||
a = _num(e_a.get()) or 0
|
||||
x = _num(e_x.get()) or 0
|
||||
r = _num(e_r.get())
|
||||
if c is None or r is None or r <= 0:
|
||||
result.delete('1.0', tk.END)
|
||||
result.insert('1.0', "Заполните C (суммарные затраты на объект) и R_год (доходы за 5 лет).")
|
||||
return
|
||||
if isys:
|
||||
_ci = dict(getattr(isys, 'criterion_input', None) or {})
|
||||
_ci['8_c'] = str(e_c.get().strip())
|
||||
_ci['8_a'] = str(e_a.get().strip())
|
||||
_ci['8_x'] = str(e_x.get().strip())
|
||||
isys.criterion_input = _ci
|
||||
u_y8 = c + a + x
|
||||
val = u_y8 / r * 100
|
||||
lines.append(f"Суммарные затраты на объект КИИ: C = {c:,.0f} руб.")
|
||||
lines.append(f"Затраты на возмещение ущерба потерпевшим: A = {a:,.0f} руб.")
|
||||
lines.append(f"Ущерб от ранее произошедших инцидентов: X = {x:,.0f} руб.")
|
||||
lines.append(f"U_у8 = C + A + X = {c:,.0f} + {a:,.0f} + {x:,.0f} = {u_y8:,.0f} руб.")
|
||||
lines.append(f"Усреднённый годовой доход за 5 лет: R_год = {r:,.0f} руб.")
|
||||
lines.append(f"U_8 = U_у8 / R_год × 100% = {u_y8:,.0f} / {r:,.0f} × 100 = {val:.2f} %")
|
||||
if val > 20:
|
||||
cat = 'I'
|
||||
elif val > 10:
|
||||
cat = 'II'
|
||||
elif val >= 1:
|
||||
cat = 'III'
|
||||
else:
|
||||
cat = None
|
||||
if cat:
|
||||
lines.append(f"Категория значимости: {cat} (более 20% → I; 10-20% → II; 1-10% → III)")
|
||||
else:
|
||||
lines.append("Значение менее 1% — ни одна категория не присваивается («—»).")
|
||||
elif num == '8':
|
||||
d = _num(e_dincome.get())
|
||||
b = _num(e_budget.get())
|
||||
if d is None or b is None or b <= 0:
|
||||
@@ -2403,6 +2743,19 @@ class DokoGenApp:
|
||||
# Если расчёт не удался (ошибка «Заполните…») — не применяем и не пишем в обоснование
|
||||
if txt.startswith('Заполните'):
|
||||
return
|
||||
# Сохраняем введённое значение показателя в модель (для автогенерации шаблона)
|
||||
if (num in CRITERION_QUANT_THRESHOLDS or num in CRITERION_CHOICE_INDICATORS) and isys:
|
||||
try:
|
||||
if num in CRITERION_CHOICE_INDICATORS:
|
||||
_v = e_choice.get().strip()
|
||||
else:
|
||||
_v = e_value.get().strip()
|
||||
if _v:
|
||||
_ci = dict(getattr(isys, 'criterion_input', None) or {})
|
||||
_ci[num] = _v
|
||||
isys.criterion_input = _ci
|
||||
except Exception:
|
||||
pass
|
||||
lines = txt.split('\n')
|
||||
cat_line = next((l for l in lines if l.startswith('Категория значимости:')), '')
|
||||
cat = None
|
||||
@@ -2415,23 +2768,15 @@ class DokoGenApp:
|
||||
if target:
|
||||
var.set(target)
|
||||
prefix = f"Показатель {num}. {name} — {cat} категория."
|
||||
full = prefix + "\n" + txt
|
||||
cur = justify_txt.get('1.0', 'end-1c').strip()
|
||||
if prefix not in cur:
|
||||
if cur:
|
||||
justify_txt.insert('end', '\n' + full)
|
||||
else:
|
||||
justify_txt.insert('1.0', full)
|
||||
_append_justify(prefix, txt)
|
||||
else:
|
||||
var.set('—')
|
||||
prefix = f"Показатель {num}. {name} — применим, категория не присвоена (—)."
|
||||
full = prefix + "\n" + txt
|
||||
cur = justify_txt.get('1.0', 'end-1c').strip()
|
||||
if prefix not in cur:
|
||||
if cur:
|
||||
justify_txt.insert('end', '\n' + full)
|
||||
else:
|
||||
justify_txt.insert('1.0', full)
|
||||
_append_justify(prefix, txt)
|
||||
w.grab_release()
|
||||
w.destroy()
|
||||
|
||||
@@ -2489,8 +2834,11 @@ class DokoGenApp:
|
||||
combo.pack(side=tk.RIGHT, padx=6)
|
||||
_combos[num] = combo
|
||||
|
||||
# Кнопка «🧮» — авторасчёт показателя (п. 8 «ущерб субъекту КИИ» и п. 9 «ущерб бюджетам»)
|
||||
if grp_num in ('8', '9'):
|
||||
# Кнопка «🧮» — авторасчёт показателя:
|
||||
# 8/9/10 — экономические формулы; остальные (1–7, 11–14) — по порогам ПП-127
|
||||
if (grp_num in ('8', '9', '10')
|
||||
or num in CRITERION_QUANT_THRESHOLDS
|
||||
or num in CRITERION_CHOICE_INDICATORS):
|
||||
ttk.Button(row, text='🧮', width=3,
|
||||
command=lambda n=num, nm=name, v=var, c=combo:
|
||||
_open_calc_dialog(n, nm, v, c)
|
||||
@@ -2502,13 +2850,9 @@ class DokoGenApp:
|
||||
txt = _just_text(_num)
|
||||
prefix = f"Показатель {_num}. {_name} — не применим."
|
||||
if txt:
|
||||
full = prefix + " " + txt
|
||||
cur = justify_txt.get('1.0', 'end-1c').strip()
|
||||
if prefix not in cur:
|
||||
if cur:
|
||||
justify_txt.insert('end', '\n' + full)
|
||||
else:
|
||||
justify_txt.insert('1.0', full)
|
||||
_append_justify(prefix, txt)
|
||||
combo.bind('<<ComboboxSelected>>', _on_pick)
|
||||
|
||||
if not applicable_here:
|
||||
@@ -2538,6 +2882,21 @@ class DokoGenApp:
|
||||
anchor=tk.W, pady=(10, 2))
|
||||
justify_txt = tk.Text(body, width=110, height=5, wrap=tk.WORD)
|
||||
justify_txt.pack(fill=tk.X, pady=(0, 6))
|
||||
# Висячий отступ: заголовок «Показатель N. …» — первая строка без отступа,
|
||||
# перенесённые строки — с отступом (чтобы продолжение не выглядело новым пунктом);
|
||||
# тело расчёта — с отступом слева; между показателями — пустая строка.
|
||||
justify_txt.tag_configure('just_head', lmargin1=0, lmargin2=30, spacing1=6, spacing3=2)
|
||||
justify_txt.tag_configure('just_body', lmargin1=30, lmargin2=30, spacing3=4)
|
||||
|
||||
def _append_justify(prefix, body):
|
||||
"""Добавить запись в обоснование: заголовок с висячим отступом + тело."""
|
||||
cur = justify_txt.get('1.0', 'end-1c').strip()
|
||||
if cur:
|
||||
justify_txt.insert('end', '\n\n')
|
||||
justify_txt.insert('end', str(prefix), 'just_head')
|
||||
if body:
|
||||
justify_txt.insert('end', '\n' + str(body), 'just_body')
|
||||
|
||||
if hasattr(self, 'is_criteria_justification_listbox') and self.is_criteria_justification_listbox.size():
|
||||
justify_txt.insert('1.0', '\n'.join(
|
||||
self.is_criteria_justification_listbox.get(i)
|
||||
@@ -2695,13 +3054,9 @@ class DokoGenApp:
|
||||
'указанных в п. 8 перечня показателей критериев '
|
||||
'значимости объектов КИИ.')
|
||||
prefix = f"Показатель {num}. {name} — не применим."
|
||||
full = prefix + " " + txt
|
||||
cur = justify_txt.get('1.0', 'end-1c').strip()
|
||||
if prefix not in cur:
|
||||
if cur:
|
||||
justify_txt.insert('end', '\n' + full)
|
||||
else:
|
||||
justify_txt.insert('1.0', full)
|
||||
_append_justify(prefix, txt)
|
||||
filled.append(f"п. {num}: не применим (тип организации)")
|
||||
elif g == '9':
|
||||
profit = _num2(getattr(self.model, 'profit', '') or '')
|
||||
@@ -2761,13 +3116,9 @@ class DokoGenApp:
|
||||
f"Категория значимости: {cat or '— (не присваивается)'}"
|
||||
)
|
||||
prefix = f"Показатель {num}. {name} — рекомендация по расчёту:"
|
||||
full = prefix + "\n" + calc_txt
|
||||
cur = justify_txt.get('1.0', 'end-1c').strip()
|
||||
if prefix not in cur:
|
||||
if cur:
|
||||
justify_txt.insert('end', '\n' + full)
|
||||
else:
|
||||
justify_txt.insert('1.0', full)
|
||||
_append_justify(prefix, calc_txt)
|
||||
filled.append(f"п. {num}: №9 = {val:.6f}% → {cat or '—'}")
|
||||
# Что осталось незаполненным (применимые, без значения)
|
||||
left = []
|
||||
@@ -2873,9 +3224,12 @@ class DokoGenApp:
|
||||
if key in fill and hasattr(self, attr):
|
||||
lb = getattr(self, attr)
|
||||
lb.delete(0, tk.END)
|
||||
for line in fill[key].split('\n'):
|
||||
if line.strip():
|
||||
lb.insert(tk.END, line.strip())
|
||||
lines = [ln.strip() for ln in fill[key].split('\n') if ln.strip()]
|
||||
if key == 'threats':
|
||||
from .threat_rules import sort_threats_by_ubi
|
||||
lines = sort_threats_by_ubi(lines)
|
||||
for line in lines:
|
||||
lb.insert(tk.END, line)
|
||||
|
||||
self._collect_is() # сохранить в модель
|
||||
filled = ', '.join({
|
||||
@@ -2911,7 +3265,8 @@ class DokoGenApp:
|
||||
return
|
||||
lb = self.is_threats_listbox
|
||||
lb.delete(0, tk.END)
|
||||
for t in threats:
|
||||
from .threat_rules import sort_threats_by_ubi
|
||||
for t in sort_threats_by_ubi(threats):
|
||||
lb.insert(tk.END, t)
|
||||
self._collect_is() # сохранить в модель — галочки в словаре будут стоять
|
||||
self.log(f"⚙️ Угрозы подобраны программой: {len(threats)} шт.")
|
||||
@@ -3286,6 +3641,12 @@ class DokoGenApp:
|
||||
existing.add(key)
|
||||
added += 1
|
||||
if added:
|
||||
# сортируем по возрастанию кода УБИ
|
||||
from .threat_rules import sort_threats_by_ubi
|
||||
items = [str(lb.get(i)) for i in range(lb.size())]
|
||||
lb.delete(0, tk.END)
|
||||
for t in sort_threats_by_ubi(items):
|
||||
lb.insert(tk.END, t)
|
||||
self._collect_is()
|
||||
self.log(f"➕ Добавлено недостающих угроз (по словарю): {added}")
|
||||
else:
|
||||
@@ -3462,15 +3823,21 @@ class DokoGenApp:
|
||||
e = entries.get(key)
|
||||
return e.get('1.0', 'end-1c').strip() if e else ''
|
||||
|
||||
def _fill_listbox(lb, text):
|
||||
def _fill_listbox(lb, text, sort_ubi=False):
|
||||
if not lb:
|
||||
return
|
||||
lb.delete(0, tk.END)
|
||||
import re as _re3
|
||||
lines = []
|
||||
for line in _re3.split(r'[\n,;]+', text or ''):
|
||||
line = line.strip().lstrip('•-–— ')
|
||||
if line:
|
||||
lb.insert(tk.END, line)
|
||||
lines.append(line)
|
||||
if sort_ubi:
|
||||
from .threat_rules import sort_threats_by_ubi
|
||||
lines = sort_threats_by_ubi(lines)
|
||||
for line in lines:
|
||||
lb.insert(tk.END, line)
|
||||
|
||||
try:
|
||||
if _wanted('attacker_category'):
|
||||
@@ -3480,7 +3847,7 @@ class DokoGenApp:
|
||||
exact = next((v for v in values if v.lower() == val.lower()), None)
|
||||
self.is_attacker_category_combo.set(exact or val)
|
||||
if _wanted('threats'):
|
||||
_fill_listbox(getattr(self, 'is_threats_listbox', None), _text('threats'))
|
||||
_fill_listbox(getattr(self, 'is_threats_listbox', None), _text('threats'), sort_ubi=True)
|
||||
if _wanted('incidents'):
|
||||
_fill_listbox(getattr(self, 'is_incidents_listbox', None), _text('incidents'))
|
||||
if _wanted('criteria_values'):
|
||||
@@ -3738,6 +4105,13 @@ class DokoGenApp:
|
||||
'category': isys.category or '',
|
||||
'sphere': isys.sphere or '',
|
||||
'object_type': isys.object_type or '',
|
||||
'description': isys.description or '',
|
||||
'architecture': isys.architecture or '',
|
||||
'hw_sw': isys.hw_sw or '',
|
||||
'os_sw': isys.os_sw or '',
|
||||
'app_sw': isys.app_sw or '',
|
||||
'net_category': isys.net_category or '',
|
||||
'is_internet': bool(getattr(isys, 'is_internet', False)),
|
||||
'threats': isys.threats or '',
|
||||
'attacker_category': isys.attacker_category or '',
|
||||
'defense_tools': list(self.defense_tools_listbox.get(0, tk.END)) or [],
|
||||
@@ -3803,6 +4177,13 @@ class DokoGenApp:
|
||||
'category': isys.category or '',
|
||||
'sphere': isys.sphere or '',
|
||||
'object_type': isys.object_type or '',
|
||||
'description': isys.description or '',
|
||||
'architecture': isys.architecture or '',
|
||||
'hw_sw': isys.hw_sw or '',
|
||||
'os_sw': isys.os_sw or '',
|
||||
'app_sw': isys.app_sw or '',
|
||||
'net_category': isys.net_category or '',
|
||||
'is_internet': bool(getattr(isys, 'is_internet', False)),
|
||||
'threats': isys.threats or '',
|
||||
'attacker_category': isys.attacker_category or '',
|
||||
'defense_tools': list(self.defense_tools_listbox.get(0, tk.END)) or [],
|
||||
@@ -4114,9 +4495,16 @@ class DokoGenApp:
|
||||
# Последние 5 лет: income_year_5 (самый свежий) ... income_year_1 (самый старый)
|
||||
last5 = rows[-5:]
|
||||
mapping = []
|
||||
total = 0.0
|
||||
for i, (y, inc) in enumerate(last5):
|
||||
# y — календарный год; income_year_1 — самый старый из пяти
|
||||
mapping.append((f'income_year_{i+1}', y, inc))
|
||||
total += inc
|
||||
# Основное поле формы — «Доход за 5 лет (суммарно)»
|
||||
entry = self._company_entries.get('income_5y')
|
||||
if entry:
|
||||
entry.delete(0, tk.END)
|
||||
entry.insert(0, f"{total:,.0f}".replace(',', ' '))
|
||||
for key, y, inc in mapping:
|
||||
entry = self._company_entries.get(key)
|
||||
if entry:
|
||||
@@ -4125,7 +4513,7 @@ class DokoGenApp:
|
||||
self._collect_company()
|
||||
years = ', '.join(f"{y}: {inc:,.0f} руб." for _, y, inc in mapping)
|
||||
self.log(f"🌐 Доходы из DaData загружены: {years}")
|
||||
messagebox.showinfo("DaData", f"Доходы загружены из DaData:\n{years}")
|
||||
messagebox.showinfo("DaData", f"Доходы загружены из DaData (сумма за 5 лет: {total:,.0f} руб.):\n{years}")
|
||||
|
||||
def _select_main_okved(self):
|
||||
"""Выбор основного ОКВЭД из словаря (всегда один)."""
|
||||
@@ -4446,6 +4834,10 @@ class DokoGenApp:
|
||||
# Автоопределение города из адреса после импорта
|
||||
self._on_address_changed()
|
||||
|
||||
# Ответы по физической безопасности (для автогенерации орг. мер)
|
||||
if data.get('physicalSecurity'):
|
||||
self.model.physical_security = dict(data['physicalSecurity'])
|
||||
|
||||
# Должностные лица (маппинг)
|
||||
officials_map = {
|
||||
'chief_position': 'chiefPosition',
|
||||
@@ -4486,6 +4878,7 @@ class DokoGenApp:
|
||||
|
||||
# Показатель 8: доходы за 5 лет + суммарный ущерб от нарушения критических процессов
|
||||
income_map = [
|
||||
('income_5y', ('income5Y', 'income_5y', 'income5y')),
|
||||
('income_year_1', ('incomeYear1', 'income_year_1')),
|
||||
('income_year_2', ('incomeYear2', 'income_year_2')),
|
||||
('income_year_3', ('incomeYear3', 'income_year_3')),
|
||||
@@ -4535,23 +4928,43 @@ class DokoGenApp:
|
||||
entry.insert(0, resolve_okved(data['okved'], _tok))
|
||||
if data.get('okvedExtra'):
|
||||
if hasattr(self, 'okved_extra_listbox'):
|
||||
from .verify import resolve_okved
|
||||
from .verify import resolve_okved, split_okved_extra
|
||||
from .api_settings import load_api_settings as _las2
|
||||
_tok2 = (_las2().get('dadata_token') or '')
|
||||
self.okved_extra_listbox.delete(0, tk.END)
|
||||
extra = data['okvedExtra']
|
||||
if isinstance(extra, str):
|
||||
import re as _re
|
||||
parts = [p.strip() for p in _re.split(r'[;,\n]+', extra) if p.strip()]
|
||||
else:
|
||||
parts = [str(p).strip() for p in extra if str(p).strip()]
|
||||
for p in parts:
|
||||
# Доп. ОКВЭД могут быть склеены с переносами внутри названий
|
||||
# («25.93 — Производство изделий из проволоки\nцепей и пружин 32.12.1 — …») —
|
||||
# нарезаем по кодам ОКВЭД, затем нормализуем каждый в «код название».
|
||||
for p in split_okved_extra(data['okvedExtra']):
|
||||
self.okved_extra_listbox.insert(tk.END, resolve_okved(p, _tok2))
|
||||
|
||||
# Объекты КИИ
|
||||
if data.get('informationSystems'):
|
||||
self.model.information_systems.clear()
|
||||
for is_data in data['informationSystems']:
|
||||
# Фильтр по сфере: оставляем только показатели, применимые к сфере объекта
|
||||
# (организация заполняет в опроснике только свои; лишнее отбрасываем).
|
||||
_crit_in = dict(is_data.get('criterion_input', {}) or {})
|
||||
if _crit_in:
|
||||
import re as _re_sphere
|
||||
_appl = None
|
||||
try:
|
||||
import json as _json_sphere
|
||||
_ap_path = os.path.join(os.path.dirname(__file__),
|
||||
'dictionaries', 'criteria_applicability.json')
|
||||
with open(_ap_path, encoding='utf-8') as _f_sphere:
|
||||
_appl_all = _json_sphere.load(_f_sphere)
|
||||
_appl = _appl_all.get(is_data.get('sphere', '') or '')
|
||||
except Exception:
|
||||
_appl = None
|
||||
if _appl:
|
||||
_filtered = {}
|
||||
for _n, _v in _crit_in.items():
|
||||
_m = _re_sphere.match(r'(\d+)', str(_n))
|
||||
_g = _m.group(1) if _m else str(_n)
|
||||
if _g in _appl:
|
||||
_filtered[_n] = _v
|
||||
_crit_in = _filtered
|
||||
isys = InformationSystem(
|
||||
name=is_data.get('name', ''),
|
||||
description=is_data.get('description', ''),
|
||||
@@ -4583,8 +4996,9 @@ class DokoGenApp:
|
||||
attacker_category=is_data.get('attacker_category', ''),
|
||||
attacker_level=is_data.get('attacker_level', ''),
|
||||
attacker_types=is_data.get('attacker_types', ''),
|
||||
threats=is_data.get('threats', ''),
|
||||
incidents=is_data.get('incidents', ''),
|
||||
threats=normalize_threats(is_data.get('threats', '')),
|
||||
incidents=normalize_threats(is_data.get('incidents', '')),
|
||||
criterion_input=_crit_in,
|
||||
criteria_values=is_data.get('criteria_values', ''),
|
||||
criteria_justification=is_data.get('criteria_justification', ''),
|
||||
org_measures=is_data.get('org_measures', ''),
|
||||
|
||||
+427
-17
@@ -61,7 +61,8 @@ VARIABLE_DEFS = [
|
||||
('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'),
|
||||
('{{company_profit}}', 'A', 'Сумма прибыли за предыдущий год (руб.)', 'company:profit'),
|
||||
('{{company_profit_tax}}', 'A', 'Сумма налога с прибыли (руб.)', 'company:profit_tax'),
|
||||
('{{company_income_year_1}}', 'A', 'Доход за 5 лет назад (руб.) — показатель 8', 'company:income_year_1'),
|
||||
('{{company_income_5y}}', 'A', 'Доход за 5 лет суммарно (руб.) — показатель 8', 'company:income_5y'),
|
||||
('{{company_income_year_1}}', 'A', 'Доход за 5 лет назад (руб.) — показатель 8 (обратная совместимость)', 'company:income_year_1'),
|
||||
('{{company_income_year_2}}', 'A', 'Доход за 4 года назад (руб.) — показатель 8', 'company:income_year_2'),
|
||||
('{{company_income_year_3}}', 'A', 'Доход за 3 года назад (руб.) — показатель 8', 'company:income_year_3'),
|
||||
('{{company_income_year_4}}', 'A', 'Доход за 2 года назад (руб.) — показатель 8', 'company:income_year_4'),
|
||||
@@ -357,7 +358,10 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
||||
'incidents': getattr(isys, 'incidents', '') or '—',
|
||||
'criteria_values': getattr(isys, 'criteria_values', '') or '—',
|
||||
'criteria_justification': getattr(isys, 'criteria_justification', '') or '—',
|
||||
'org_measures': getattr(isys, 'org_measures', '') or '—',
|
||||
# Орг. меры: если не заполнены вручную — автогенерация по опроснику
|
||||
# (с учётом ответов раздела «Физическая безопасность»).
|
||||
'org_measures': (getattr(isys, 'org_measures', '') or '')
|
||||
or _build_org_measures_text(company, isys),
|
||||
'tech_measures': getattr(isys, 'tech_measures', '') or '—',
|
||||
})
|
||||
loops['information_systems'] = items
|
||||
@@ -425,12 +429,21 @@ def _calc_criterion_8_9(company, isys, num):
|
||||
d = _num(getattr(company, 'income_loss', '') or '')
|
||||
if d is None:
|
||||
d = _num(getattr(isys, 'balance_value', '') or '')
|
||||
# Доходы за 5 лет
|
||||
incs = [_num(getattr(company, f'income_year_{i}', '') or '') for i in range(1, 6)]
|
||||
incs = [x for x in incs if x is not None]
|
||||
r_avg = (sum(incs) / len(incs)) if incs else None
|
||||
# Доходы за 5 лет: основное поле income_5y (суммарно), иначе — обратная совместимость по годам
|
||||
inc5 = _num(getattr(company, 'income_5y', '') or '')
|
||||
if inc5:
|
||||
r_avg = inc5 / 5.0
|
||||
else:
|
||||
incs = [_num(getattr(company, f'income_year_{i}', '') or '') for i in range(1, 6)]
|
||||
incs = [x for x in incs if x is not None]
|
||||
r_avg = (sum(incs) / len(incs)) if incs else None
|
||||
|
||||
if num == '8':
|
||||
# Фолбэк: ущерб не заполнен → оценка «доход за год × 5» (наихудший сценарий)
|
||||
d_estimated = False
|
||||
if d is None and r_avg:
|
||||
d = r_avg * 5
|
||||
d_estimated = True
|
||||
if d is None or not r_avg:
|
||||
return ("Показатель применим. Для расчёта заполните данные организации: "
|
||||
"«Снижение уровня дохода» и доходы за 5 лет, или балансовую стоимость объекта КИИ.", None)
|
||||
@@ -445,11 +458,16 @@ def _calc_criterion_8_9(company, isys, num):
|
||||
cat = None
|
||||
text = (f"Реализация компьютерных атак на объект не приведет к снижению доходов субъекта КИИ. "
|
||||
f"Объем среднегодового дохода субъекта КИИ (с учетом НДС), усредненный за прошедший "
|
||||
f"5-летний период, составляет {r_avg:,.2f} руб. В случае выхода из строя объекта КИИ "
|
||||
f"потребуется замена, установка и настройка, стоимость которых составит около {d:,.2f} руб. "
|
||||
f"Абсолютный ущерб субъекта КИИ: U={d:,.2f} руб. "
|
||||
f"Показатель ущерба субъекту КИИ: U_8 = U / R_год = {d:,.2f} / {r_avg:,.2f} "
|
||||
f"= {val_pct:.6f} %.")
|
||||
f"5-летний период, составляет {r_avg:,.2f} руб. ")
|
||||
if d_estimated:
|
||||
text += (f"Снижение уровня дохода не заполнено, поэтому оценка ущерба принята по наихудшему "
|
||||
f"сценарию: доход за год × 5 лет = {r_avg:,.2f} × 5 = {d:,.2f} руб. ")
|
||||
else:
|
||||
text += (f"В случае выхода из строя объекта КИИ потребуется замена, установка и настройка, "
|
||||
f"стоимость которых составит около {d:,.2f} руб. ")
|
||||
text += (f"Абсолютный ущерб субъекта КИИ: U={d:,.2f} руб. "
|
||||
f"Показатель ущерба субъекту КИИ: U_8 = U / R_год = {d:,.2f} / {r_avg:,.2f} "
|
||||
f"= {val_pct:.6f} %.")
|
||||
if cat:
|
||||
text += (f" Так как {val_pct:.6f} % более 1% (пороговое значение для 3-й категории "
|
||||
f"значимости согласно Постановлению № 127), по данному показателю объекту "
|
||||
@@ -505,6 +523,332 @@ def _calc_criterion_8_9(company, isys, num):
|
||||
return text, cat
|
||||
|
||||
|
||||
def _calc_criterion_8_opk(company, isys):
|
||||
"""Показатель 8 (ущерб субъекту КИИ) по отраслевым особенностям ОПК
|
||||
(ПП РФ № 796 от 27.06.2026, п. 18–20):
|
||||
|
||||
U_8 = U_у8 / R_год × 100%,
|
||||
U_у8 = C + A + X,
|
||||
|
||||
где C — суммарные затраты на объект КИИ;
|
||||
A — затраты на возмещение ущерба потерпевшим и их имуществу (если инцидент
|
||||
не влечёт ущерба, A = 0);
|
||||
X — ущерб от ранее произошедших компьютерных инцидентов (если инцидентов
|
||||
не было или нет данных, X = 0);
|
||||
R_год — усреднённый объём годового дохода за прошедший 5-летний период.
|
||||
|
||||
Значения C/A/X берутся из criterion_input (ключи 8_c, 8_a, 8_x);
|
||||
если C не заполнен — используется балансовая стоимость объекта КИИ.
|
||||
Возвращает (текст_обоснования, категория или None).
|
||||
"""
|
||||
crit = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
|
||||
c_val = _num(crit.get('8_c', '') or '') or _num(getattr(isys, 'balance_value', '') or '')
|
||||
a_val = _num(crit.get('8_a', '') or '') or 0.0
|
||||
x_val = _num(crit.get('8_x', '') or '') or 0.0
|
||||
|
||||
inc5 = _num(getattr(company, 'income_5y', '') or '')
|
||||
if inc5:
|
||||
r_avg = inc5 / 5.0
|
||||
else:
|
||||
incs = [_num(getattr(company, f'income_year_{i}', '') or '') for i in range(1, 6)]
|
||||
incs = [x for x in incs if x is not None]
|
||||
r_avg = (sum(incs) / len(incs)) if incs else None
|
||||
|
||||
if c_val is None or not r_avg:
|
||||
return ("Показатель применим (отраслевые особенности ОПК, ПП РФ № 796). "
|
||||
"Для расчёта заполните суммарные затраты на объект КИИ (C) и доходы за 5 лет "
|
||||
"(или балансовую стоимость объекта КИИ).", None)
|
||||
|
||||
u_y8 = c_val + a_val + x_val
|
||||
val_pct = u_y8 / r_avg * 100
|
||||
if val_pct > 20:
|
||||
cat = 'I'
|
||||
elif val_pct > 10:
|
||||
cat = 'II'
|
||||
elif val_pct >= 1:
|
||||
cat = 'III'
|
||||
else:
|
||||
cat = None
|
||||
|
||||
text = (f"Показатель 8 рассчитывается по отраслевым особенностям ОПК "
|
||||
f"(ПП РФ № 796 от 27.06.2026): U_8 = U_у8 / R_год × 100%, U_у8 = C + A + X. "
|
||||
f"Суммарные затраты на объект КИИ: C = {c_val:,.2f} руб. "
|
||||
f"Затраты на возмещение ущерба потерпевшим: A = {a_val:,.2f} руб. "
|
||||
f"Ущерб от ранее произошедших компьютерных инцидентов: X = {x_val:,.2f} руб. "
|
||||
f"Ущерб субъекту КИИ: U_у8 = {c_val:,.2f} + {a_val:,.2f} + {x_val:,.2f} "
|
||||
f"= {u_y8:,.2f} руб. "
|
||||
f"Усреднённый годовой доход за 5-летний период: R_год = {r_avg:,.2f} руб. "
|
||||
f"Показатель ущерба: U_8 = {u_y8:,.2f} / {r_avg:,.2f} × 100% = {val_pct:.6f} %.")
|
||||
if cat:
|
||||
text += (f" Так как {val_pct:.6f} % более 1% (пороговое значение для 3-й категории "
|
||||
f"значимости), по данному показателю объекту присваивается {cat}-я категория значимости.")
|
||||
else:
|
||||
text += (f" Так как {val_pct:.6f} % менее 1% (пороговое значение для 3-й категории "
|
||||
f"значимости), по данному показателю объекту категория значимости не присваивается.")
|
||||
return text, cat
|
||||
|
||||
|
||||
def _calc_criterion_13_opk(company, isys, num):
|
||||
"""Показатели 13а/13б по отраслевым особенностям ОПК (ПП РФ № 796, п. 34, 38):
|
||||
|
||||
13а: V = (1 − (V_max − V_инц) / V_ГОЗ) × 100% (снижение объёма продукции ГОЗ)
|
||||
13б: T = (1 − (t_норм / t_ненорм)) × 100% (увеличение времени изготовления)
|
||||
|
||||
Значения берутся из criterion_input: 13a_vmax, 13a_vinc, 13a_vgoz;
|
||||
13b_tnorm, 13b_tnonorm. Категория — по порогам ПП-127 (CRITERION_QUANT_THRESHOLDS).
|
||||
Возвращает (текст_обоснования, категория или None).
|
||||
"""
|
||||
crit = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
|
||||
num = str(num)
|
||||
if num == '13а':
|
||||
vmax = _num(crit.get('13a_vmax', '') or '')
|
||||
vinc = _num(crit.get('13a_vinc', '') or '')
|
||||
vgoz = _num(crit.get('13a_vgoz', '') or '')
|
||||
if vmax is None or vinc is None or not vgoz:
|
||||
return ("Показатель 13а рассчитывается по отраслевым особенностям ОПК "
|
||||
"(ПП РФ № 796): V = (1 − (V_max − V_инц) / V_ГОЗ) × 100%. "
|
||||
"Заполните V_max, V_инц, V_ГОЗ (объёмы продукции ГОЗ).", None)
|
||||
val = (1 - (vmax - vinc) / vgoz) * 100
|
||||
formula = (f"V = (1 − ({vmax:,.2f} − {vinc:,.2f}) / {vgoz:,.2f}) × 100% = {val:.6f} %")
|
||||
else: # 13б
|
||||
tnorm = _num(crit.get('13b_tnorm', '') or '')
|
||||
tnon = _num(crit.get('13b_tnonorm', '') or '')
|
||||
if tnorm is None or tnon is None or tnon <= 0:
|
||||
return ("Показатель 13б рассчитывается по отраслевым особенностям ОПК "
|
||||
"(ПП РФ № 796): T = (1 − (t_норм / t_ненорм)) × 100%. "
|
||||
"Заполните t_норм и t_ненорм (время операций по ГОЗ).", None)
|
||||
val = (1 - tnorm / tnon) * 100
|
||||
formula = (f"T = (1 − ({tnorm:,.2f} / {tnon:,.2f})) × 100% = {val:.6f} %")
|
||||
|
||||
cat = _criterion_other_cat(num, val)
|
||||
text = (f"Показатель {num} рассчитывается по отраслевым особенностям ОПК "
|
||||
f"(ПП РФ № 796 от 27.06.2026): {formula}.")
|
||||
if cat:
|
||||
text += (f" По порогам ПП-127 это соответствует {cat}-й категории значимости.")
|
||||
else:
|
||||
text += " По порогам ПП-127 категория значимости не присваивается."
|
||||
return text, cat
|
||||
|
||||
|
||||
# Пороги показателя 10 и подпоказателей 10.1–10.7 (ПП-127, «Значение показателя»)
|
||||
CRITERION_10_THRESHOLDS = {
|
||||
# num: (категория → (нижняя граница, верхняя граница)), границы «включительно»
|
||||
# Показатель 10 — среднедневное количество операций (млн ед.):
|
||||
# III — менее или равно 70, II — более 70 но менее или равно 120, I — более 120.
|
||||
'10': {'I': (120, None), 'II': (70, 120), 'III': (None, 70)},
|
||||
'10.1': {'I': (10, None), 'II': (1, 10), 'III': (None, 1)}, # трлн руб.
|
||||
'10.2': {'I': (25, None), 'II': (10, 25), 'III': (None, 10)}, # тыс. шт.
|
||||
'10.3': {'I': (2000, None), 'II': (1000, 2000), 'III': (50, 1000)}, # млрд руб.
|
||||
'10.4': {'I': (5000, None), 'II': (1500, 5000), 'III': (100, 1500)}, # млрд руб.
|
||||
'10.5': {'I': (150, None), 'II': (100, 150), 'III': (25, 100)}, # договоров
|
||||
'10.6': {'I': (5000, None), 'II': (1000, 5000), 'III': (100, 1000)}, # млрд руб.
|
||||
'10.7': {'I': (500, None), 'II': (200, 500), 'III': (30, 200)}, # млн ед.
|
||||
}
|
||||
|
||||
|
||||
def _criterion_10_cat(num, value):
|
||||
"""Категория значимости по показателю 10 / 10.x (ПП-127).
|
||||
|
||||
value — численное значение показателя. Возвращает 'I'/'II'/'III' или None.
|
||||
"""
|
||||
thr = CRITERION_10_THRESHOLDS.get(str(num))
|
||||
if not thr or value is None:
|
||||
return None
|
||||
for cat in ('I', 'II', 'III'):
|
||||
lo, hi = thr[cat]
|
||||
if lo is not None and value < lo:
|
||||
continue
|
||||
if hi is not None and value > hi:
|
||||
continue
|
||||
return cat
|
||||
return None
|
||||
|
||||
|
||||
def _calc_criterion_10(company, isys, num):
|
||||
"""Расчёт показателя 10 (прекращение финансовых операций) и подпоказателей 10.1–10.7.
|
||||
|
||||
Показатель 10: среднедневное количество операций = операции за год (млн ед.) / дни.
|
||||
Подпоказатели 10.x: значение показателя (в своих единицах) сравнивается с порогами.
|
||||
Возвращает (текст_обоснования, категория или None).
|
||||
"""
|
||||
num = str(num)
|
||||
if num not in CRITERION_10_THRESHOLDS:
|
||||
return None, None
|
||||
if num == '10':
|
||||
ops = _num(getattr(company, 'ops_count', '') or '')
|
||||
days = _num(getattr(company, 'calendar_days', '') or '')
|
||||
if days is None or days <= 0:
|
||||
days = 365
|
||||
if ops is None:
|
||||
return ("Показатель применим. Для расчёта заполните данные организации: "
|
||||
"«Количество операций за прошедший год (млн ед.)».", None)
|
||||
val = ops / days
|
||||
cat = _criterion_10_cat('10', val)
|
||||
text = (f"Показатель прекращения финансовых операций оценивается среднедневным количеством "
|
||||
f"операций, проведенных клиентами по банковским счетам и (или) без открытия банковского "
|
||||
f"счета. Количество операций за прошедший год: {ops:,.2f} млн ед. Количество календарных "
|
||||
f"дней в году: {days:.0f}. Среднедневное количество операций: {ops:,.2f} / {days:.0f} "
|
||||
f"= {val:.6f} млн ед./день.")
|
||||
else:
|
||||
value = _num(getattr(company, 'criterion10_value', '') or '')
|
||||
if value is None:
|
||||
return (f"Показатель применим. Для расчёта заполните значение показателя "
|
||||
f"«{num}» в данных организации.", None)
|
||||
val = value
|
||||
cat = _criterion_10_cat(num, val)
|
||||
text = (f"Показатель {num} оценивается значением показателя: {val:,.6f} "
|
||||
f"(в единицах, установленных ПП-127).")
|
||||
if cat:
|
||||
text += (f" Согласно Постановлению № 127 полученное значение соответствует "
|
||||
f"{cat}-й категории значимости, по данному показателю объекту присваивается "
|
||||
f"{cat}-я категория значимости.")
|
||||
else:
|
||||
text += (" Значение не достигает порога III категории — по данному показателю "
|
||||
"категория значимости не присваивается.")
|
||||
return text, cat
|
||||
|
||||
|
||||
# Пороги количественных показателей 1–7, 11–14 (ПП-127), кроме 8/9/10:
|
||||
# num → [(категория, lo, lo_incl, hi, hi_incl)] — значение соответствует, если
|
||||
# (lo is None или value > lo при not lo_incl / value >= lo при lo_incl) и
|
||||
# (hi is None или value < hi при not hi_incl / value <= hi при hi_incl).
|
||||
CRITERION_QUANT_THRESHOLDS = {
|
||||
'1': [('III', 1, True, 50, True), ('II', 50, False, 500, True), ('I', 500, False, None, False)], # человек
|
||||
'2б': [('III', 2, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. чел.
|
||||
'3б': [('III', 2, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. чел.
|
||||
'4а': [('III', 3, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. абонентов
|
||||
'5а': [('III', 12, False, 24, True), ('II', 6, False, 12, True), ('I', None, False, 6, True)], # часов
|
||||
'5б': [('III', None, False, 30, True), ('II', 30, False, 70, True), ('I', 70, False, None, False)], # %
|
||||
'11б': [('III', 2, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. чел.
|
||||
'13а': [('III', 0, False, 10, True), ('II', 10, False, 15, True), ('I', 15, False, None, False)], # %
|
||||
'13б': [('III', 0, False, 10, True), ('II', 10, False, 40, True), ('I', 40, False, None, False)], # %
|
||||
'14': [('III', 2, False, 4, True), ('II', 1, False, 2, True), ('I', None, False, 1, True)], # часов
|
||||
}
|
||||
|
||||
# Качественные показатели (выбор одного из вариантов III/II/I): 2а, 3а, 3в, 3г, 4б, 6, 7, 11а, 12, 13.1
|
||||
CRITERION_CHOICE_INDICATORS = {'2а', '3а', '3в', '3г', '4б', '6', '7', '11а', '12', '13.1'}
|
||||
|
||||
# Единицы измерения количественных показателей (для подписи в окне авторасчёта)
|
||||
CRITERION_QUANT_UNITS = {
|
||||
'1': 'человек', '2б': 'тыс. человек', '3б': 'тыс. человек', '4а': 'тыс. абонентов',
|
||||
'5а': 'часов', '5б': '%', '11б': 'тыс. человек', '13а': '%', '13б': '%', '14': 'часов',
|
||||
}
|
||||
|
||||
|
||||
def _criterion_other_cat(num, value):
|
||||
"""Категория количественного показателя (кроме 8/9/10) по значению (ПП-127)."""
|
||||
rules = CRITERION_QUANT_THRESHOLDS.get(str(num))
|
||||
if not rules or value is None:
|
||||
return None
|
||||
for cat, lo, lo_incl, hi, hi_incl in rules:
|
||||
ok = True
|
||||
if lo is not None:
|
||||
ok = ok and ((value >= lo) if lo_incl else (value > lo))
|
||||
if hi is not None:
|
||||
ok = ok and ((value <= hi) if hi_incl else (value < hi))
|
||||
if ok:
|
||||
return cat
|
||||
return None
|
||||
|
||||
|
||||
def _calc_criterion_other(company, isys, num, value=None, choice=None):
|
||||
"""Расчёт остальных показателей критериев значимости (1–7, 11–14).
|
||||
|
||||
Количественные (1, 2б, 3б, 4а, 5а, 5б, 11б, 13а, 13б, 14): value — число,
|
||||
категория по порогам ПП-127 (CRITERION_QUANT_THRESHOLDS).
|
||||
Качественные (2а, 3а, 3в, 3г, 4б, 6, 7, 11а, 12, 13.1): choice — текст
|
||||
выбранного варианта из criteria_127.json (values), категория — его ключ.
|
||||
Возвращает (текст_обоснования, категория или None).
|
||||
"""
|
||||
num = str(num)
|
||||
if num in CRITERION_CHOICE_INDICATORS:
|
||||
# Качественный показатель: выбираем один из вариантов III/II/I
|
||||
criteria = _load_json('criteria_127.json')
|
||||
for grp in (criteria if isinstance(criteria, list) else []):
|
||||
for ind in grp.get('indicators', []):
|
||||
if str(ind.get('num', '')) == num:
|
||||
vals = ind.get('values', {}) or {}
|
||||
if not choice:
|
||||
return (f"Показатель применим. Для расчёта выберите один из вариантов "
|
||||
f"(III/II/I категория) в окне авторасчёта.", None)
|
||||
cat = None
|
||||
for c, txt in vals.items():
|
||||
if txt and str(txt).strip() == str(choice).strip():
|
||||
cat = c
|
||||
break
|
||||
if not cat:
|
||||
return (f"Показатель применим. Выбранный вариант не найден в перечне значений.", None)
|
||||
text = (f"Показатель {num}: {choice}. Согласно Постановлению № 127 данному значению "
|
||||
f"соответствует {cat}-я категория значимости.")
|
||||
return text, cat
|
||||
return (f"Показатель применим. Варианты значений не найдены в справочнике.", None)
|
||||
|
||||
# Количественный показатель
|
||||
if value is None:
|
||||
unit = CRITERION_QUANT_UNITS.get(num, '')
|
||||
return (f"Показатель применим. Для расчёта введите значение показателя"
|
||||
+ (f" ({unit})" if unit else "") + ".", None)
|
||||
cat = _criterion_other_cat(num, value)
|
||||
unit = CRITERION_QUANT_UNITS.get(num, '')
|
||||
text = (f"Показатель {num}: значение показателя — {value:,.6f} {unit}. "
|
||||
f"Согласно Постановлению № 127 ")
|
||||
if cat:
|
||||
text += (f"данному значению соответствует {cat}-я категория значимости.")
|
||||
else:
|
||||
text += ("значение не достигает порога III категории — категория значимости не присваивается.")
|
||||
return text, cat
|
||||
|
||||
|
||||
def _build_org_measures_text(company, isys=None):
|
||||
"""Формирует текст организационных мер из словаря org_measures.json
|
||||
с учётом ответов раздела «Физическая безопасность» опросника.
|
||||
|
||||
Если в опроснике указано «нет» (охрана, сигнализация, тревожная кнопка,
|
||||
видеонаблюдение, металлические двери и т.п.) — соответствующие пункты
|
||||
из текста убираются. Пункты, не привязанные к средствам, остаются всегда.
|
||||
"""
|
||||
import os as _os
|
||||
items = _load_json('org_measures.json')
|
||||
if not isinstance(items, list):
|
||||
items = []
|
||||
ps = getattr(company, 'physical_security', None) or {}
|
||||
|
||||
def _no(*keys):
|
||||
"""True, если хотя бы по одному ключу явно указано «нет»/«не имеется»."""
|
||||
for k in keys:
|
||||
v = str(ps.get(k) or '').strip().lower()
|
||||
if v in ('нет', 'не имеется', 'нет,', '0', 'false'):
|
||||
return True
|
||||
return False
|
||||
|
||||
rules = [
|
||||
# (подстрока пункта, ключи physical_security, при которых пункт УБИРАЕМ)
|
||||
('Физическая охрана периметра', ('physicalGuard', 'physicalTurnstile', 'physicalVisitorLog')),
|
||||
('Имеется система видеонаблюдения', ('physicalVideoSurveillance',)),
|
||||
('Здания не имеют окон', ('physicalMetalDoors',)),
|
||||
('охранно-пожарная сигнализация и тревожная кнопка', ('physicalFireAlarm', 'physicalPanicButton')),
|
||||
('охранно-пожарная сигнализация', ('physicalFireAlarm',)),
|
||||
('тревожная кнопка', ('physicalPanicButton',)),
|
||||
('турникетом на проходной', ('physicalTurnstile',)),
|
||||
('СКУД', ('physicalTurnstile',)),
|
||||
('учет посетителей', ('physicalVisitorLog',)),
|
||||
]
|
||||
|
||||
out = []
|
||||
for line in items:
|
||||
line = str(line).strip()
|
||||
if not line:
|
||||
continue
|
||||
drop = False
|
||||
for sub, keys in rules:
|
||||
if sub in line and _no(*keys):
|
||||
drop = True
|
||||
break
|
||||
if not drop:
|
||||
out.append(line)
|
||||
return '\n'.join(out) if out else '—'
|
||||
|
||||
|
||||
def _build_categorization_indicators(company, is_list):
|
||||
"""Строки таблицы показателей критериев значимости для шаблона
|
||||
«Сведения о результатах категорирования» (цикл categorization_indicators).
|
||||
@@ -530,6 +874,9 @@ def _build_categorization_indicators(company, is_list):
|
||||
isys = is_list[0] if is_list else None
|
||||
|
||||
rows = []
|
||||
first_cat_num = None # номер показателя, по которому объект отнесён к I категории (п. 6 ПП-127)
|
||||
# Введённые значения показателей (из опросника/окна авторасчёта): {num: значение}
|
||||
crit_in = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
|
||||
for grp in criteria:
|
||||
section = grp.get('criteria', '')
|
||||
for ind in grp.get('indicators', []):
|
||||
@@ -542,23 +889,86 @@ def _build_categorization_indicators(company, is_list):
|
||||
value = '—'
|
||||
justification = '—'
|
||||
category = '—'
|
||||
if not applicable:
|
||||
if first_cat_num is not None:
|
||||
# Остановка расчёта: объект уже отнесён к I категории по одному из показателей
|
||||
# (п. 6 Правил, утв. ПП РФ № 127: «расчет по остальным показателям не проводится»).
|
||||
if applicable:
|
||||
value = '—'
|
||||
justification = (f"Расчёт по данному показателю не проводится: объект отнесён к "
|
||||
f"I категории значимости по показателю {first_cat_num} "
|
||||
f"(п. 6 Правил категорирования, утв. ПП РФ № 127).")
|
||||
category = '—'
|
||||
else:
|
||||
value = 'Показатель не применим.'
|
||||
justification = (just.get(num) or just.get(grp_num)
|
||||
or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».")
|
||||
elif not applicable:
|
||||
value = 'Показатель не применим.'
|
||||
justification = (just.get(num) or just.get(grp_num)
|
||||
or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».")
|
||||
elif grp_num in ('8', '9'):
|
||||
text, cat = _calc_criterion_8_9(company, isys, grp_num)
|
||||
if grp_num == '8' and sphere == 'Оборонная промышленность':
|
||||
# ОПК: U_8 = (C+A+X)/R_год×100% (ПП РФ № 796, п. 18–20)
|
||||
text, cat = _calc_criterion_8_opk(company, isys)
|
||||
else:
|
||||
text, cat = _calc_criterion_8_9(company, isys, grp_num)
|
||||
if text:
|
||||
value = text
|
||||
if cat:
|
||||
category = f"{cat}-я категория"
|
||||
else:
|
||||
category = '—'
|
||||
elif grp_num == '10':
|
||||
text, cat = _calc_criterion_10(company, isys, num)
|
||||
if text:
|
||||
value = text
|
||||
if cat:
|
||||
category = f"{cat}-я категория"
|
||||
else:
|
||||
category = '—'
|
||||
elif num in ('13а', '13б') and sphere == 'Оборонная промышленность':
|
||||
# ОПК: V = (1−(V_max−V_инц)/V_ГОЗ)×100%, T = (1−(t_норм/t_ненорм))×100%
|
||||
# (ПП РФ № 796, п. 34, 38) — при наличии введённых составляющих
|
||||
_opk_crit = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
|
||||
_opk_ready = bool(_opk_crit.get('13a_vmax', '') or _opk_crit.get('13a_vinc', '') or _opk_crit.get('13a_vgoz', '') or _opk_crit.get('13b_tnorm', '') or _opk_crit.get('13b_tnonorm', ''))
|
||||
if _opk_ready:
|
||||
text, cat = _calc_criterion_13_opk(company, isys, num)
|
||||
if text:
|
||||
value = text
|
||||
if cat:
|
||||
category = f"{cat}-я категория"
|
||||
else:
|
||||
category = '—'
|
||||
else:
|
||||
value = '—'
|
||||
justification = (just.get(num) or just.get(grp_num)
|
||||
or 'Показатель применим к объекту КИИ; значение определяется по данным объекта (для ОПК — по формулам ПП РФ № 796: V = (1−(V_max−V_инц)/V_ГОЗ)×100%, T = (1−(t_норм/t_ненорм))×100%).')
|
||||
else:
|
||||
# Применимый показатель без авторасчёта — оператор заполняет значение
|
||||
value = '—'
|
||||
justification = (just.get(num) or just.get(grp_num)
|
||||
or 'Показатель применим к объекту КИИ; значение определяется по данным объекта.')
|
||||
# Применимый показатель без формульного авторасчёта (1–7, 11–14):
|
||||
# если значение введено (опросник/окно авторасчёта) — считаем категорию по порогам
|
||||
if num in crit_in and str(crit_in.get(num, '')).strip():
|
||||
raw = str(crit_in.get(num, '')).strip()
|
||||
# Число или выбранный вариант (качественный)
|
||||
try:
|
||||
_v = float(raw.replace(' ', '').replace(',', '.').replace('\u00a0', ''))
|
||||
text, cat = _calc_criterion_other(company, isys, num, value=_v)
|
||||
except ValueError:
|
||||
text, cat = _calc_criterion_other(company, isys, num, choice=raw)
|
||||
if text:
|
||||
value = text
|
||||
if cat:
|
||||
category = f"{cat}-я категория"
|
||||
else:
|
||||
category = '—'
|
||||
else:
|
||||
value = '—'
|
||||
justification = (just.get(num) or just.get(grp_num)
|
||||
or 'Показатель применим к объекту КИИ; значение определяется по данным объекта.')
|
||||
|
||||
# Если по показателю присвоена I категория — фиксируем для остановки расчёта
|
||||
# (точное совпадение «I-я категория», а не startswith('I'), иначе ловится II/III)
|
||||
if first_cat_num is None and category.startswith('I-я'):
|
||||
first_cat_num = num
|
||||
|
||||
rows.append({
|
||||
'section': section,
|
||||
|
||||
+61
-10
@@ -161,15 +161,20 @@ def normalize_address(text):
|
||||
|
||||
|
||||
def resolve_okved(code, dadata_token=None):
|
||||
"""Код ОКВЭД → «код - название».
|
||||
"""Код ОКВЭД → «код название» (без дефиса), например «27.32.2 Производство силовых кабелей».
|
||||
|
||||
Принимает и голый код («27.32.2»), и строку с названием
|
||||
(«27.32.2 - Производство силовых кабелей» / «27.32.2 Производство силовых кабелей»).
|
||||
1) Ищем в словаре dictionaries/okved.json (строки вида «27.32.2 Название»).
|
||||
2) Если не нашли — запрашиваем DaData suggest okved2 (если есть токен).
|
||||
3) Иначе возвращаем код как есть.
|
||||
3) Иначе возвращаем нормализованную строку «код название» как есть.
|
||||
"""
|
||||
code = (code or '').strip()
|
||||
if not code:
|
||||
return ''
|
||||
# Извлекаем код из входной строки: «27.32.2 - Производство силовых кабелей» → «27.32.2»
|
||||
m = re.match(r'^\s*(\d+(?:\.\d+)*)', code)
|
||||
code_only = m.group(1) if m else code.split()[0].rstrip('.')
|
||||
# 1) Словарь
|
||||
try:
|
||||
import os, json
|
||||
@@ -178,12 +183,8 @@ def resolve_okved(code, dadata_token=None):
|
||||
items = json.load(f)
|
||||
for item in items:
|
||||
item = str(item).strip()
|
||||
# совпадение по началу: «27.32.2» или «27.32.2 - ...» / «27.32.2 ...»
|
||||
if item.split()[0].rstrip('.') == code.rstrip('.'):
|
||||
# нормализуем: «код - название»
|
||||
rest = item[len(item.split()[0]):].strip().lstrip('- ').strip()
|
||||
if rest:
|
||||
return f"{code} - {rest}"
|
||||
# совпадение по коду: «27.32.2» или «27.32.2 Название»
|
||||
if item.split()[0].rstrip('.') == code_only.rstrip('.'):
|
||||
return item
|
||||
except Exception:
|
||||
pass
|
||||
@@ -191,7 +192,7 @@ def resolve_okved(code, dadata_token=None):
|
||||
if dadata_token:
|
||||
try:
|
||||
js, err = _http_json('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/okved2',
|
||||
data={'query': code, 'count': 1},
|
||||
data={'query': code_only, 'count': 1},
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Authorization': f'Token {dadata_token}'})
|
||||
if not err and js:
|
||||
@@ -200,7 +201,12 @@ def resolve_okved(code, dadata_token=None):
|
||||
return sugg[0]['value']
|
||||
except Exception:
|
||||
pass
|
||||
return code
|
||||
# 3) Нормализуем вход: «27.32.2 - Название» → «27.32.2 Название» (без дефиса)
|
||||
rest = code[len(m.group(1)):].strip() if m else ''
|
||||
rest = rest.lstrip('- ').strip()
|
||||
if rest:
|
||||
return f"{code_only} {rest}"
|
||||
return code_only
|
||||
|
||||
|
||||
def _fmt_date(value):
|
||||
@@ -538,6 +544,51 @@ def _norm_threat_text(text):
|
||||
return s.lower()
|
||||
|
||||
|
||||
def normalize_threats(text):
|
||||
"""Склеить переносы внутри названий угроз.
|
||||
|
||||
В опросниках встречается «УБИ.209 Угроза … памяти\r\nядра процессора» —
|
||||
перенос строки внутри одного названия. Строка, не начинающаяся с «УБИ.»,
|
||||
считается продолжением предыдущей угрозы.
|
||||
"""
|
||||
if not text:
|
||||
return ''
|
||||
lines = [ln.strip() for ln in str(text).split('\n') if ln.strip()]
|
||||
out = []
|
||||
for ln in lines:
|
||||
if re.match(r'^уби\.?\s*\d+', ln, re.IGNORECASE):
|
||||
out.append(ln)
|
||||
elif out:
|
||||
out[-1] = out[-1] + ' ' + ln
|
||||
else:
|
||||
out.append(ln)
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
def split_okved_extra(value):
|
||||
"""Разбить okvedExtra (строка или список строк с переносами) на записи «код название».
|
||||
|
||||
В опросниках доп. ОКВЭД часто идут склеенными:
|
||||
«25.93 — Производство изделий из проволоки\nцепей и пружин 32.12.1 — …»
|
||||
Здесь «цепей и пружин» — продолжение названия 25.93, а 32.12.1 — новый код.
|
||||
Нарезаем по кодам ОКВЭД (двузначный код с точками в начале записи).
|
||||
"""
|
||||
if isinstance(value, (list, tuple)):
|
||||
text = ' '.join(str(x) for x in value)
|
||||
else:
|
||||
text = str(value or '')
|
||||
text = text.replace('\r', ' ').replace('\n', ' ')
|
||||
# Нарезаем по началу кода ОКВЭД: «25.93», «32.12.1», «43.2» …
|
||||
# Перед кодом не должно быть цифры или точки (чтобы не разрезать «25.93» на «25» и «93»)
|
||||
parts = re.split(r'(?<!\d)(?<!\.)(?=\d{2}(?:\.\d+)*\b)', text)
|
||||
out = []
|
||||
for p in parts:
|
||||
p = p.strip().strip('-—– ').strip()
|
||||
if p and re.match(r'^\d{2}(?:\.\d+)*', p):
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def filter_threats_by_dict(lines, threats_full=None):
|
||||
"""Приводит строки угроз (от ИИ или оператора) к каноническому виду по словарю УБИ.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user