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/
|
||||
Сгенерированные_документы/
|
||||
*.result.docx
|
||||
|
||||
# Секреты и локальные настройки — НЕ коммитить
|
||||
api_settings.json
|
||||
.env
|
||||
.env.local
|
||||
|
||||
+10
-4
@@ -13,11 +13,17 @@ import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# ВНИМАНИЕ: реальные ключи НЕ хранятся в коде (утечка в git-историю — см. аудит).
|
||||
# Источники ключей (по приоритету):
|
||||
# 1) переменные окружения DADATA_TOKEN / DEEPSEEK_API_KEY / DEEPSEEK_MODEL / DEEPSEEK_BASE_URL
|
||||
# 2) файл api_settings.json рядом с программой
|
||||
# 3) DEFAULT_SETTINGS ниже (пустые значения)
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
'dadata_token': '431aa11706bf83f571cef0319259289aa750bcb1',
|
||||
'ai_api_key': 'sk-3c7e6673f245412697d0e14b6096435f',
|
||||
'ai_model': 'deepseek-chat',
|
||||
'ai_base_url': 'https://api.deepseek.com',
|
||||
'dadata_token': os.environ.get('DADATA_TOKEN', ''),
|
||||
'ai_api_key': os.environ.get('DEEPSEEK_API_KEY', ''),
|
||||
'ai_model': os.environ.get('DEEPSEEK_MODEL', 'deepseek-chat'),
|
||||
'ai_base_url': os.environ.get('DEEPSEEK_BASE_URL', 'https://api.deepseek.com'),
|
||||
}
|
||||
|
||||
# Старые ключи (для совместимости со старым файлом api_settings.json)
|
||||
|
||||
Binary file not shown.
@@ -16,19 +16,27 @@
|
||||
],
|
||||
"Наука": [
|
||||
"1",
|
||||
"11",
|
||||
"13",
|
||||
"7",
|
||||
"8",
|
||||
"9"
|
||||
"9",
|
||||
"11",
|
||||
"13"
|
||||
],
|
||||
"Транспорт": [
|
||||
"10",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"8",
|
||||
"9",
|
||||
"11",
|
||||
"3"
|
||||
"13",
|
||||
"14"
|
||||
],
|
||||
"Связь": [
|
||||
"4"
|
||||
"4",
|
||||
"6",
|
||||
"8",
|
||||
"9"
|
||||
],
|
||||
"Энергетика": [
|
||||
"1",
|
||||
@@ -61,6 +69,8 @@
|
||||
"14"
|
||||
],
|
||||
"Банковская сфера и финансовый рынок": [
|
||||
"8",
|
||||
"9",
|
||||
"10"
|
||||
],
|
||||
"Топливно-энергетический комплекс": [
|
||||
@@ -82,20 +92,26 @@
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"13",
|
||||
"14"
|
||||
"13"
|
||||
],
|
||||
"Оборонная промышленность": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"11",
|
||||
"12",
|
||||
"13"
|
||||
],
|
||||
"Ракетно-космическая промышленность": [
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"6": "Организация сферы здравоохранения не является органом государственной власти, в связи с этим критерий неприменим.",
|
||||
"7": "Организация сферы здравоохранения не является субъектом политической сферы деятельности, в связи с этим критерий неприменим.",
|
||||
"8": "Организация сферы здравоохранения не является государственной корпорацией, государственным унитарным предприятием, государственной компанией, стратегическим акционерным обществом, стратегическим предприятием, в связи с этим критерий неприменим.",
|
||||
"9": "Объект КИИ задействован только в предоставлении медицинских услуг, освобожденных от налогообложения, в связи с этим критерий неприменим.",
|
||||
"9": "Объект КИИ задействован в предоставлении медицинских услуг. Показатель 9 применим ко всем субъектам КИИ; при расчёте учитывается, что медицинские услуги освобождены от налогообложения (значение показателя может быть минимальным).",
|
||||
"10": "Организация сферы здравоохранения не является субъектом банковской сферы деятельности.",
|
||||
"10.1": "Организация сферы здравоохранения не является центральным контрагентом.",
|
||||
"10.2": "Организация сферы здравоохранения не является центральным депозитарием.",
|
||||
|
||||
+2595
-92
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, если текущий набор СЗИ покрывает требования категории. Не придумывай конкретные торговые марки, если их нет в списке — указывай класс средства."
|
||||
}
|
||||
|
||||
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">')
|
||||
|
||||
|
||||
def _expand_nested_loops(template: str, item: Dict) -> str:
|
||||
def _expand_nested_loops(template: str, item: Dict, loops: Dict = None) -> str:
|
||||
"""Разворачивает вложенные циклы вида <!-- loop:users_loop --> ... <!-- loop_end -->
|
||||
данными из item (список словарей). Используется для циклов внутри цикла ИС,
|
||||
например пользователи ИС. Переменные внутри: {{user_fio}}, {{user_position}}."""
|
||||
данными из item (список словарей) или из общего словаря циклов loops.
|
||||
Используется для циклов внутри цикла ИС, например пользователи ИС,
|
||||
а также для вложенных таблиц (показатели значимости внутри объекта КИИ).
|
||||
Переменные внутри: {{user_fio}}, {{user_position}}, {{item.*}}."""
|
||||
if loops is None:
|
||||
loops = {}
|
||||
result = template
|
||||
for _ in range(20):
|
||||
m_start = LOOP_START.search(result)
|
||||
@@ -321,6 +325,8 @@ def _expand_nested_loops(template: str, item: Dict) -> str:
|
||||
break
|
||||
key = m_start.group(1)
|
||||
data = item.get(key)
|
||||
if not isinstance(data, list):
|
||||
data = loops.get(key)
|
||||
if not isinstance(data, list):
|
||||
# Нет данных для вложенного цикла — удаляем блок целиком
|
||||
depth = 1
|
||||
@@ -478,7 +484,7 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
|
||||
clone = template_clean
|
||||
# Вложенные циклы (users_loop и т.п.) — разворачиваем ДО подстановки
|
||||
# обычных полей, чтобы {{user_fio}} внутри них не затёрлись алиасами
|
||||
clone = _expand_nested_loops(clone, item)
|
||||
clone = _expand_nested_loops(clone, item, loops)
|
||||
for field, value in item.items():
|
||||
if isinstance(value, list):
|
||||
continue # списки — данные вложенных циклов, не подставляем
|
||||
@@ -692,9 +698,6 @@ def _highlight_unmatched(doc):
|
||||
pos = m.end()
|
||||
if pos < len(full):
|
||||
_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:
|
||||
_process_para(para)
|
||||
|
||||
@@ -83,7 +83,7 @@ def _generate_variables_reference(path):
|
||||
_p(doc, 'Список переменных для шаблонов Word. Переменная подставляется в текст '
|
||||
'как есть: {{имя_переменной}}. Циклы: <!-- loop:имя --> ... <!-- loop_end -->.')
|
||||
|
||||
groups = {'A': 'Общие переменные', 'B': '187-ФЗ (персональные данные)'}
|
||||
groups = {'A': 'Общие переменные', 'B': '187-ФЗ (критическая информационная инфраструктура)'}
|
||||
by_group = {'A': [], 'B': []}
|
||||
for var, group, desc, _ in VARIABLE_DEFS:
|
||||
by_group.setdefault(group, []).append((var, desc))
|
||||
|
||||
+434
-43
@@ -7,48 +7,439 @@
|
||||
],
|
||||
"value_column": 1,
|
||||
"fields": [
|
||||
{"name": "Полное наименование организации", "var": "fullName", "cell": "", "keywords": "полное наименование организации"},
|
||||
{"name": "Сокращённое наименование организации", "var": "shortName", "cell": "", "keywords": "сокращенное наименование организации"},
|
||||
{"name": "Юридический адрес", "var": "addressLegal", "cell": "", "keywords": "адрес организации \\(юридический\\)"},
|
||||
{"name": "Фактический адрес", "var": "addressActual", "cell": "", "keywords": "адрес организации \\(фактический\\)"},
|
||||
{"name": "Электронная почта", "var": "email", "cell": "", "keywords": "электронная почта"},
|
||||
{"name": "Телефон", "var": "phone", "cell": "", "keywords": "телефон"},
|
||||
{"name": "ИНН", "var": "inn", "cell": "", "keywords": "инн"},
|
||||
{"name": "ОГРН", "var": "ogrn", "cell": "", "keywords": "огрн"},
|
||||
{"name": "Дата получения ОГРН", "var": "ogrnDate", "cell": "", "keywords": "дата получения огрн|дата.*огрн"},
|
||||
{"name": "КПП", "var": "kpp", "cell": "", "keywords": "кпп"},
|
||||
{"name": "Основной ОКВЭД", "var": "okved", "cell": "", "keywords": "основной оквэд"},
|
||||
{"name": "Дополнительные ОКВЭД", "var": "okvedExtra", "cell": "", "keywords": "дополнительные оквэд"},
|
||||
{"name": "Должность руководителя", "var": "chiefPosition", "cell": "", "keywords": "должность руководителя"},
|
||||
{"name": "ФИО руководителя", "var": "chiefFio", "cell": "", "keywords": "фио руководителя"},
|
||||
{"name": "Должность ответственного лица (безопасность ЗОКИИ)", "var": "responsiblePosition", "cell": "", "keywords": "должность ответственного лица"},
|
||||
{"name": "ФИО ответственного лица (безопасность ЗОКИИ)", "var": "responsibleFio", "cell": "", "keywords": "фио ответственного лица"},
|
||||
{"name": "Подразделение по безопасности ОКИИ", "var": "ispdnDepartment", "cell": "", "keywords": "структур.*подраздел"},
|
||||
{"name": "Рабочий телефон ответственного", "var": "ispdnPhone", "cell": "", "keywords": "рабочий телефон ответственного|мобильный телефон ответственного"},
|
||||
{"name": "Электронная почта ответственного", "var": "ispdnEmail", "cell": "", "keywords": "электронная почта ответственного"},
|
||||
{"name": "Должность специалиста по ЗИ/администратора", "var": "administratorPosition", "cell": "", "keywords": "должность специалиста по защите информации|должность.*администрат"},
|
||||
{"name": "ФИО специалиста по ЗИ/администратора", "var": "administratorFio", "cell": "", "keywords": "фио специалиста по защите информации|фио.*администрат"},
|
||||
{"name": "Право владения зданием", "var": "buildingRight", "cell": "", "keywords": "право владения зданием"},
|
||||
{"name": "Сумма прибыли за год", "var": "profit", "cell": "", "keywords": "сумма прибыли"},
|
||||
{"name": "Сумма налога с прибыли", "var": "profitTax", "cell": "", "keywords": "сумма налога с прибыли"},
|
||||
{"name": "Доменное имя и внешний IP", "var": "domainName", "cell": "", "keywords": "доменное имя и внешний сетевой адрес"},
|
||||
{"name": "Сайт организации", "var": "site_name", "cell": "", "keywords": "сайт организации"},
|
||||
{"name": "Должность ответственного за сайт", "var": "site_responsible_position", "cell": "", "keywords": "должность ответственного за сайт"},
|
||||
{"name": "ФИО ответственного за сайт", "var": "site_responsible_fio", "cell": "", "keywords": "фио ответственного за сайт"},
|
||||
{"name": "Разработчик сайта", "var": "site_service_provider", "cell": "", "keywords": "организации, которая разрабатывала и размещала корпоративный сайт"},
|
||||
{"name": "ИНН разработчика сайта", "var": "site_service_provider_inn", "cell": "", "keywords": "инн организации, которая разрабатывала"},
|
||||
{"name": "Размещение сайта", "var": "site_hosting", "cell": "", "keywords": "размещение сайта"},
|
||||
{"name": "Адрес расположения серверов", "var": "site_hosting_address", "cell": "", "keywords": "адрес расположения серверов"},
|
||||
{"name": "Облачная платформа", "var": "cloud_platform", "cell": "", "keywords": "облачную платформу"},
|
||||
{"name": "Аренда выделенного сервера", "var": "cloud_dedicated_server", "cell": "", "keywords": "выделенный сервер"},
|
||||
{"name": "Аренда виртуального ЦОД", "var": "cloud_vdc", "cell": "", "keywords": "виртуальный дата-центр"},
|
||||
{"name": "Серверы в дата-центре", "var": "cloud_datacenter", "cell": "", "keywords": "сервера в дата-центре"},
|
||||
{"name": "Средства контейнеризации", "var": "cloud_container", "cell": "", "keywords": "контейнеризации"},
|
||||
{"name": "Собственная серверная (ЦОД)", "var": "cloud_own_server", "cell": "", "keywords": "собственная серверная"},
|
||||
{"name": "Общее количество компьютеров", "var": "computers_total", "cell": "", "keywords": "общее количество компьютеров"},
|
||||
{"name": "Компьютеры, подключённые к объектам КИИ", "var": "computers_kii", "cell": "", "keywords": "компьютеров подключенных к объектам кии"},
|
||||
{"name": "Центр мониторинга", "var": "monitoring_center", "cell": "", "keywords": "к какому центру подключены"},
|
||||
{"name": "Детали подключения к центру мониторинга", "var": "monitoring_details", "cell": "", "keywords": "детали подключения"},
|
||||
{"name": "Дополнительная информация", "var": "additionalInfo", "cell": "", "keywords": "дополнительная информация"}
|
||||
{
|
||||
"name": "Полное наименование организации",
|
||||
"var": "fullName",
|
||||
"cell": "",
|
||||
"keywords": "полное наименование организации"
|
||||
},
|
||||
{
|
||||
"name": "Сокращённое наименование организации",
|
||||
"var": "shortName",
|
||||
"cell": "",
|
||||
"keywords": "сокращенное наименование организации"
|
||||
},
|
||||
{
|
||||
"name": "Юридический адрес",
|
||||
"var": "addressLegal",
|
||||
"cell": "",
|
||||
"keywords": "адрес организации \\(юридический\\)"
|
||||
},
|
||||
{
|
||||
"name": "Фактический адрес",
|
||||
"var": "addressActual",
|
||||
"cell": "",
|
||||
"keywords": "адрес организации \\(фактический\\)"
|
||||
},
|
||||
{
|
||||
"name": "Электронная почта",
|
||||
"var": "email",
|
||||
"cell": "",
|
||||
"keywords": "электронная почта"
|
||||
},
|
||||
{
|
||||
"name": "Телефон",
|
||||
"var": "phone",
|
||||
"cell": "",
|
||||
"keywords": "телефон"
|
||||
},
|
||||
{
|
||||
"name": "ИНН",
|
||||
"var": "inn",
|
||||
"cell": "",
|
||||
"keywords": "инн"
|
||||
},
|
||||
{
|
||||
"name": "ОГРН",
|
||||
"var": "ogrn",
|
||||
"cell": "",
|
||||
"keywords": "огрн"
|
||||
},
|
||||
{
|
||||
"name": "Дата получения ОГРН",
|
||||
"var": "ogrnDate",
|
||||
"cell": "",
|
||||
"keywords": "дата получения огрн|дата.*огрн"
|
||||
},
|
||||
{
|
||||
"name": "КПП",
|
||||
"var": "kpp",
|
||||
"cell": "",
|
||||
"keywords": "кпп"
|
||||
},
|
||||
{
|
||||
"name": "Основной ОКВЭД",
|
||||
"var": "okved",
|
||||
"cell": "",
|
||||
"keywords": "основной оквэд"
|
||||
},
|
||||
{
|
||||
"name": "Дополнительные ОКВЭД",
|
||||
"var": "okvedExtra",
|
||||
"cell": "",
|
||||
"keywords": "дополнительные оквэд"
|
||||
},
|
||||
{
|
||||
"name": "Должность руководителя",
|
||||
"var": "chiefPosition",
|
||||
"cell": "",
|
||||
"keywords": "должность руководителя"
|
||||
},
|
||||
{
|
||||
"name": "ФИО руководителя",
|
||||
"var": "chiefFio",
|
||||
"cell": "",
|
||||
"keywords": "фио руководителя"
|
||||
},
|
||||
{
|
||||
"name": "Должность ответственного лица (безопасность ЗОКИИ)",
|
||||
"var": "responsiblePosition",
|
||||
"cell": "",
|
||||
"keywords": "должность ответственного лица"
|
||||
},
|
||||
{
|
||||
"name": "ФИО ответственного лица (безопасность ЗОКИИ)",
|
||||
"var": "responsibleFio",
|
||||
"cell": "",
|
||||
"keywords": "фио ответственного лица"
|
||||
},
|
||||
{
|
||||
"name": "Подразделение по безопасности ОКИИ",
|
||||
"var": "ispdnDepartment",
|
||||
"cell": "",
|
||||
"keywords": "структур.*подраздел"
|
||||
},
|
||||
{
|
||||
"name": "Рабочий телефон ответственного",
|
||||
"var": "ispdnPhone",
|
||||
"cell": "",
|
||||
"keywords": "рабочий телефон ответственного|мобильный телефон ответственного"
|
||||
},
|
||||
{
|
||||
"name": "Электронная почта ответственного",
|
||||
"var": "ispdnEmail",
|
||||
"cell": "",
|
||||
"keywords": "электронная почта ответственного"
|
||||
},
|
||||
{
|
||||
"name": "Должность специалиста по ЗИ/администратора",
|
||||
"var": "administratorPosition",
|
||||
"cell": "",
|
||||
"keywords": "должность специалиста по защите информации|должность.*администрат"
|
||||
},
|
||||
{
|
||||
"name": "ФИО специалиста по ЗИ/администратора",
|
||||
"var": "administratorFio",
|
||||
"cell": "",
|
||||
"keywords": "фио специалиста по защите информации|фио.*администрат"
|
||||
},
|
||||
{
|
||||
"name": "Право владения зданием",
|
||||
"var": "buildingRight",
|
||||
"cell": "",
|
||||
"keywords": "право владения зданием"
|
||||
},
|
||||
{
|
||||
"name": "Сумма прибыли за год",
|
||||
"var": "profit",
|
||||
"cell": "",
|
||||
"keywords": "сумма прибыли"
|
||||
},
|
||||
{
|
||||
"name": "Сумма налога с прибыли",
|
||||
"var": "profitTax",
|
||||
"cell": "",
|
||||
"keywords": "сумма налога с прибыли"
|
||||
},
|
||||
{
|
||||
"name": "Доменное имя и внешний IP",
|
||||
"var": "domainName",
|
||||
"cell": "",
|
||||
"keywords": "доменное имя и внешний сетевой адрес"
|
||||
},
|
||||
{
|
||||
"name": "Сайт организации",
|
||||
"var": "site_name",
|
||||
"cell": "",
|
||||
"keywords": "сайт организации"
|
||||
},
|
||||
{
|
||||
"name": "Должность ответственного за сайт",
|
||||
"var": "site_responsible_position",
|
||||
"cell": "",
|
||||
"keywords": "должность ответственного за сайт"
|
||||
},
|
||||
{
|
||||
"name": "ФИО ответственного за сайт",
|
||||
"var": "site_responsible_fio",
|
||||
"cell": "",
|
||||
"keywords": "фио ответственного за сайт"
|
||||
},
|
||||
{
|
||||
"name": "Разработчик сайта",
|
||||
"var": "site_service_provider",
|
||||
"cell": "",
|
||||
"keywords": "организации, которая разрабатывала и размещала корпоративный сайт"
|
||||
},
|
||||
{
|
||||
"name": "ИНН разработчика сайта",
|
||||
"var": "site_service_provider_inn",
|
||||
"cell": "",
|
||||
"keywords": "инн организации, которая разрабатывала"
|
||||
},
|
||||
{
|
||||
"name": "Размещение сайта",
|
||||
"var": "site_hosting",
|
||||
"cell": "",
|
||||
"keywords": "размещение сайта"
|
||||
},
|
||||
{
|
||||
"name": "Адрес расположения серверов",
|
||||
"var": "site_hosting_address",
|
||||
"cell": "",
|
||||
"keywords": "адрес расположения серверов"
|
||||
},
|
||||
{
|
||||
"name": "Облачная платформа",
|
||||
"var": "cloud_platform",
|
||||
"cell": "",
|
||||
"keywords": "облачную платформу"
|
||||
},
|
||||
{
|
||||
"name": "Аренда выделенного сервера",
|
||||
"var": "cloud_dedicated_server",
|
||||
"cell": "",
|
||||
"keywords": "выделенный сервер"
|
||||
},
|
||||
{
|
||||
"name": "Аренда виртуального ЦОД",
|
||||
"var": "cloud_vdc",
|
||||
"cell": "",
|
||||
"keywords": "виртуальный дата-центр"
|
||||
},
|
||||
{
|
||||
"name": "Серверы в дата-центре",
|
||||
"var": "cloud_datacenter",
|
||||
"cell": "",
|
||||
"keywords": "сервера в дата-центре"
|
||||
},
|
||||
{
|
||||
"name": "Средства контейнеризации",
|
||||
"var": "cloud_container",
|
||||
"cell": "",
|
||||
"keywords": "контейнеризации"
|
||||
},
|
||||
{
|
||||
"name": "Собственная серверная (ЦОД)",
|
||||
"var": "cloud_own_server",
|
||||
"cell": "",
|
||||
"keywords": "собственная серверная"
|
||||
},
|
||||
{
|
||||
"name": "Общее количество компьютеров",
|
||||
"var": "computers_total",
|
||||
"cell": "",
|
||||
"keywords": "общее количество компьютеров"
|
||||
},
|
||||
{
|
||||
"name": "Компьютеры, подключённые к объектам КИИ",
|
||||
"var": "computers_kii",
|
||||
"cell": "",
|
||||
"keywords": "компьютеров подключенных к объектам кии"
|
||||
},
|
||||
{
|
||||
"name": "Центр мониторинга",
|
||||
"var": "monitoring_center",
|
||||
"cell": "",
|
||||
"keywords": "к какому центру подключены"
|
||||
},
|
||||
{
|
||||
"name": "Детали подключения к центру мониторинга",
|
||||
"var": "monitoring_details",
|
||||
"cell": "",
|
||||
"keywords": "детали подключения"
|
||||
},
|
||||
{
|
||||
"name": "Дополнительная информация",
|
||||
"var": "additionalInfo",
|
||||
"cell": "",
|
||||
"keywords": "дополнительная информация"
|
||||
},
|
||||
{
|
||||
"name": "Физическая охрана (ЧОП, сторожа, охранники)",
|
||||
"var": "physicalGuard",
|
||||
"cell": "",
|
||||
"keywords": "физическая охрана"
|
||||
},
|
||||
{
|
||||
"name": "Пожарная сигнализация",
|
||||
"var": "physicalFireAlarm",
|
||||
"cell": "",
|
||||
"keywords": "пожарная сигнализация"
|
||||
},
|
||||
{
|
||||
"name": "Тревожная кнопка",
|
||||
"var": "physicalPanicButton",
|
||||
"cell": "",
|
||||
"keywords": "тревожная кнопка"
|
||||
},
|
||||
{
|
||||
"name": "Решётки на окнах",
|
||||
"var": "physicalBars",
|
||||
"cell": "",
|
||||
"keywords": "решетки на окнах"
|
||||
},
|
||||
{
|
||||
"name": "Металлические двери",
|
||||
"var": "physicalMetalDoors",
|
||||
"cell": "",
|
||||
"keywords": "металлические двери"
|
||||
},
|
||||
{
|
||||
"name": "Система видеонаблюдения",
|
||||
"var": "physicalVideoSurveillance",
|
||||
"cell": "",
|
||||
"keywords": "система видеонаблюдения"
|
||||
},
|
||||
{
|
||||
"name": "Видеодомофон",
|
||||
"var": "physicalVideoIntercom",
|
||||
"cell": "",
|
||||
"keywords": "видеодомофон"
|
||||
},
|
||||
{
|
||||
"name": "Турникет на входе",
|
||||
"var": "physicalTurnstile",
|
||||
"cell": "",
|
||||
"keywords": "турникет на входе"
|
||||
},
|
||||
{
|
||||
"name": "Журнал учёта посетителей",
|
||||
"var": "physicalVisitorLog",
|
||||
"cell": "",
|
||||
"keywords": "журнал учета посетителей|журнал учёта посетителей"
|
||||
}
|
||||
],
|
||||
"criterion_fields": [
|
||||
{
|
||||
"num": "1",
|
||||
"name": "Максимальное число пострадавших людей",
|
||||
"keywords": "пострадавш",
|
||||
"unit": "человек"
|
||||
},
|
||||
{
|
||||
"num": "2б",
|
||||
"name": "Люди с нарушенными условиями жизнедеятельности",
|
||||
"keywords": "условия жизнедеятельности",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "3б",
|
||||
"name": "Люди, для которых недоступны транспортные услуги",
|
||||
"keywords": "транспортн",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "4а",
|
||||
"name": "Абоненты, для которых недоступны услуги связи",
|
||||
"keywords": "абонент",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "5а",
|
||||
"name": "Максимальное время недоступности государственной услуги",
|
||||
"keywords": "недоступна государственная услуга|недоступности государственной",
|
||||
"unit": "часов"
|
||||
},
|
||||
{
|
||||
"num": "5б",
|
||||
"name": "Время, в течение которого госуслуга не может быть оказана",
|
||||
"keywords": "регламентом|административным регламентом",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"num": "11б",
|
||||
"name": "Люди, подверженные вредным воздействиям на окружающую среду",
|
||||
"keywords": "вредным воздействиям",
|
||||
"unit": "тыс. человек"
|
||||
},
|
||||
{
|
||||
"num": "13а",
|
||||
"name": "Снижение объёма продукции ГОЗ",
|
||||
"keywords": "объем продукции|объём продукции",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"num": "13б",
|
||||
"name": "Увеличение времени изготовления единицы продукции ГОЗ",
|
||||
"keywords": "время изготовления",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"num": "14",
|
||||
"name": "Максимальное время недоступности ИС (оборона)",
|
||||
"keywords": "недоступна пользователю",
|
||||
"unit": "часов"
|
||||
},
|
||||
{
|
||||
"num": "2а",
|
||||
"name": "Территория нарушения жизнеобеспечения населения",
|
||||
"keywords": "территория.*жизнедеятельности|жизнедеятельности населения",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "3а",
|
||||
"name": "Территория нарушения транспортного сообщения",
|
||||
"keywords": "территория.*транспорт",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "3в",
|
||||
"name": "Типы нарушенных перевозок и грузов",
|
||||
"keywords": "типам нарушенных перевозок|типы нарушенных перевозок",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "3г",
|
||||
"name": "Категория объекта транспортной инфраструктуры",
|
||||
"keywords": "категорированию объектов транспортной|категорию объекта транспортной",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "4б",
|
||||
"name": "Уровень органа власти, для которого недоступны услуги связи",
|
||||
"keywords": "уровню органа государственной власти",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "6",
|
||||
"name": "Уровень государственного органа (невыполнение функции)",
|
||||
"keywords": "функционирования государственного органа|государственного органа в части",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "7",
|
||||
"name": "Уровень международного договора",
|
||||
"keywords": "международного договора",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "11а",
|
||||
"name": "Территория вредных воздействий на окружающую среду",
|
||||
"keywords": "территория.*окружающую среду|окружающая среда может",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "12",
|
||||
"name": "Уровень пункта управления (ситуационного центра)",
|
||||
"keywords": "пункта управления|ситуационного центра",
|
||||
"unit": "вариант"
|
||||
},
|
||||
{
|
||||
"num": "13.1",
|
||||
"name": "Статус в кооперации головного исполнителя ГОЗ",
|
||||
"keywords": "кооперации головного исполнителя|статусу в кооперации",
|
||||
"unit": "вариант"
|
||||
}
|
||||
]
|
||||
}
|
||||
+38
-5
@@ -97,13 +97,18 @@ def normalize_address(text: str) -> str:
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def clean_value(val):
|
||||
"""Очистка и нормализация значения ячейки Excel."""
|
||||
def clean_value(val, as_date=False):
|
||||
"""Очистка и нормализация значения ячейки Excel.
|
||||
|
||||
as_date=True — конвертировать серийный номер Excel в дату (только для
|
||||
полей-дат, напр. ogrnDate). Для финансовых полей НЕ вызывается,
|
||||
иначе число 50 000 руб. превратится в «дату» (см. аудит importer.py:105-109).
|
||||
"""
|
||||
if val is None:
|
||||
return ''
|
||||
if isinstance(val, (int, float)):
|
||||
# Попытка распознать дату (серийный номер Excel)
|
||||
if 10000 < val < 80000:
|
||||
# Попытка распознать дату (серийный номер Excel) — только для полей-дат
|
||||
if as_date and 10000 < val < 80000:
|
||||
serial = int(val)
|
||||
if serial > 60:
|
||||
serial -= 1
|
||||
@@ -169,6 +174,21 @@ def import_187fz(filepath: str, log_fn: Optional[Callable] = None, settings_path
|
||||
finally:
|
||||
wb.close()
|
||||
|
||||
# Ответы раздела «Физическая безопасность» (да/нет) — собираем в словарь,
|
||||
# чтобы текст организационных мер генерировался с учётом реальных средств.
|
||||
ps_keys = [
|
||||
'physicalGuard', 'physicalFireAlarm', 'physicalPanicButton',
|
||||
'physicalBars', 'physicalMetalDoors', 'physicalVideoSurveillance',
|
||||
'physicalVideoIntercom', 'physicalTurnstile', 'physicalVisitorLog',
|
||||
]
|
||||
ps = {}
|
||||
for k in ps_keys:
|
||||
v = str(data.get(k) or '').strip()
|
||||
if v:
|
||||
ps[k] = v
|
||||
if ps:
|
||||
data['physicalSecurity'] = ps
|
||||
|
||||
# Копирование полей ИСПДн в админа, если админ не заполнен
|
||||
for src, dst in [('ispdnFio', 'administratorFio'), ('ispdnFio', 'adminFio'),
|
||||
('ispdnPosition', 'administratorPosition'), ('ispdnPosition', 'adminPosition'),
|
||||
@@ -253,7 +273,7 @@ def _import_187_common(wb, data, settings, log_fn):
|
||||
if sn.lower() == sheet_part.lower():
|
||||
target_ws = wb[sn]
|
||||
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 key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
||||
val = _extract_single_fio(val)
|
||||
@@ -273,6 +293,8 @@ def _import_187_common(wb, data, settings, log_fn):
|
||||
if val:
|
||||
if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
||||
val = _extract_single_fio(val)
|
||||
elif 'дата' in name.lower() or 'date' in key.lower():
|
||||
val = clean_value(val, as_date=True)
|
||||
data[key] = val
|
||||
log_fn(f" {name} [по ключу]: {val}")
|
||||
break
|
||||
@@ -534,6 +556,17 @@ def _import_187_is_list(wb, data, settings, log_fn):
|
||||
if _set(field_lower, 'tech_measures', val_b):
|
||||
log_fn(f" Тех. меры: {val_b[:60]}")
|
||||
continue
|
||||
# --- Показатели критериев значимости (ПП-127) → criterion_input ---
|
||||
# Строки вида «Показатель 1. …» / «Показатель 2а. …» / «Показатель 13.1. …»
|
||||
m_crit = re.match(r'^показател[ья]?\s*(\d+(?:\.\d+)?)([а-яА-Я]?)', field_lower)
|
||||
if m_crit:
|
||||
_num_crit = m_crit.group(1) + m_crit.group(2)
|
||||
# нормализация: «10.1» → «10.1», «13.1» → «13.1», «2а» → «2а»
|
||||
_num_crit = _num_crit.replace(' .', '.').strip()
|
||||
if val_b and 'выбрать' not in field_lower and not _is_hint(val_b):
|
||||
is_obj.setdefault('criterion_input', {})[_num_crit] = str(val_b).strip()
|
||||
log_fn(f" Показатель {_num_crit}: {str(val_b)[:60]}")
|
||||
continue
|
||||
if 'интернет' in field_lower:
|
||||
is_obj['isInternet'] = is_checked
|
||||
continue
|
||||
|
||||
+40
-2
@@ -80,6 +80,12 @@ class InformationSystem:
|
||||
org_measures: str = "" # Организационные меры (\n — разделитель)
|
||||
tech_measures: str = "" # Технические меры (\n — разделитель)
|
||||
|
||||
# Введённые значения показателей ПП-127 для авторасчёта: {num: значение}
|
||||
# num — «1», «2а», «2б», …, «14»; значение — число (количественные)
|
||||
# или выбранный вариант (качественные). Заполняется окном авторасчёта,
|
||||
# импортом из опросника или вручную; используется в _build_categorization_indicators.
|
||||
criterion_input: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Company:
|
||||
@@ -130,14 +136,27 @@ class Company:
|
||||
profit_tax: str = "" # Сумма налога с прибыли за предыдущий год
|
||||
|
||||
# Показатель 8 (ущерб субъекту КИИ): доходы за 5 лет и суммарный ущерб
|
||||
income_year_1: str = "" # Доход за 5 лет назад (руб.)
|
||||
# В форме — одна строка «Доход за 5 лет (суммарно)»; по годам разбивает сам оператор.
|
||||
income_5y: str = "" # Доход за 5 лет суммарно (руб.)
|
||||
income_year_1: str = "" # Доход за 5 лет назад (руб.) — обратная совместимость
|
||||
income_year_2: str = "" # Доход за 4 года назад (руб.)
|
||||
income_year_3: str = "" # Доход за 3 года назад (руб.)
|
||||
income_year_4: str = "" # Доход за 2 года назад (руб.)
|
||||
income_year_5: str = "" # Доход за прошлый год (руб.)
|
||||
income_loss: str = "" # Суммарный экономический ущерб от нарушения
|
||||
income_loss: str = "" # Снижение уровня дохода (суммарный ущерб) от нарушения
|
||||
# критических процессов (руб.) — ΔДоходов для п. 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)
|
||||
information_systems: List[InformationSystem] = field(default_factory=list)
|
||||
@@ -146,6 +165,11 @@ class Company:
|
||||
# Наименование структурного подразделения по безопасности
|
||||
security_department: str = ""
|
||||
|
||||
# Ответы раздела «Физическая безопасность» опросника (да/нет):
|
||||
# охрана, пожарная сигнализация, тревожная кнопка, решётки, металлические двери,
|
||||
# видеонаблюдение, видеодомофон, турникет, журнал посетителей.
|
||||
physical_security: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def update_declensions(self, inflect_func):
|
||||
"""Обновить склонения полного названия"""
|
||||
if self.full_name:
|
||||
@@ -208,6 +232,20 @@ class Company:
|
||||
d["okved_extra"] = []
|
||||
valid = {f.name for f in cls.__dataclass_fields__.values()}
|
||||
obj = cls(**{k: v for k, v in d.items() if k in valid})
|
||||
# Миграция: старые файлы с доходами по годам → суммарный доход за 5 лет
|
||||
if not (getattr(obj, 'income_5y', '') or '').strip():
|
||||
_sum = 0.0
|
||||
_has = False
|
||||
for i in range(1, 6):
|
||||
v = str(getattr(obj, f'income_year_{i}', '') or '').strip()
|
||||
if v:
|
||||
try:
|
||||
_sum += float(v.replace(' ', '').replace(',', '.').replace('\u00a0', ''))
|
||||
_has = True
|
||||
except Exception:
|
||||
pass
|
||||
if _has:
|
||||
obj.income_5y = f"{_sum:,.0f}".replace(',', ' ')
|
||||
# Если в поле ФИО несколько человек — оставить одного (старые опросники)
|
||||
for fio_field in ('chief_fio', 'responsible_fio', 'admin_fio'):
|
||||
val = getattr(obj, fio_field, '')
|
||||
|
||||
+95
-9
@@ -11,6 +11,24 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def _ubi_num(text):
|
||||
"""Код УБИ из строки → int: «УБИ.069 Название» → 69. None, если кода нет."""
|
||||
m = re.search(r'уби\.?\s*(\d+)', str(text), re.IGNORECASE)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
def sort_threats_by_ubi(items):
|
||||
"""Сортировка угроз по возрастанию кода УБИ (УБИ.001, УБИ.002, …).
|
||||
|
||||
Строки без кода УБИ уходят в конец (по алфавиту).
|
||||
"""
|
||||
def _key(t):
|
||||
n = _ubi_num(t)
|
||||
return (0, n) if n is not None else (1, str(t).lower())
|
||||
return sorted(items, key=_key)
|
||||
|
||||
|
||||
def load_threats(dict_dir=None):
|
||||
@@ -42,8 +60,26 @@ BASE_KEYWORDS = [
|
||||
'вредоносн', 'несанкционированн', 'утечк', 'перехват', 'модификац',
|
||||
'уничтожен', 'отказ в обслуживании', 'нсд', 'биос', 'пароль',
|
||||
'криптографическ', 'шифрован', 'аутентификац',
|
||||
# базовые угрозы локальных сетей / клиент-серверных архитектур
|
||||
'доступ', 'данн', 'систем', 'конфигурац', 'целостност', 'информац',
|
||||
'программн', 'пользовател', 'авторизац', 'сеанс', 'журнал', 'реестр',
|
||||
'файл', 'память', 'буфер', 'ресурс', 'сервер', 'клиент', 'учётн',
|
||||
'учетн', 'идентификац', 'привилег', 'средств', 'сетев', 'локальн',
|
||||
]
|
||||
|
||||
# Технологии, которые по умолчанию НЕ используются на объекте.
|
||||
# Если в характеристиках объекта есть явные упоминания — группа возвращается.
|
||||
TECH_GROUPS = {
|
||||
'виртуализац': ['виртуал', 'гипервизор', 'контейнер'],
|
||||
'облако': ['облачн', 'облак'],
|
||||
'мобильн': ['мобильн', 'смартфон', 'планшет'],
|
||||
'грид': ['грид', 'суперкомпьютер'],
|
||||
'большие данные': ['больших данных', 'хранилищ'],
|
||||
'беспроводн': ['беспроводн', 'wi-fi', 'wifi'],
|
||||
'smart-карты': ['smart-карт', 'смарт-карт'],
|
||||
'машинное обучение': ['машинного обучения', 'искусственного интеллекта', 'нейросет'],
|
||||
}
|
||||
|
||||
# Объект имеет подключение к сети Интернет / сеть электросвязи
|
||||
INTERNET_KEYWORDS = [
|
||||
'удалённ', 'удаленн', 'сетев', 'фишинг', 'ddos', 'отказ в обслуживании',
|
||||
@@ -153,6 +189,21 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
||||
is_asu = _is_asu(isys)
|
||||
is_network = _is_network(isys)
|
||||
|
||||
# --- Технологии, которые явно НЕ используются на объекте ---
|
||||
# Если в характеристиках объекта (описание, архитектура, ПО, сфера) нет
|
||||
# упоминаний технологии — угрозы этой группы исключаем.
|
||||
tech_context = ' '.join([
|
||||
str(getattr(isys, 'description', '') or ''),
|
||||
str(getattr(isys, 'architecture', '') or ''),
|
||||
str(getattr(isys, 'hw_sw', '') or ''),
|
||||
str(getattr(isys, 'os_sw', '') or ''),
|
||||
str(getattr(isys, 'app_sw', '') or ''),
|
||||
str(getattr(isys, 'object_type', '') or ''),
|
||||
str(getattr(isys, 'sphere', '') or ''),
|
||||
str(getattr(isys, 'net_category', '') or ''),
|
||||
str(getattr(isys, 'typical_object', '') or ''),
|
||||
]).lower()
|
||||
|
||||
# --- Слова-исключения (если встретились в названии угрозы — она неактуальна) ---
|
||||
exclude = []
|
||||
|
||||
@@ -167,6 +218,19 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
||||
if is_asu:
|
||||
# АСУ: нет BIOS/виртуализации/браузера в привычном понимании
|
||||
exclude += ['биос', 'грид', 'гипервизор', 'виртуальн', 'браузер']
|
||||
else:
|
||||
# Не АСУ — исключаем угрозы АСУ ТП / технологических процессов
|
||||
exclude += ['технологическ', 'датчик', 'исполнительн', 'асутп', 'scada',
|
||||
'промышленн', 'контроллер', 'числовым программным управлением',
|
||||
'программируемых логических']
|
||||
|
||||
# Технологии, не упомянутые в характеристиках объекта → исключаем
|
||||
for tech_name, kws in TECH_GROUPS.items():
|
||||
if any(kw in tech_context for kw in kws):
|
||||
continue # технология используется — угрозы оставляем
|
||||
# Технология не упомянута — исключаем её угрозы
|
||||
for kw in kws:
|
||||
exclude.append(kw)
|
||||
|
||||
if not is_network and not is_asu:
|
||||
# Обычная ИС: неактуальны чисто сетевые/телеком-угрозы без указания
|
||||
@@ -198,9 +262,9 @@ def suggest_threats_program(isys, threats=None, limit=None):
|
||||
sc = sum(1 for kw in pos if kw in tl)
|
||||
scored.append((sc, t))
|
||||
|
||||
# Сортируем по релевантности (сначала — с наибольшим совпадением)
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
result = [t for sc, t in scored if sc > 0]
|
||||
# Сортируем по возрастанию кода УБИ (УБИ.001 → УБИ.211…)
|
||||
scored = [t for sc, t in scored if sc > 0]
|
||||
result = sort_threats_by_ubi(scored)
|
||||
|
||||
if limit and len(result) > limit:
|
||||
result = result[:limit]
|
||||
@@ -234,19 +298,18 @@ BASE_ORG_MEASURES = [
|
||||
'Утверждение перечня лиц, имеющих доступ к объекту',
|
||||
]
|
||||
|
||||
# Базовый набор тех. мер
|
||||
# Базовый набор тех. мер — только универсальные, не требующие конкретных СЗИ.
|
||||
# Меры, зависящие от средств защиты (антивирус, СЗИ от НСД, регистрация событий
|
||||
# и т.п.), добавляются отдельно по факту наличия СЗИ (DEFENSE_TOOL_MEASURES).
|
||||
BASE_TECH_MEASURES = [
|
||||
'Идентификация и аутентификация пользователей',
|
||||
'Управление доступом',
|
||||
'Ограничение программной среды',
|
||||
'Антивирусная защита',
|
||||
'Защита информации от несанкционированного доступа (СЗИ от НСД)',
|
||||
'Регистрация и мониторинг событий безопасности',
|
||||
'Резервное копирование и восстановление',
|
||||
'Защита технических средств и линий связи',
|
||||
]
|
||||
|
||||
# Средства защиты → доп. тех. меры
|
||||
# Средства защиты → доп. тех. меры (по факту наличия СЗИ у объекта)
|
||||
DEFENSE_TOOL_MEASURES = [
|
||||
('межсетев', 'Межсетевое экранирование'),
|
||||
('firewall', 'Межсетевое экранирование'),
|
||||
@@ -260,6 +323,23 @@ DEFENSE_TOOL_MEASURES = [
|
||||
('виртуализ', 'Защита среды виртуализации'),
|
||||
('vmware', 'Защита среды виртуализации'),
|
||||
('hyper-v', 'Защита среды виртуализации'),
|
||||
('антивирус', 'Антивирусная защита'),
|
||||
('kaspersky', 'Антивирусная защита'),
|
||||
('eset', 'Антивирусная защита'),
|
||||
('dr.web', 'Антивирусная защита'),
|
||||
('аваст', 'Антивирусная защита'),
|
||||
('avast', 'Антивирусная защита'),
|
||||
('сзи', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('от нсд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('dallas', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('secret net', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('аккорд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
|
||||
('siem', 'Регистрация и мониторинг событий безопасности'),
|
||||
('dlp', 'Регистрация и мониторинг событий безопасности'),
|
||||
('мониторинг', 'Регистрация и мониторинг событий безопасности'),
|
||||
('регистрац', 'Регистрация и мониторинг событий безопасности'),
|
||||
('доверенная загрузка', 'Контроль (доверенная) загрузки'),
|
||||
('резервн', 'Резервное копирование и восстановление'),
|
||||
]
|
||||
|
||||
|
||||
@@ -331,8 +411,13 @@ def auto_fill_security(isys, threats=None):
|
||||
if not (getattr(isys, 'org_measures', '') or '').strip():
|
||||
res['org_measures'] = '\n'.join(BASE_ORG_MEASURES)
|
||||
|
||||
# 7. Тех. меры — базовые + по СЗИ объекта
|
||||
# 7. Тех. меры — базовые + по СЗИ объекта.
|
||||
# Только если объекту присвоена категория значимости (1/2/3):
|
||||
# для незначимого объекта технические меры не требуются.
|
||||
if not (getattr(isys, 'tech_measures', '') or '').strip():
|
||||
cat = (getattr(isys, 'category', '') or '').strip()
|
||||
has_category = cat in ('1', '2', '3')
|
||||
if has_category:
|
||||
tech = list(BASE_TECH_MEASURES)
|
||||
defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
|
||||
dlow = defense.lower()
|
||||
@@ -340,6 +425,7 @@ def auto_fill_security(isys, threats=None):
|
||||
if kw in dlow and measure not in tech:
|
||||
tech.append(measure)
|
||||
res['tech_measures'] = '\n'.join(tech)
|
||||
# else: категория не присвоена — технические меры не заполняем
|
||||
|
||||
return res
|
||||
|
||||
|
||||
+890
-160
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_profit}}', 'A', 'Сумма прибыли за предыдущий год (руб.)', 'company:profit'),
|
||||
('{{company_profit_tax}}', 'A', 'Сумма налога с прибыли (руб.)', 'company:profit_tax'),
|
||||
('{{company_income_year_1}}', 'A', 'Доход за 5 лет назад (руб.) — показатель 8', 'company:income_year_1'),
|
||||
('{{company_income_5y}}', 'A', 'Доход за 5 лет суммарно (руб.) — показатель 8', 'company:income_5y'),
|
||||
('{{company_income_year_1}}', 'A', 'Доход за 5 лет назад (руб.) — показатель 8 (обратная совместимость)', 'company:income_year_1'),
|
||||
('{{company_income_year_2}}', 'A', 'Доход за 4 года назад (руб.) — показатель 8', 'company:income_year_2'),
|
||||
('{{company_income_year_3}}', 'A', 'Доход за 3 года назад (руб.) — показатель 8', 'company:income_year_3'),
|
||||
('{{company_income_year_4}}', 'A', 'Доход за 2 года назад (руб.) — показатель 8', 'company:income_year_4'),
|
||||
@@ -357,7 +358,10 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
||||
'incidents': getattr(isys, 'incidents', '') or '—',
|
||||
'criteria_values': getattr(isys, 'criteria_values', '') or '—',
|
||||
'criteria_justification': getattr(isys, 'criteria_justification', '') or '—',
|
||||
'org_measures': getattr(isys, 'org_measures', '') or '—',
|
||||
# Орг. меры: если не заполнены вручную — автогенерация по опроснику
|
||||
# (с учётом ответов раздела «Физическая безопасность»).
|
||||
'org_measures': (getattr(isys, 'org_measures', '') or '')
|
||||
or _build_org_measures_text(company, isys),
|
||||
'tech_measures': getattr(isys, 'tech_measures', '') or '—',
|
||||
})
|
||||
loops['information_systems'] = items
|
||||
@@ -373,9 +377,610 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
||||
})
|
||||
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
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ПОКАЗАТЕЛИ КРИТЕРИЕВ ЗНАЧИМОСТИ (ПП-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):
|
||||
"""Код ОКВЭД → «код - название».
|
||||
"""Код ОКВЭД → «код название» (без дефиса), например «27.32.2 Производство силовых кабелей».
|
||||
|
||||
Принимает и голый код («27.32.2»), и строку с названием
|
||||
(«27.32.2 - Производство силовых кабелей» / «27.32.2 Производство силовых кабелей»).
|
||||
1) Ищем в словаре dictionaries/okved.json (строки вида «27.32.2 Название»).
|
||||
2) Если не нашли — запрашиваем DaData suggest okved2 (если есть токен).
|
||||
3) Иначе возвращаем код как есть.
|
||||
3) Иначе возвращаем нормализованную строку «код название» как есть.
|
||||
"""
|
||||
code = (code or '').strip()
|
||||
if not code:
|
||||
return ''
|
||||
# Извлекаем код из входной строки: «27.32.2 - Производство силовых кабелей» → «27.32.2»
|
||||
m = re.match(r'^\s*(\d+(?:\.\d+)*)', code)
|
||||
code_only = m.group(1) if m else code.split()[0].rstrip('.')
|
||||
# 1) Словарь
|
||||
try:
|
||||
import os, json
|
||||
@@ -178,12 +183,8 @@ def resolve_okved(code, dadata_token=None):
|
||||
items = json.load(f)
|
||||
for item in items:
|
||||
item = str(item).strip()
|
||||
# совпадение по началу: «27.32.2» или «27.32.2 - ...» / «27.32.2 ...»
|
||||
if item.split()[0].rstrip('.') == code.rstrip('.'):
|
||||
# нормализуем: «код - название»
|
||||
rest = item[len(item.split()[0]):].strip().lstrip('- ').strip()
|
||||
if rest:
|
||||
return f"{code} - {rest}"
|
||||
# совпадение по коду: «27.32.2» или «27.32.2 Название»
|
||||
if item.split()[0].rstrip('.') == code_only.rstrip('.'):
|
||||
return item
|
||||
except Exception:
|
||||
pass
|
||||
@@ -191,7 +192,7 @@ def resolve_okved(code, dadata_token=None):
|
||||
if dadata_token:
|
||||
try:
|
||||
js, err = _http_json('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/okved2',
|
||||
data={'query': code, 'count': 1},
|
||||
data={'query': code_only, 'count': 1},
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Authorization': f'Token {dadata_token}'})
|
||||
if not err and js:
|
||||
@@ -200,7 +201,12 @@ def resolve_okved(code, dadata_token=None):
|
||||
return sugg[0]['value']
|
||||
except Exception:
|
||||
pass
|
||||
return code
|
||||
# 3) Нормализуем вход: «27.32.2 - Название» → «27.32.2 Название» (без дефиса)
|
||||
rest = code[len(m.group(1)):].strip() if m else ''
|
||||
rest = rest.lstrip('- ').strip()
|
||||
if rest:
|
||||
return f"{code_only} {rest}"
|
||||
return code_only
|
||||
|
||||
|
||||
def _fmt_date(value):
|
||||
@@ -340,8 +346,15 @@ def check_company_dadata(inn, token):
|
||||
# ОКВЭД: основной (okved) и дополнительные (okveds — список словарей с 'code')
|
||||
okved = d.get('okved', '') or ''
|
||||
okved_extra = []
|
||||
for ok in (d.get('okveds') or []):
|
||||
code = (ok or {}).get('code') or ''
|
||||
raw_okveds = d.get('okveds') 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:
|
||||
okved_extra.append(code)
|
||||
|
||||
@@ -518,6 +531,111 @@ def compare_company_values(old_data, new_data):
|
||||
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,
|
||||
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):
|
||||
threats_out = [threats_out]
|
||||
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
|
||||
|
||||
|
||||
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,
|
||||
typical_objects=None, spheres=None, object_types=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:
|
||||
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
|
||||
system = (
|
||||
"Ты — юрист по защите персональных данных (187-ФЗ). Проверь реквизиты организации. "
|
||||
"Ты — специалист по проверке реквизитов организации (ИНН/КПП/ОГРН). "
|
||||
"Проверь реквизиты организации. "
|
||||
"Верни ТОЛЬКО JSON: "
|
||||
'{"оценка": "ok|warning|error", '
|
||||
'"замечания": ["..."], '
|
||||
|
||||
+8
-8
@@ -30,8 +30,8 @@ def create_test_template():
|
||||
doc.add_paragraph('<!-- loop:information_systems -->')
|
||||
doc.add_paragraph('=== IS: {{item.name}} ===')
|
||||
doc.add_paragraph('Category: {{item.category}}')
|
||||
doc.add_paragraph('Threat type: {{item.threat_type}}')
|
||||
doc.add_paragraph('Defence level: {{item.defence_level}}')
|
||||
doc.add_paragraph('Sphere: {{item.sphere}}')
|
||||
doc.add_paragraph('Object type: {{item.object_type}}')
|
||||
doc.add_paragraph('')
|
||||
|
||||
# Inner loop: commission
|
||||
@@ -72,12 +72,12 @@ def test_zip_loop_expansion():
|
||||
CommissionMember(role='Член комиссии', position='Бухгалтер', fio='Сидорова А.А.'),
|
||||
],
|
||||
information_systems=[
|
||||
InformationSystem(name='ИС-1 "Бухгалтерия"', category='специальные',
|
||||
pd_count='менее 100 000', threat_type='1', defence_level='2'),
|
||||
InformationSystem(name='ИС-2 "Кадры"', category='иные',
|
||||
pd_count='более 100 000', threat_type='3', defence_level='4'),
|
||||
InformationSystem(name='ИС-3 "Склад"', category='общедоступные',
|
||||
pd_count='менее 100 000', threat_type='2', defence_level='3'),
|
||||
InformationSystem(name='ОКИИ-1 "Платёжный шлюз"', category='1',
|
||||
sphere='Банковская сфера и финансовый рынок', object_type='АСУ'),
|
||||
InformationSystem(name='ОКИИ-2 "Кадры"', category='2',
|
||||
sphere='Наука', object_type='ИС'),
|
||||
InformationSystem(name='ОКИИ-3 "Склад"', category='3',
|
||||
sphere='Здравоохранение', object_type='ИС'),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user