Compare commits
26
Commits
868e0065f3
...
source
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
691b167725 | ||
|
|
a7878804e4 | ||
|
|
8e97e10714 | ||
|
|
dbf8c613dd | ||
|
|
b2b68258fd | ||
|
|
1840f1e2d4 | ||
|
|
e15610ba29 | ||
|
|
36d2d5a87b | ||
|
|
c90660a0f8 | ||
|
|
5bb6a2f5e9 | ||
|
|
829a1a98e3 | ||
|
|
ad54d3e0e2 | ||
|
|
9f8bf3999d | ||
|
|
07bf25ebc3 | ||
|
|
0b3ef66125 | ||
|
|
1793182735 | ||
|
|
52b4ee6f1a | ||
|
|
c9dffdd4ca | ||
|
|
136c58c948 | ||
|
|
28fa7edaa8 | ||
|
|
614044a6ca | ||
|
|
9a1058559a | ||
|
|
a8fa3f908c | ||
|
|
033c21fccc | ||
|
|
bf75d67f5e | ||
|
|
5af23ae525 |
@@ -3,3 +3,8 @@ __pycache__/
|
|||||||
.autosave/
|
.autosave/
|
||||||
Сгенерированные_документы/
|
Сгенерированные_документы/
|
||||||
*.result.docx
|
*.result.docx
|
||||||
|
|
||||||
|
# Секреты и локальные настройки — НЕ коммитить
|
||||||
|
api_settings.json
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|||||||
+10
-4
@@ -13,11 +13,17 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# ВНИМАНИЕ: реальные ключи НЕ хранятся в коде (утечка в git-историю — см. аудит).
|
||||||
|
# Источники ключей (по приоритету):
|
||||||
|
# 1) переменные окружения DADATA_TOKEN / DEEPSEEK_API_KEY / DEEPSEEK_MODEL / DEEPSEEK_BASE_URL
|
||||||
|
# 2) файл api_settings.json рядом с программой
|
||||||
|
# 3) DEFAULT_SETTINGS ниже (пустые значения)
|
||||||
|
|
||||||
DEFAULT_SETTINGS = {
|
DEFAULT_SETTINGS = {
|
||||||
'dadata_token': '431aa11706bf83f571cef0319259289aa750bcb1',
|
'dadata_token': os.environ.get('DADATA_TOKEN', ''),
|
||||||
'ai_api_key': 'sk-3c7e6673f245412697d0e14b6096435f',
|
'ai_api_key': os.environ.get('DEEPSEEK_API_KEY', ''),
|
||||||
'ai_model': 'deepseek-chat',
|
'ai_model': os.environ.get('DEEPSEEK_MODEL', 'deepseek-chat'),
|
||||||
'ai_base_url': 'https://api.deepseek.com',
|
'ai_base_url': os.environ.get('DEEPSEEK_BASE_URL', 'https://api.deepseek.com'),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Старые ключи (для совместимости со старым файлом api_settings.json)
|
# Старые ключи (для совместимости со старым файлом api_settings.json)
|
||||||
|
|||||||
Binary file not shown.
@@ -16,19 +16,27 @@
|
|||||||
],
|
],
|
||||||
"Наука": [
|
"Наука": [
|
||||||
"1",
|
"1",
|
||||||
"11",
|
|
||||||
"13",
|
|
||||||
"7",
|
"7",
|
||||||
"8",
|
"8",
|
||||||
"9"
|
"9",
|
||||||
|
"11",
|
||||||
|
"13"
|
||||||
],
|
],
|
||||||
"Транспорт": [
|
"Транспорт": [
|
||||||
"10",
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
"11",
|
"11",
|
||||||
"3"
|
"13",
|
||||||
|
"14"
|
||||||
],
|
],
|
||||||
"Связь": [
|
"Связь": [
|
||||||
"4"
|
"4",
|
||||||
|
"6",
|
||||||
|
"8",
|
||||||
|
"9"
|
||||||
],
|
],
|
||||||
"Энергетика": [
|
"Энергетика": [
|
||||||
"1",
|
"1",
|
||||||
@@ -61,6 +69,8 @@
|
|||||||
"14"
|
"14"
|
||||||
],
|
],
|
||||||
"Банковская сфера и финансовый рынок": [
|
"Банковская сфера и финансовый рынок": [
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
"10"
|
"10"
|
||||||
],
|
],
|
||||||
"Топливно-энергетический комплекс": [
|
"Топливно-энергетический комплекс": [
|
||||||
@@ -82,20 +92,26 @@
|
|||||||
"1",
|
"1",
|
||||||
"2",
|
"2",
|
||||||
"3",
|
"3",
|
||||||
"4",
|
|
||||||
"5",
|
"5",
|
||||||
|
"6",
|
||||||
"7",
|
"7",
|
||||||
"8",
|
"8",
|
||||||
"9",
|
"9",
|
||||||
"10",
|
|
||||||
"11",
|
"11",
|
||||||
"12",
|
"12",
|
||||||
"13",
|
"13"
|
||||||
"14"
|
|
||||||
],
|
],
|
||||||
"Оборонная промышленность": [
|
"Оборонная промышленность": [
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
"8",
|
"8",
|
||||||
"9",
|
"9",
|
||||||
|
"11",
|
||||||
|
"12",
|
||||||
"13"
|
"13"
|
||||||
],
|
],
|
||||||
"Ракетно-космическая промышленность": [
|
"Ракетно-космическая промышленность": [
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"Здравоохранение": {
|
"Здравоохранение": {
|
||||||
"1": "Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания медицинской помощи, проведения медицинских экспертиз, медицинских осмотров и медицинских освидетельствований, санитарно-противоэпидемических (профилактических) мероприятий и не осуществляет управление, контроль или мониторинг данных бизнес-процессов. Объект является вспомогательной информационной системой, целью которой является автоматизация процессов ведения бухгалтерского учета. Объект не осуществляет мониторинг, контроль или управление другими объектами КИИ, обеспечивающими жизненно важные функции организма. Утрата или иное повреждение объекта КИИ не может повлиять на жизненно важные функции организма.",
|
"1": "Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания медицинской помощи, проведения медицинских экспертиз, медицинских осмотров и медицинских освидетельствований, санитарно-противоэпидемических (профилактических) мероприятий и не осуществляет управление, контроль или мониторинг данных бизнес-процессов. Объект является вспомогательной информационной системой, целью которой является автоматизация процессов ведения бухгалтерского учета. Объект не осуществляет мониторинг, контроль или управление другими объектами КИИ, обеспечивающими жизненно важные функции организма. Утрата или иное повреждение объекта КИИ не может повлиять на жизненно важные функции организма.",
|
||||||
"2": "Объект КИИ не относится к объектам обеспечения жизнедеятельности населения, в том числе объектам водоснабжения и канализации, очистки сточных вод, тепло- и электроснабжения, гидротехнических сооружений.",
|
"2": "Объект КИИ не относится к объектам обеспечения жизнедеятельности населения, в том числе объектам водоснабжения и канализации, очистки сточных вод, тепло- и электроснабжения, гидротехнических сооружений.",
|
||||||
"3": "Объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры, и субъект КИИ не имеет в своем составе подразделения транспортной инфраструктуры.",
|
"3": "Объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры, и субъект КИИ не имеет в своем составе подразделения транспортной инфраструктуры.",
|
||||||
"4": "Субъект КИИ не является оператором связи, объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры.",
|
"4": "Субъект КИИ не является оператором связи, объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры.",
|
||||||
"5": "Организация сферы здравоохранения является юридическим лицом, обеспечивающим эксплуатацию информационной системы, задействованной в процессе оказания государственной услуги, однако нарушение функционирования информационной системы (рассматриваемого объекта КИИ), не участвующей в обработке информации, необходимой для оказания государственной услуги, не влечет за собой нарушение и (или) прекращение доступа к государственной услуге. Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания государственной услуги.",
|
"5": "Организация сферы здравоохранения является юридическим лицом, обеспечивающим эксплуатацию информационной системы, задействованной в процессе оказания государственной услуги, однако нарушение функционирования информационной системы (рассматриваемого объекта КИИ), не участвующей в обработке информации, необходимой для оказания государственной услуги, не влечет за собой нарушение и (или) прекращение доступа к государственной услуге. Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания государственной услуги.",
|
||||||
"6": "Организация сферы здравоохранения не является органом государственной власти, в связи с этим критерий неприменим.",
|
"6": "Организация сферы здравоохранения не является органом государственной власти, в связи с этим критерий неприменим.",
|
||||||
"7": "Организация сферы здравоохранения не является субъектом политической сферы деятельности, в связи с этим критерий неприменим.",
|
"7": "Организация сферы здравоохранения не является субъектом политической сферы деятельности, в связи с этим критерий неприменим.",
|
||||||
"8": "Организация сферы здравоохранения не является государственной корпорацией, государственным унитарным предприятием, государственной компанией, стратегическим акционерным обществом, стратегическим предприятием, в связи с этим критерий неприменим.",
|
"8": "Организация сферы здравоохранения не является государственной корпорацией, государственным унитарным предприятием, государственной компанией, стратегическим акционерным обществом, стратегическим предприятием, в связи с этим критерий неприменим.",
|
||||||
"9": "Объект КИИ задействован только в предоставлении медицинских услуг, освобожденных от налогообложения, в связи с этим критерий неприменим.",
|
"9": "Объект КИИ задействован в предоставлении медицинских услуг. Показатель 9 применим ко всем субъектам КИИ; при расчёте учитывается, что медицинские услуги освобождены от налогообложения (значение показателя может быть минимальным).",
|
||||||
"10": "Организация сферы здравоохранения не является субъектом банковской сферы деятельности.",
|
"10": "Организация сферы здравоохранения не является субъектом банковской сферы деятельности.",
|
||||||
"10.1": "Организация сферы здравоохранения не является центральным контрагентом.",
|
"10.1": "Организация сферы здравоохранения не является центральным контрагентом.",
|
||||||
"10.2": "Организация сферы здравоохранения не является центральным депозитарием.",
|
"10.2": "Организация сферы здравоохранения не является центральным депозитарием.",
|
||||||
"10.3": "Организация сферы здравоохранения не является негосударственным пенсионным фондом.",
|
"10.3": "Организация сферы здравоохранения не является негосударственным пенсионным фондом.",
|
||||||
"10.4": "Организация сферы здравоохранения не является страховой организацией.",
|
"10.4": "Организация сферы здравоохранения не является страховой организацией.",
|
||||||
"10.5": "Организация сферы здравоохранения не является оператором услуг информационного обмена.",
|
"10.5": "Организация сферы здравоохранения не является оператором услуг информационного обмена.",
|
||||||
"11": "Объекты КИИ организации сферы здравоохранения не относятся к опасным производственным объектам в соответствии с законодательством Российской Федерации.",
|
"11": "Объекты КИИ организации сферы здравоохранения не относятся к опасным производственным объектам в соответствии с законодательством Российской Федерации.",
|
||||||
"12": "Организация сферы здравоохранения не выполняет функции пункта управления (ситуационного центра) государственного органа власти или государственной корпорации.",
|
"12": "Организация сферы здравоохранения не выполняет функции пункта управления (ситуационного центра) государственного органа власти или государственной корпорации.",
|
||||||
"13": "Организация сферы здравоохранения не является субъектом обеспечения обороны страны, безопасности государства и правопорядка.",
|
"13": "Организация сферы здравоохранения не является субъектом обеспечения обороны страны, безопасности государства и правопорядка.",
|
||||||
"14": "Организация сферы здравоохранения не имеет информационных систем, функционирующих в области обеспечения обороны страны, безопасности государства и правопорядка."
|
"14": "Организация сферы здравоохранения не имеет информационных систем, функционирующих в области обеспечения обороны страны, безопасности государства и правопорядка."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+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, если текущий набор СЗИ покрывает требования категории. Не придумывай конкретные торговые марки, если их нет в списке — указывай класс средства."
|
"response_format": "Верни ТОЛЬКО JSON: {\"рекомендации\": [{\"сзи\": \"название класса/средства\", \"зачем\": \"для чего нужно\"}], \"общие_советы\": [\"...\"], \"достаточно\": true|false}. Поле «достаточно» — true, если текущий набор СЗИ покрывает требования категории. Не придумывай конкретные торговые марки, если их нет в списке — указывай класс средства."
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+10
-7
@@ -310,10 +310,14 @@ def _subst_var_xml(xml: str, var: str, val: str) -> str:
|
|||||||
+ '</w:r><w:r><w:t xml:space="preserve">')
|
+ '</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 -->
|
"""Разворачивает вложенные циклы вида <!-- loop:users_loop --> ... <!-- loop_end -->
|
||||||
данными из item (список словарей). Используется для циклов внутри цикла ИС,
|
данными из item (список словарей) или из общего словаря циклов loops.
|
||||||
например пользователи ИС. Переменные внутри: {{user_fio}}, {{user_position}}."""
|
Используется для циклов внутри цикла ИС, например пользователи ИС,
|
||||||
|
а также для вложенных таблиц (показатели значимости внутри объекта КИИ).
|
||||||
|
Переменные внутри: {{user_fio}}, {{user_position}}, {{item.*}}."""
|
||||||
|
if loops is None:
|
||||||
|
loops = {}
|
||||||
result = template
|
result = template
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
m_start = LOOP_START.search(result)
|
m_start = LOOP_START.search(result)
|
||||||
@@ -321,6 +325,8 @@ def _expand_nested_loops(template: str, item: Dict) -> str:
|
|||||||
break
|
break
|
||||||
key = m_start.group(1)
|
key = m_start.group(1)
|
||||||
data = item.get(key)
|
data = item.get(key)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
data = loops.get(key)
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
# Нет данных для вложенного цикла — удаляем блок целиком
|
# Нет данных для вложенного цикла — удаляем блок целиком
|
||||||
depth = 1
|
depth = 1
|
||||||
@@ -478,7 +484,7 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
|
|||||||
clone = template_clean
|
clone = template_clean
|
||||||
# Вложенные циклы (users_loop и т.п.) — разворачиваем ДО подстановки
|
# Вложенные циклы (users_loop и т.п.) — разворачиваем ДО подстановки
|
||||||
# обычных полей, чтобы {{user_fio}} внутри них не затёрлись алиасами
|
# обычных полей, чтобы {{user_fio}} внутри них не затёрлись алиасами
|
||||||
clone = _expand_nested_loops(clone, item)
|
clone = _expand_nested_loops(clone, item, loops)
|
||||||
for field, value in item.items():
|
for field, value in item.items():
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
continue # списки — данные вложенных циклов, не подставляем
|
continue # списки — данные вложенных циклов, не подставляем
|
||||||
@@ -692,9 +698,6 @@ def _highlight_unmatched(doc):
|
|||||||
pos = m.end()
|
pos = m.end()
|
||||||
if pos < len(full):
|
if pos < len(full):
|
||||||
_add_run_with_tabs(para, full[pos:], first)
|
_add_run_with_tabs(para, full[pos:], first)
|
||||||
run = para.add_run(full[pos:])
|
|
||||||
if first is not None:
|
|
||||||
_copy_run_style(first, run)
|
|
||||||
|
|
||||||
for para in doc.paragraphs:
|
for para in doc.paragraphs:
|
||||||
_process_para(para)
|
_process_para(para)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def _generate_variables_reference(path):
|
|||||||
_p(doc, 'Список переменных для шаблонов Word. Переменная подставляется в текст '
|
_p(doc, 'Список переменных для шаблонов Word. Переменная подставляется в текст '
|
||||||
'как есть: {{имя_переменной}}. Циклы: <!-- loop:имя --> ... <!-- loop_end -->.')
|
'как есть: {{имя_переменной}}. Циклы: <!-- loop:имя --> ... <!-- loop_end -->.')
|
||||||
|
|
||||||
groups = {'A': 'Общие переменные', 'B': '187-ФЗ (персональные данные)'}
|
groups = {'A': 'Общие переменные', 'B': '187-ФЗ (критическая информационная инфраструктура)'}
|
||||||
by_group = {'A': [], 'B': []}
|
by_group = {'A': [], 'B': []}
|
||||||
for var, group, desc, _ in VARIABLE_DEFS:
|
for var, group, desc, _ in VARIABLE_DEFS:
|
||||||
by_group.setdefault(group, []).append((var, desc))
|
by_group.setdefault(group, []).append((var, desc))
|
||||||
|
|||||||
+434
-43
@@ -7,48 +7,439 @@
|
|||||||
],
|
],
|
||||||
"value_column": 1,
|
"value_column": 1,
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Полное наименование организации", "var": "fullName", "cell": "", "keywords": "полное наименование организации"},
|
{
|
||||||
{"name": "Сокращённое наименование организации", "var": "shortName", "cell": "", "keywords": "сокращенное наименование организации"},
|
"name": "Полное наименование организации",
|
||||||
{"name": "Юридический адрес", "var": "addressLegal", "cell": "", "keywords": "адрес организации \\(юридический\\)"},
|
"var": "fullName",
|
||||||
{"name": "Фактический адрес", "var": "addressActual", "cell": "", "keywords": "адрес организации \\(фактический\\)"},
|
"cell": "",
|
||||||
{"name": "Электронная почта", "var": "email", "cell": "", "keywords": "электронная почта"},
|
"keywords": "полное наименование организации"
|
||||||
{"name": "Телефон", "var": "phone", "cell": "", "keywords": "телефон"},
|
},
|
||||||
{"name": "ИНН", "var": "inn", "cell": "", "keywords": "инн"},
|
{
|
||||||
{"name": "ОГРН", "var": "ogrn", "cell": "", "keywords": "огрн"},
|
"name": "Сокращённое наименование организации",
|
||||||
{"name": "Дата получения ОГРН", "var": "ogrnDate", "cell": "", "keywords": "дата получения огрн|дата.*огрн"},
|
"var": "shortName",
|
||||||
{"name": "КПП", "var": "kpp", "cell": "", "keywords": "кпп"},
|
"cell": "",
|
||||||
{"name": "Основной ОКВЭД", "var": "okved", "cell": "", "keywords": "основной оквэд"},
|
"keywords": "сокращенное наименование организации"
|
||||||
{"name": "Дополнительные ОКВЭД", "var": "okvedExtra", "cell": "", "keywords": "дополнительные оквэд"},
|
},
|
||||||
{"name": "Должность руководителя", "var": "chiefPosition", "cell": "", "keywords": "должность руководителя"},
|
{
|
||||||
{"name": "ФИО руководителя", "var": "chiefFio", "cell": "", "keywords": "фио руководителя"},
|
"name": "Юридический адрес",
|
||||||
{"name": "Должность ответственного лица (безопасность ЗОКИИ)", "var": "responsiblePosition", "cell": "", "keywords": "должность ответственного лица"},
|
"var": "addressLegal",
|
||||||
{"name": "ФИО ответственного лица (безопасность ЗОКИИ)", "var": "responsibleFio", "cell": "", "keywords": "фио ответственного лица"},
|
"cell": "",
|
||||||
{"name": "Подразделение по безопасности ОКИИ", "var": "ispdnDepartment", "cell": "", "keywords": "структур.*подраздел"},
|
"keywords": "адрес организации \\(юридический\\)"
|
||||||
{"name": "Рабочий телефон ответственного", "var": "ispdnPhone", "cell": "", "keywords": "рабочий телефон ответственного|мобильный телефон ответственного"},
|
},
|
||||||
{"name": "Электронная почта ответственного", "var": "ispdnEmail", "cell": "", "keywords": "электронная почта ответственного"},
|
{
|
||||||
{"name": "Должность специалиста по ЗИ/администратора", "var": "administratorPosition", "cell": "", "keywords": "должность специалиста по защите информации|должность.*администрат"},
|
"name": "Фактический адрес",
|
||||||
{"name": "ФИО специалиста по ЗИ/администратора", "var": "administratorFio", "cell": "", "keywords": "фио специалиста по защите информации|фио.*администрат"},
|
"var": "addressActual",
|
||||||
{"name": "Право владения зданием", "var": "buildingRight", "cell": "", "keywords": "право владения зданием"},
|
"cell": "",
|
||||||
{"name": "Сумма прибыли за год", "var": "profit", "cell": "", "keywords": "сумма прибыли"},
|
"keywords": "адрес организации \\(фактический\\)"
|
||||||
{"name": "Сумма налога с прибыли", "var": "profitTax", "cell": "", "keywords": "сумма налога с прибыли"},
|
},
|
||||||
{"name": "Доменное имя и внешний IP", "var": "domainName", "cell": "", "keywords": "доменное имя и внешний сетевой адрес"},
|
{
|
||||||
{"name": "Сайт организации", "var": "site_name", "cell": "", "keywords": "сайт организации"},
|
"name": "Электронная почта",
|
||||||
{"name": "Должность ответственного за сайт", "var": "site_responsible_position", "cell": "", "keywords": "должность ответственного за сайт"},
|
"var": "email",
|
||||||
{"name": "ФИО ответственного за сайт", "var": "site_responsible_fio", "cell": "", "keywords": "фио ответственного за сайт"},
|
"cell": "",
|
||||||
{"name": "Разработчик сайта", "var": "site_service_provider", "cell": "", "keywords": "организации, которая разрабатывала и размещала корпоративный сайт"},
|
"keywords": "электронная почта"
|
||||||
{"name": "ИНН разработчика сайта", "var": "site_service_provider_inn", "cell": "", "keywords": "инн организации, которая разрабатывала"},
|
},
|
||||||
{"name": "Размещение сайта", "var": "site_hosting", "cell": "", "keywords": "размещение сайта"},
|
{
|
||||||
{"name": "Адрес расположения серверов", "var": "site_hosting_address", "cell": "", "keywords": "адрес расположения серверов"},
|
"name": "Телефон",
|
||||||
{"name": "Облачная платформа", "var": "cloud_platform", "cell": "", "keywords": "облачную платформу"},
|
"var": "phone",
|
||||||
{"name": "Аренда выделенного сервера", "var": "cloud_dedicated_server", "cell": "", "keywords": "выделенный сервер"},
|
"cell": "",
|
||||||
{"name": "Аренда виртуального ЦОД", "var": "cloud_vdc", "cell": "", "keywords": "виртуальный дата-центр"},
|
"keywords": "телефон"
|
||||||
{"name": "Серверы в дата-центре", "var": "cloud_datacenter", "cell": "", "keywords": "сервера в дата-центре"},
|
},
|
||||||
{"name": "Средства контейнеризации", "var": "cloud_container", "cell": "", "keywords": "контейнеризации"},
|
{
|
||||||
{"name": "Собственная серверная (ЦОД)", "var": "cloud_own_server", "cell": "", "keywords": "собственная серверная"},
|
"name": "ИНН",
|
||||||
{"name": "Общее количество компьютеров", "var": "computers_total", "cell": "", "keywords": "общее количество компьютеров"},
|
"var": "inn",
|
||||||
{"name": "Компьютеры, подключённые к объектам КИИ", "var": "computers_kii", "cell": "", "keywords": "компьютеров подключенных к объектам кии"},
|
"cell": "",
|
||||||
{"name": "Центр мониторинга", "var": "monitoring_center", "cell": "", "keywords": "к какому центру подключены"},
|
"keywords": "инн"
|
||||||
{"name": "Детали подключения к центру мониторинга", "var": "monitoring_details", "cell": "", "keywords": "детали подключения"},
|
},
|
||||||
{"name": "Дополнительная информация", "var": "additionalInfo", "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": "вариант"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+38
-5
@@ -97,13 +97,18 @@ def normalize_address(text: str) -> str:
|
|||||||
return ''.join(out)
|
return ''.join(out)
|
||||||
|
|
||||||
|
|
||||||
def clean_value(val):
|
def clean_value(val, as_date=False):
|
||||||
"""Очистка и нормализация значения ячейки Excel."""
|
"""Очистка и нормализация значения ячейки Excel.
|
||||||
|
|
||||||
|
as_date=True — конвертировать серийный номер Excel в дату (только для
|
||||||
|
полей-дат, напр. ogrnDate). Для финансовых полей НЕ вызывается,
|
||||||
|
иначе число 50 000 руб. превратится в «дату» (см. аудит importer.py:105-109).
|
||||||
|
"""
|
||||||
if val is None:
|
if val is None:
|
||||||
return ''
|
return ''
|
||||||
if isinstance(val, (int, float)):
|
if isinstance(val, (int, float)):
|
||||||
# Попытка распознать дату (серийный номер Excel)
|
# Попытка распознать дату (серийный номер Excel) — только для полей-дат
|
||||||
if 10000 < val < 80000:
|
if as_date and 10000 < val < 80000:
|
||||||
serial = int(val)
|
serial = int(val)
|
||||||
if serial > 60:
|
if serial > 60:
|
||||||
serial -= 1
|
serial -= 1
|
||||||
@@ -169,6 +174,21 @@ def import_187fz(filepath: str, log_fn: Optional[Callable] = None, settings_path
|
|||||||
finally:
|
finally:
|
||||||
wb.close()
|
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'),
|
for src, dst in [('ispdnFio', 'administratorFio'), ('ispdnFio', 'adminFio'),
|
||||||
('ispdnPosition', 'administratorPosition'), ('ispdnPosition', 'adminPosition'),
|
('ispdnPosition', 'administratorPosition'), ('ispdnPosition', 'adminPosition'),
|
||||||
@@ -253,7 +273,7 @@ def _import_187_common(wb, data, settings, log_fn):
|
|||||||
if sn.lower() == sheet_part.lower():
|
if sn.lower() == sheet_part.lower():
|
||||||
target_ws = wb[sn]
|
target_ws = wb[sn]
|
||||||
break
|
break
|
||||||
val = clean_value(cell_value(target_ws, coord))
|
val = clean_value(cell_value(target_ws, coord), as_date=('дата' in name.lower() or 'date' in key.lower()))
|
||||||
if val:
|
if val:
|
||||||
if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
||||||
val = _extract_single_fio(val)
|
val = _extract_single_fio(val)
|
||||||
@@ -273,6 +293,8 @@ def _import_187_common(wb, data, settings, log_fn):
|
|||||||
if val:
|
if val:
|
||||||
if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
||||||
val = _extract_single_fio(val)
|
val = _extract_single_fio(val)
|
||||||
|
elif 'дата' in name.lower() or 'date' in key.lower():
|
||||||
|
val = clean_value(val, as_date=True)
|
||||||
data[key] = val
|
data[key] = val
|
||||||
log_fn(f" {name} [по ключу]: {val}")
|
log_fn(f" {name} [по ключу]: {val}")
|
||||||
break
|
break
|
||||||
@@ -534,6 +556,17 @@ def _import_187_is_list(wb, data, settings, log_fn):
|
|||||||
if _set(field_lower, 'tech_measures', val_b):
|
if _set(field_lower, 'tech_measures', val_b):
|
||||||
log_fn(f" Тех. меры: {val_b[:60]}")
|
log_fn(f" Тех. меры: {val_b[:60]}")
|
||||||
continue
|
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:
|
if 'интернет' in field_lower:
|
||||||
is_obj['isInternet'] = is_checked
|
is_obj['isInternet'] = is_checked
|
||||||
continue
|
continue
|
||||||
|
|||||||
+40
-2
@@ -80,6 +80,12 @@ class InformationSystem:
|
|||||||
org_measures: str = "" # Организационные меры (\n — разделитель)
|
org_measures: str = "" # Организационные меры (\n — разделитель)
|
||||||
tech_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
|
@dataclass
|
||||||
class Company:
|
class Company:
|
||||||
@@ -130,14 +136,27 @@ class Company:
|
|||||||
profit_tax: str = "" # Сумма налога с прибыли за предыдущий год
|
profit_tax: str = "" # Сумма налога с прибыли за предыдущий год
|
||||||
|
|
||||||
# Показатель 8 (ущерб субъекту КИИ): доходы за 5 лет и суммарный ущерб
|
# Показатель 8 (ущерб субъекту КИИ): доходы за 5 лет и суммарный ущерб
|
||||||
income_year_1: str = "" # Доход за 5 лет назад (руб.)
|
# В форме — одна строка «Доход за 5 лет (суммарно)»; по годам разбивает сам оператор.
|
||||||
|
income_5y: str = "" # Доход за 5 лет суммарно (руб.)
|
||||||
|
income_year_1: str = "" # Доход за 5 лет назад (руб.) — обратная совместимость
|
||||||
income_year_2: str = "" # Доход за 4 года назад (руб.)
|
income_year_2: str = "" # Доход за 4 года назад (руб.)
|
||||||
income_year_3: str = "" # Доход за 3 года назад (руб.)
|
income_year_3: str = "" # Доход за 3 года назад (руб.)
|
||||||
income_year_4: str = "" # Доход за 2 года назад (руб.)
|
income_year_4: str = "" # Доход за 2 года назад (руб.)
|
||||||
income_year_5: str = "" # Доход за прошлый год (руб.)
|
income_year_5: str = "" # Доход за прошлый год (руб.)
|
||||||
income_loss: str = "" # Суммарный экономический ущерб от нарушения
|
income_loss: str = "" # Снижение уровня дохода (суммарный ущерб) от нарушения
|
||||||
# критических процессов (руб.) — ΔДоходов для п. 8
|
# критических процессов (руб.) — ΔДоходов для п. 8
|
||||||
|
|
||||||
|
# Показатель 9: компоненты ΔБ (унификация автозаполнения и авторасчёта)
|
||||||
|
ndpi: str = "" # Снижение выплат от налога на добычу ПИ ΔНДПИ (руб.)
|
||||||
|
div: str = "" # Снижение выплат от дивидендов ΔДИВ (руб.)
|
||||||
|
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)
|
commission: List[CommissionMember] = field(default_factory=list)
|
||||||
information_systems: List[InformationSystem] = field(default_factory=list)
|
information_systems: List[InformationSystem] = field(default_factory=list)
|
||||||
@@ -146,6 +165,11 @@ class Company:
|
|||||||
# Наименование структурного подразделения по безопасности
|
# Наименование структурного подразделения по безопасности
|
||||||
security_department: str = ""
|
security_department: str = ""
|
||||||
|
|
||||||
|
# Ответы раздела «Физическая безопасность» опросника (да/нет):
|
||||||
|
# охрана, пожарная сигнализация, тревожная кнопка, решётки, металлические двери,
|
||||||
|
# видеонаблюдение, видеодомофон, турникет, журнал посетителей.
|
||||||
|
physical_security: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
def update_declensions(self, inflect_func):
|
def update_declensions(self, inflect_func):
|
||||||
"""Обновить склонения полного названия"""
|
"""Обновить склонения полного названия"""
|
||||||
if self.full_name:
|
if self.full_name:
|
||||||
@@ -208,6 +232,20 @@ class Company:
|
|||||||
d["okved_extra"] = []
|
d["okved_extra"] = []
|
||||||
valid = {f.name for f in cls.__dataclass_fields__.values()}
|
valid = {f.name for f in cls.__dataclass_fields__.values()}
|
||||||
obj = cls(**{k: v for k, v in d.items() if k in valid})
|
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'):
|
for fio_field in ('chief_fio', 'responsible_fio', 'admin_fio'):
|
||||||
val = getattr(obj, fio_field, '')
|
val = getattr(obj, fio_field, '')
|
||||||
|
|||||||
+102
-16
@@ -11,6 +11,24 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
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):
|
def load_threats(dict_dir=None):
|
||||||
@@ -42,8 +60,26 @@ BASE_KEYWORDS = [
|
|||||||
'вредоносн', 'несанкционированн', 'утечк', 'перехват', 'модификац',
|
'вредоносн', 'несанкционированн', 'утечк', 'перехват', 'модификац',
|
||||||
'уничтожен', 'отказ в обслуживании', 'нсд', 'биос', 'пароль',
|
'уничтожен', 'отказ в обслуживании', 'нсд', 'биос', 'пароль',
|
||||||
'криптографическ', 'шифрован', 'аутентификац',
|
'криптографическ', 'шифрован', 'аутентификац',
|
||||||
|
# базовые угрозы локальных сетей / клиент-серверных архитектур
|
||||||
|
'доступ', 'данн', 'систем', 'конфигурац', 'целостност', 'информац',
|
||||||
|
'программн', 'пользовател', 'авторизац', 'сеанс', 'журнал', 'реестр',
|
||||||
|
'файл', 'память', 'буфер', 'ресурс', 'сервер', 'клиент', 'учётн',
|
||||||
|
'учетн', 'идентификац', 'привилег', 'средств', 'сетев', 'локальн',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Технологии, которые по умолчанию НЕ используются на объекте.
|
||||||
|
# Если в характеристиках объекта есть явные упоминания — группа возвращается.
|
||||||
|
TECH_GROUPS = {
|
||||||
|
'виртуализац': ['виртуал', 'гипервизор', 'контейнер'],
|
||||||
|
'облако': ['облачн', 'облак'],
|
||||||
|
'мобильн': ['мобильн', 'смартфон', 'планшет'],
|
||||||
|
'грид': ['грид', 'суперкомпьютер'],
|
||||||
|
'большие данные': ['больших данных', 'хранилищ'],
|
||||||
|
'беспроводн': ['беспроводн', 'wi-fi', 'wifi'],
|
||||||
|
'smart-карты': ['smart-карт', 'смарт-карт'],
|
||||||
|
'машинное обучение': ['машинного обучения', 'искусственного интеллекта', 'нейросет'],
|
||||||
|
}
|
||||||
|
|
||||||
# Объект имеет подключение к сети Интернет / сеть электросвязи
|
# Объект имеет подключение к сети Интернет / сеть электросвязи
|
||||||
INTERNET_KEYWORDS = [
|
INTERNET_KEYWORDS = [
|
||||||
'удалённ', 'удаленн', 'сетев', 'фишинг', 'ddos', 'отказ в обслуживании',
|
'удалённ', 'удаленн', 'сетев', 'фишинг', 'ddos', 'отказ в обслуживании',
|
||||||
@@ -153,6 +189,21 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
|||||||
is_asu = _is_asu(isys)
|
is_asu = _is_asu(isys)
|
||||||
is_network = _is_network(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 = []
|
exclude = []
|
||||||
|
|
||||||
@@ -167,6 +218,19 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
|||||||
if is_asu:
|
if is_asu:
|
||||||
# АСУ: нет BIOS/виртуализации/браузера в привычном понимании
|
# АСУ: нет BIOS/виртуализации/браузера в привычном понимании
|
||||||
exclude += ['биос', 'грид', 'гипервизор', 'виртуальн', 'браузер']
|
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:
|
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)
|
sc = sum(1 for kw in pos if kw in tl)
|
||||||
scored.append((sc, t))
|
scored.append((sc, t))
|
||||||
|
|
||||||
# Сортируем по релевантности (сначала — с наибольшим совпадением)
|
# Сортируем по возрастанию кода УБИ (УБИ.001 → УБИ.211…)
|
||||||
scored.sort(key=lambda x: -x[0])
|
scored = [t for sc, t in scored if sc > 0]
|
||||||
result = [t for sc, t in scored if sc > 0]
|
result = sort_threats_by_ubi(scored)
|
||||||
|
|
||||||
if limit and len(result) > limit:
|
if limit and len(result) > limit:
|
||||||
result = result[:limit]
|
result = result[:limit]
|
||||||
@@ -234,19 +298,18 @@ BASE_ORG_MEASURES = [
|
|||||||
'Утверждение перечня лиц, имеющих доступ к объекту',
|
'Утверждение перечня лиц, имеющих доступ к объекту',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Базовый набор тех. мер
|
# Базовый набор тех. мер — только универсальные, не требующие конкретных СЗИ.
|
||||||
|
# Меры, зависящие от средств защиты (антивирус, СЗИ от НСД, регистрация событий
|
||||||
|
# и т.п.), добавляются отдельно по факту наличия СЗИ (DEFENSE_TOOL_MEASURES).
|
||||||
BASE_TECH_MEASURES = [
|
BASE_TECH_MEASURES = [
|
||||||
'Идентификация и аутентификация пользователей',
|
'Идентификация и аутентификация пользователей',
|
||||||
'Управление доступом',
|
'Управление доступом',
|
||||||
'Ограничение программной среды',
|
'Ограничение программной среды',
|
||||||
'Антивирусная защита',
|
|
||||||
'Защита информации от несанкционированного доступа (СЗИ от НСД)',
|
|
||||||
'Регистрация и мониторинг событий безопасности',
|
|
||||||
'Резервное копирование и восстановление',
|
'Резервное копирование и восстановление',
|
||||||
'Защита технических средств и линий связи',
|
'Защита технических средств и линий связи',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Средства защиты → доп. тех. меры
|
# Средства защиты → доп. тех. меры (по факту наличия СЗИ у объекта)
|
||||||
DEFENSE_TOOL_MEASURES = [
|
DEFENSE_TOOL_MEASURES = [
|
||||||
('межсетев', 'Межсетевое экранирование'),
|
('межсетев', 'Межсетевое экранирование'),
|
||||||
('firewall', 'Межсетевое экранирование'),
|
('firewall', 'Межсетевое экранирование'),
|
||||||
@@ -260,6 +323,23 @@ DEFENSE_TOOL_MEASURES = [
|
|||||||
('виртуализ', 'Защита среды виртуализации'),
|
('виртуализ', 'Защита среды виртуализации'),
|
||||||
('vmware', 'Защита среды виртуализации'),
|
('vmware', 'Защита среды виртуализации'),
|
||||||
('hyper-v', 'Защита среды виртуализации'),
|
('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():
|
if not (getattr(isys, 'org_measures', '') or '').strip():
|
||||||
res['org_measures'] = '\n'.join(BASE_ORG_MEASURES)
|
res['org_measures'] = '\n'.join(BASE_ORG_MEASURES)
|
||||||
|
|
||||||
# 7. Тех. меры — базовые + по СЗИ объекта
|
# 7. Тех. меры — базовые + по СЗИ объекта.
|
||||||
|
# Только если объекту присвоена категория значимости (1/2/3):
|
||||||
|
# для незначимого объекта технические меры не требуются.
|
||||||
if not (getattr(isys, 'tech_measures', '') or '').strip():
|
if not (getattr(isys, 'tech_measures', '') or '').strip():
|
||||||
tech = list(BASE_TECH_MEASURES)
|
cat = (getattr(isys, 'category', '') or '').strip()
|
||||||
defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
|
has_category = cat in ('1', '2', '3')
|
||||||
dlow = defense.lower()
|
if has_category:
|
||||||
for kw, measure in DEFENSE_TOOL_MEASURES:
|
tech = list(BASE_TECH_MEASURES)
|
||||||
if kw in dlow and measure not in tech:
|
defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
|
||||||
tech.append(measure)
|
dlow = defense.lower()
|
||||||
res['tech_measures'] = '\n'.join(tech)
|
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
|
return res
|
||||||
|
|
||||||
|
|||||||
+899
-169
File diff suppressed because it is too large
Load Diff
+607
-2
@@ -61,7 +61,8 @@ VARIABLE_DEFS = [
|
|||||||
('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'),
|
('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'),
|
||||||
('{{company_profit}}', 'A', 'Сумма прибыли за предыдущий год (руб.)', 'company:profit'),
|
('{{company_profit}}', 'A', 'Сумма прибыли за предыдущий год (руб.)', 'company:profit'),
|
||||||
('{{company_profit_tax}}', 'A', 'Сумма налога с прибыли (руб.)', 'company:profit_tax'),
|
('{{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_2}}', 'A', 'Доход за 4 года назад (руб.) — показатель 8', 'company:income_year_2'),
|
||||||
('{{company_income_year_3}}', 'A', 'Доход за 3 года назад (руб.) — показатель 8', 'company:income_year_3'),
|
('{{company_income_year_3}}', 'A', 'Доход за 3 года назад (руб.) — показатель 8', 'company:income_year_3'),
|
||||||
('{{company_income_year_4}}', 'A', 'Доход за 2 года назад (руб.) — показатель 8', 'company:income_year_4'),
|
('{{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 '—',
|
'incidents': getattr(isys, 'incidents', '') or '—',
|
||||||
'criteria_values': getattr(isys, 'criteria_values', '') or '—',
|
'criteria_values': getattr(isys, 'criteria_values', '') or '—',
|
||||||
'criteria_justification': getattr(isys, 'criteria_justification', '') 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 '—',
|
'tech_measures': getattr(isys, 'tech_measures', '') or '—',
|
||||||
})
|
})
|
||||||
loops['information_systems'] = items
|
loops['information_systems'] = items
|
||||||
@@ -373,9 +377,610 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
|||||||
})
|
})
|
||||||
loops['employees_access'] = items
|
loops['employees_access'] = items
|
||||||
|
|
||||||
|
# --- Шаблон «Сведения о результатах категорирования» (ФСТЭК) ---
|
||||||
|
# Цикл объектов КИИ — тот же список, что information_systems.
|
||||||
|
if is_list:
|
||||||
|
loops['kii_objects'] = loops.get('information_systems', [])
|
||||||
|
|
||||||
|
# Цикл показателей критериев значимости (ПП-127): автоматически
|
||||||
|
# формируем строки таблицы с обоснованием «применим/не применим» по сфере
|
||||||
|
# и расчётом для показателей 8 и 9 (как в примере заказчика).
|
||||||
|
loops['categorization_indicators'] = _build_categorization_indicators(company, is_list)
|
||||||
|
|
||||||
return loops
|
return loops
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ПОКАЗАТЕЛИ КРИТЕРИЕВ ЗНАЧИМОСТИ (ПП-127)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _num(s):
|
||||||
|
"""«121 118 270 руб», «1 000», «2 212.5» → число (float) или None."""
|
||||||
|
s = str(s or '').strip()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
s = s.replace('\u00a0', ' ').replace(' ', '')\
|
||||||
|
.replace('руб', '').replace('₽', '').replace('р.', '').replace(',', '.')
|
||||||
|
try:
|
||||||
|
return float(s)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json(name):
|
||||||
|
import json as _json
|
||||||
|
import os as _os
|
||||||
|
p = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'dictionaries', name)
|
||||||
|
try:
|
||||||
|
with open(p, encoding='utf-8') as f:
|
||||||
|
return _json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_criterion_8_9(company, isys, num):
|
||||||
|
"""Расчёт показателя 8 (ущерб субъекту КИИ) или 9 (ущерб бюджету РФ).
|
||||||
|
|
||||||
|
Возвращает (текст_обоснования, категория или None).
|
||||||
|
Логика — как в авторасчёте программы (окно ПП-127), текст — по образцу
|
||||||
|
заполнения заказчика для ОПК.
|
||||||
|
"""
|
||||||
|
# ΔДоходов: ущерб из вкладки «Организация» или балансовая стоимость объекта
|
||||||
|
d = _num(getattr(company, 'income_loss', '') or '')
|
||||||
|
if d is None:
|
||||||
|
d = _num(getattr(isys, 'balance_value', '') or '')
|
||||||
|
# Доходы за 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)
|
||||||
|
val_pct = d / 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"Реализация компьютерных атак на объект не приведет к снижению доходов субъекта КИИ. "
|
||||||
|
f"Объем среднегодового дохода субъекта КИИ (с учетом НДС), усредненный за прошедший "
|
||||||
|
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), по данному показателю объекту "
|
||||||
|
f"присваивается {cat}-я категория значимости.")
|
||||||
|
else:
|
||||||
|
text += (f" Так как {val_pct:.6f} % менее 1% (пороговое значение для 3-й категории "
|
||||||
|
f"значимости согласно Постановлению № 127), по данному показателю объекту "
|
||||||
|
f"категория значимости не присваивается.")
|
||||||
|
return text, cat
|
||||||
|
|
||||||
|
# num == '9'
|
||||||
|
if d is None:
|
||||||
|
return ("Показатель применим. Для расчёта заполните данные организации: "
|
||||||
|
"«Снижение уровня дохода» или балансовую стоимость объекта КИИ.", None)
|
||||||
|
profit = _num(getattr(company, 'profit', '') or '')
|
||||||
|
tax = _num(getattr(company, 'profit_tax', '') or '')
|
||||||
|
if profit and tax is not None and profit > 0:
|
||||||
|
knp = tax / profit
|
||||||
|
dnp = d * knp
|
||||||
|
if d > profit:
|
||||||
|
dnp = tax
|
||||||
|
else:
|
||||||
|
knp = 0.0
|
||||||
|
dnp = 0.0
|
||||||
|
DEFAULT_BUDGET_AVG = 21228053357700.0 # Бусредн — прогноз годового дохода федбюджета, усреднённый за 3 года
|
||||||
|
b = DEFAULT_BUDGET_AVG
|
||||||
|
val_pct = dnp / b * 100
|
||||||
|
if val_pct > 0.001:
|
||||||
|
cat = 'I'
|
||||||
|
elif val_pct > 0.0006:
|
||||||
|
cat = 'II'
|
||||||
|
elif val_pct > 0.0003:
|
||||||
|
cat = 'III'
|
||||||
|
else:
|
||||||
|
cat = None
|
||||||
|
text = (f"Реализация компьютерных атак на объект не приведет к снижению доходов федерального бюджета. "
|
||||||
|
f"Объем прогнозируемого годового дохода федерального бюджета Российской Федерации, "
|
||||||
|
f"усредненный за планируемый трехлетний период, составляет {b:,.2f} руб. "
|
||||||
|
f"Коэффициент выплат (отчислений) от налога на прибыль субъекта КИИ = {knp:.4f}. "
|
||||||
|
f"В случае выхода из строя объекта КИИ потребуется замена, установка и настройка, "
|
||||||
|
f"стоимость которых составит около {d:,.2f} руб. "
|
||||||
|
f"Снижение выплат (отчислений) от налога на прибыль субъекта КИИ в бюджеты Российской Федерации "
|
||||||
|
f"Уабс = {d:,.2f} × {knp:.4f} = {dnp:,.2f} руб. (приближённо). "
|
||||||
|
f"Показатель ущерба бюджетам = {val_pct:.6f} %.")
|
||||||
|
if cat:
|
||||||
|
text += (f" Так как {val_pct:.6f} % более 0,0003% (пороговое значение для 3-й категории "
|
||||||
|
f"значимости согласно Постановлению № 127), по данному показателю объекту "
|
||||||
|
f"присваивается {cat}-я категория значимости.")
|
||||||
|
else:
|
||||||
|
text += (f" Так как {val_pct:.6f} % менее 0,0003% (пороговое значение для 3-й категории "
|
||||||
|
f"значимости согласно Постановлению № 127), по данному показателю объекту "
|
||||||
|
f"категория значимости не присваивается.")
|
||||||
|
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).
|
||||||
|
|
||||||
|
Для каждого показателя ПП-127 (5 групп, показатели 1–14 с подпунктами):
|
||||||
|
section, name, number, value, justification, category.
|
||||||
|
- Если показатель не применим для сферы объекта — value = «Показатель не применим.»,
|
||||||
|
justification — текст из словаря обоснований по сфере (или заготовка).
|
||||||
|
- Если применим (8/9) — автоматический расчёт из данных организации.
|
||||||
|
"""
|
||||||
|
import re as _re
|
||||||
|
criteria = _load_json('criteria_127.json')
|
||||||
|
applicability = _load_json('criteria_applicability.json')
|
||||||
|
justifications = _load_json('criteria_justifications.json')
|
||||||
|
if not isinstance(criteria, list):
|
||||||
|
criteria = []
|
||||||
|
|
||||||
|
sphere = getattr(is_list[0], 'sphere', '') if is_list else ''
|
||||||
|
sphere = (sphere or '').strip()
|
||||||
|
appl = applicability.get(sphere) if isinstance(applicability, dict) else None
|
||||||
|
just = (justifications.get(sphere) or {}) if isinstance(justifications, dict) else {}
|
||||||
|
|
||||||
|
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', []):
|
||||||
|
num = str(ind.get('num', ''))
|
||||||
|
name = ind.get('name', '')
|
||||||
|
m = _re.match(r'(\d+)', num)
|
||||||
|
grp_num = m.group(1) if m else num
|
||||||
|
applicable = appl is None or grp_num in appl
|
||||||
|
|
||||||
|
value = '—'
|
||||||
|
justification = '—'
|
||||||
|
category = '—'
|
||||||
|
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'):
|
||||||
|
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:
|
||||||
|
# Применимый показатель без формульного авторасчёта (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,
|
||||||
|
'name': name,
|
||||||
|
'number': num,
|
||||||
|
'value': value,
|
||||||
|
'justification': justification,
|
||||||
|
'category': category,
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# РЕЗОЛВЕР ЗНАЧЕНИЙ
|
# РЕЗОЛВЕР ЗНАЧЕНИЙ
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
+175
-13
@@ -161,15 +161,20 @@ def normalize_address(text):
|
|||||||
|
|
||||||
|
|
||||||
def resolve_okved(code, dadata_token=None):
|
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 Название»).
|
1) Ищем в словаре dictionaries/okved.json (строки вида «27.32.2 Название»).
|
||||||
2) Если не нашли — запрашиваем DaData suggest okved2 (если есть токен).
|
2) Если не нашли — запрашиваем DaData suggest okved2 (если есть токен).
|
||||||
3) Иначе возвращаем код как есть.
|
3) Иначе возвращаем нормализованную строку «код название» как есть.
|
||||||
"""
|
"""
|
||||||
code = (code or '').strip()
|
code = (code or '').strip()
|
||||||
if not code:
|
if not code:
|
||||||
return ''
|
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) Словарь
|
# 1) Словарь
|
||||||
try:
|
try:
|
||||||
import os, json
|
import os, json
|
||||||
@@ -178,12 +183,8 @@ def resolve_okved(code, dadata_token=None):
|
|||||||
items = json.load(f)
|
items = json.load(f)
|
||||||
for item in items:
|
for item in items:
|
||||||
item = str(item).strip()
|
item = str(item).strip()
|
||||||
# совпадение по началу: «27.32.2» или «27.32.2 - ...» / «27.32.2 ...»
|
# совпадение по коду: «27.32.2» или «27.32.2 Название»
|
||||||
if item.split()[0].rstrip('.') == code.rstrip('.'):
|
if item.split()[0].rstrip('.') == code_only.rstrip('.'):
|
||||||
# нормализуем: «код - название»
|
|
||||||
rest = item[len(item.split()[0]):].strip().lstrip('- ').strip()
|
|
||||||
if rest:
|
|
||||||
return f"{code} - {rest}"
|
|
||||||
return item
|
return item
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -191,7 +192,7 @@ def resolve_okved(code, dadata_token=None):
|
|||||||
if dadata_token:
|
if dadata_token:
|
||||||
try:
|
try:
|
||||||
js, err = _http_json('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/okved2',
|
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',
|
headers={'Content-Type': 'application/json',
|
||||||
'Authorization': f'Token {dadata_token}'})
|
'Authorization': f'Token {dadata_token}'})
|
||||||
if not err and js:
|
if not err and js:
|
||||||
@@ -200,7 +201,12 @@ def resolve_okved(code, dadata_token=None):
|
|||||||
return sugg[0]['value']
|
return sugg[0]['value']
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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):
|
def _fmt_date(value):
|
||||||
@@ -340,8 +346,15 @@ def check_company_dadata(inn, token):
|
|||||||
# ОКВЭД: основной (okved) и дополнительные (okveds — список словарей с 'code')
|
# ОКВЭД: основной (okved) и дополнительные (okveds — список словарей с 'code')
|
||||||
okved = d.get('okved', '') or ''
|
okved = d.get('okved', '') or ''
|
||||||
okved_extra = []
|
okved_extra = []
|
||||||
for ok in (d.get('okveds') or []):
|
raw_okveds = d.get('okveds') or []
|
||||||
code = (ok or {}).get('code') or ''
|
if isinstance(raw_okveds, str):
|
||||||
|
# DaData иногда отдаёт доп. ОКВЭД одной строкой через запятую
|
||||||
|
raw_okveds = [x.strip() for x in raw_okveds.split(',') if x.strip()]
|
||||||
|
for ok in raw_okveds:
|
||||||
|
if isinstance(ok, dict):
|
||||||
|
code = (ok.get('code') or '').strip()
|
||||||
|
else:
|
||||||
|
code = str(ok or '').strip()
|
||||||
if code:
|
if code:
|
||||||
okved_extra.append(code)
|
okved_extra.append(code)
|
||||||
|
|
||||||
@@ -518,6 +531,111 @@ def compare_company_values(old_data, new_data):
|
|||||||
return diffs
|
return diffs
|
||||||
|
|
||||||
|
|
||||||
|
def _ubi_code(text):
|
||||||
|
"""Извлекает код УБИ из строки: «УБИ.069 Название» → '069'."""
|
||||||
|
m = re.search(r'уби\.?\s*(\d+)', str(text), re.IGNORECASE)
|
||||||
|
return m.group(1) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_threat_text(text):
|
||||||
|
"""Нормализация названия угрозы для сравнения: без ведущего номера, лишних пробелов."""
|
||||||
|
s = re.sub(r'^\d+[.)]?\s*', '', str(text or '').strip())
|
||||||
|
s = re.sub(r'\s+', ' ', s).strip()
|
||||||
|
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):
|
||||||
|
"""Приводит строки угроз (от ИИ или оператора) к каноническому виду по словарю УБИ.
|
||||||
|
|
||||||
|
Для каждой строки:
|
||||||
|
- есть код УБИ → берём полную запись «УБИ.xxx Название» из словаря;
|
||||||
|
- кода нет → ищем по названию (точное совпадение или вхождение);
|
||||||
|
- не нашлось в словаре → отбрасываем (мусор от ИИ).
|
||||||
|
|
||||||
|
Возвращает уникальные канонические строки, отсортированные по коду УБИ.
|
||||||
|
"""
|
||||||
|
if threats_full is None:
|
||||||
|
from .threat_rules import load_threats
|
||||||
|
threats_full = load_threats()
|
||||||
|
by_code = {}
|
||||||
|
by_norm = {}
|
||||||
|
for t in threats_full:
|
||||||
|
t = str(t).strip()
|
||||||
|
code = _ubi_code(t)
|
||||||
|
if code:
|
||||||
|
by_code.setdefault(code, t)
|
||||||
|
by_norm.setdefault(_norm_threat_text(t), t)
|
||||||
|
out = []
|
||||||
|
seen = set()
|
||||||
|
for line in lines or []:
|
||||||
|
line = str(line or '').strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
code = _ubi_code(line)
|
||||||
|
canon = None
|
||||||
|
if code and code in by_code:
|
||||||
|
canon = by_code[code]
|
||||||
|
else:
|
||||||
|
n = _norm_threat_text(line)
|
||||||
|
if n in by_norm:
|
||||||
|
canon = by_norm[n]
|
||||||
|
else:
|
||||||
|
for k, v in by_norm.items():
|
||||||
|
if n and (n in k or k in n):
|
||||||
|
canon = v
|
||||||
|
break
|
||||||
|
if canon and canon not in seen:
|
||||||
|
seen.add(canon)
|
||||||
|
out.append(canon)
|
||||||
|
out.sort(key=lambda t: (int(_ubi_code(t)) if _ubi_code(t) else 10 ** 9, t))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=None,
|
def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=None,
|
||||||
threats=None):
|
threats=None):
|
||||||
"""ИИ предлагает актуальные угрозы БИ для объекта КИИ по его характеристикам.
|
"""ИИ предлагает актуальные угрозы БИ для объекта КИИ по его характеристикам.
|
||||||
@@ -564,9 +682,52 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N
|
|||||||
if isinstance(threats_out, str):
|
if isinstance(threats_out, str):
|
||||||
threats_out = [threats_out]
|
threats_out = [threats_out]
|
||||||
threats_out = [str(t).strip() for t in threats_out if str(t).strip()]
|
threats_out = [str(t).strip() for t in threats_out if str(t).strip()]
|
||||||
|
# Жёсткий фильтр по словарю: мусор отбрасываем, «УБИ.069» дополняем
|
||||||
|
# полным названием из актуального перечня (если он передан).
|
||||||
|
threats_out = filter_threats_by_dict(threats_out, threats)
|
||||||
return threats_out, None
|
return threats_out, None
|
||||||
|
|
||||||
|
|
||||||
|
def check_threats_ai(security_data, current_threats, api_key, model='deepseek-chat', base_url=None):
|
||||||
|
"""ИИ проверяет уже подобранный список угроз БИ для объекта КИИ.
|
||||||
|
|
||||||
|
В отличие от suggest_threats_ai (подбор заново), эта функция проверяет
|
||||||
|
текущий список: какие угрозы лишние, каких не хватает, и даёт комментарий.
|
||||||
|
|
||||||
|
Возвращает (dict, ошибка):
|
||||||
|
{"лишние": [...], "недостающие": [...], "комментарий": "..."}
|
||||||
|
"""
|
||||||
|
if not api_key:
|
||||||
|
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
|
||||||
|
if not current_threats:
|
||||||
|
return None, "Список угроз пуст — сначала подберите угрозы (программой или ИИ)."
|
||||||
|
|
||||||
|
system = (
|
||||||
|
"Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры "
|
||||||
|
"(187-ФЗ, приказ ФСТЭК № 236). Проверь, корректно ли подобран перечень угроз "
|
||||||
|
"безопасности информации для объекта КИИ."
|
||||||
|
)
|
||||||
|
user = (
|
||||||
|
"Характеристики объекта КИИ:\n"
|
||||||
|
f"{json.dumps(security_data, ensure_ascii=False)}\n\n"
|
||||||
|
"Текущий перечень угроз (подобран программой или оператором):\n"
|
||||||
|
+ "\n".join(f"{i+1}. {t}" for i, t in enumerate(current_threats))
|
||||||
|
+ "\n\nВерни ТОЛЬКО JSON: "
|
||||||
|
'{"лишние": ["угрозы, которые НЕ актуальны для этого объекта"], '
|
||||||
|
'"недостающие": ["актуальные угрозы, которых в списке не хватает"], '
|
||||||
|
'"комментарий": "краткий вывод по перечню"}'
|
||||||
|
)
|
||||||
|
js, err = _ai_json(api_key, model, base_url, system, user)
|
||||||
|
if err:
|
||||||
|
return None, err
|
||||||
|
# «Недостающие»/«лишние» — тоже через словарь: мусор отбрасываем,
|
||||||
|
# неполные строки («УБИ.069») дополняем полными названиями.
|
||||||
|
missing = filter_threats_by_dict(js.get('недостающие') or [])
|
||||||
|
extra = filter_threats_by_dict(js.get('лишние') or [])
|
||||||
|
comment = js.get('комментарий') or ''
|
||||||
|
return {'лишние': extra, 'недостающие': missing, 'комментарий': comment}, None
|
||||||
|
|
||||||
|
|
||||||
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
|
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
|
||||||
typical_objects=None, spheres=None, object_types=None,
|
typical_objects=None, spheres=None, object_types=None,
|
||||||
architectures=None):
|
architectures=None):
|
||||||
@@ -694,7 +855,8 @@ def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None
|
|||||||
if not api_key:
|
if not api_key:
|
||||||
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
|
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
|
||||||
system = (
|
system = (
|
||||||
"Ты — юрист по защите персональных данных (187-ФЗ). Проверь реквизиты организации. "
|
"Ты — специалист по проверке реквизитов организации (ИНН/КПП/ОГРН). "
|
||||||
|
"Проверь реквизиты организации. "
|
||||||
"Верни ТОЛЬКО JSON: "
|
"Верни ТОЛЬКО JSON: "
|
||||||
'{"оценка": "ok|warning|error", '
|
'{"оценка": "ok|warning|error", '
|
||||||
'"замечания": ["..."], '
|
'"замечания": ["..."], '
|
||||||
|
|||||||
+8
-8
@@ -30,8 +30,8 @@ def create_test_template():
|
|||||||
doc.add_paragraph('<!-- loop:information_systems -->')
|
doc.add_paragraph('<!-- loop:information_systems -->')
|
||||||
doc.add_paragraph('=== IS: {{item.name}} ===')
|
doc.add_paragraph('=== IS: {{item.name}} ===')
|
||||||
doc.add_paragraph('Category: {{item.category}}')
|
doc.add_paragraph('Category: {{item.category}}')
|
||||||
doc.add_paragraph('Threat type: {{item.threat_type}}')
|
doc.add_paragraph('Sphere: {{item.sphere}}')
|
||||||
doc.add_paragraph('Defence level: {{item.defence_level}}')
|
doc.add_paragraph('Object type: {{item.object_type}}')
|
||||||
doc.add_paragraph('')
|
doc.add_paragraph('')
|
||||||
|
|
||||||
# Inner loop: commission
|
# Inner loop: commission
|
||||||
@@ -72,12 +72,12 @@ def test_zip_loop_expansion():
|
|||||||
CommissionMember(role='Член комиссии', position='Бухгалтер', fio='Сидорова А.А.'),
|
CommissionMember(role='Член комиссии', position='Бухгалтер', fio='Сидорова А.А.'),
|
||||||
],
|
],
|
||||||
information_systems=[
|
information_systems=[
|
||||||
InformationSystem(name='ИС-1 "Бухгалтерия"', category='специальные',
|
InformationSystem(name='ОКИИ-1 "Платёжный шлюз"', category='1',
|
||||||
pd_count='менее 100 000', threat_type='1', defence_level='2'),
|
sphere='Банковская сфера и финансовый рынок', object_type='АСУ'),
|
||||||
InformationSystem(name='ИС-2 "Кадры"', category='иные',
|
InformationSystem(name='ОКИИ-2 "Кадры"', category='2',
|
||||||
pd_count='более 100 000', threat_type='3', defence_level='4'),
|
sphere='Наука', object_type='ИС'),
|
||||||
InformationSystem(name='ИС-3 "Склад"', category='общедоступные',
|
InformationSystem(name='ОКИИ-3 "Склад"', category='3',
|
||||||
pd_count='менее 100 000', threat_type='2', defence_level='3'),
|
sphere='Здравоохранение', object_type='ИС'),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user