- criteria_applicability.json: Атомная (ПП-4: без 4, 10-10.7, 14; +6), Связь (ПП-402: +6, 8, 9), Оборонная (ПП-796: 1-4, 6-13.1, без 5/10/14), Транспорт (+8, 9, 13, 14), Наука (1, 7-9, 11, 13) - variables.py: _calc_criterion_8_opk (U8=(C+A+X)/Rгод*100%, ПП-796 п.18-20), _calc_criterion_13_opk (13а: V=(1-(Vmax-Vинц)/VГОЗ)*100%, 13б: T=(1-tнорм/tненорм)*100%) - ui.py: ОПК-поля в окне авторасчёта (C/A/X, Vmax/Vинц/VГОЗ, tнорм/tненорм), сохранение в criterion_input
1179 lines
67 KiB
Python
1179 lines
67 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""DokoGen — Справочник переменных шаблонов и резолвер значений"""
|
||
|
||
from datetime import datetime
|
||
from typing import Dict, Any, List, Optional
|
||
import re
|
||
|
||
from . import declension as dc
|
||
from .models import Company, CommissionMember
|
||
|
||
|
||
def _norm_name(s):
|
||
"""Нормализация ФИО для сравнения: регистр, пробелы, точки, тире игнорируются."""
|
||
s = (s or '').lower().replace('ё', 'е')
|
||
return re.sub(r'[\s.,;:()«»"\'\-]+', '', s)
|
||
|
||
|
||
# ============================================================
|
||
# ОПИСАНИЕ ПЕРЕМЕННЫХ
|
||
# ============================================================
|
||
# Формат: (имя_переменной, группа, описание, ключ_резолвера)
|
||
# Группы: A — общие, B — 187-ФЗ
|
||
|
||
VARIABLE_DEFS = [
|
||
# === Полное название организации (6 падежей) ===
|
||
('{{company_full_name}}', 'A', 'Полное наименование организации (им.п.)', 'company:full_name'),
|
||
('{{company_full_name_genitive}}', 'A', 'Полное наименование (род.п.)', 'decl:full_name:gent'),
|
||
('{{company_full_name_dative}}', 'A', 'Полное наименование (дат.п.)', 'decl:full_name:datv'),
|
||
('{{company_full_name_accs}}', 'A', 'Полное наименование (вин.п.)', 'decl:full_name:accs'),
|
||
('{{company_full_name_ablt}}', 'A', 'Полное наименование (твор.п.)', 'decl:full_name:ablt'),
|
||
('{{company_full_name_loct}}', 'A', 'Полное наименование (пр.п.)', 'decl:full_name:loct'),
|
||
|
||
# === Краткое название организации (6 падежей) ===
|
||
('{{company_short_name}}', 'A', 'Краткое наименование (им.п.)', 'company:short_name'),
|
||
('{{company_short_name_genitive}}', 'A', 'Краткое наименование (род.п.)', 'decl:short_name:gent'),
|
||
('{{company_short_name_dative}}', 'A', 'Краткое наименование (дат.п.)', 'decl:short_name:datv'),
|
||
('{{company_short_name_accs}}', 'A', 'Краткое наименование (вин.п.)', 'decl:short_name:accs'),
|
||
('{{company_short_name_ablt}}', 'A', 'Краткое наименование (твор.п.)', 'decl:short_name:ablt'),
|
||
('{{company_short_name_loct}}', 'A', 'Краткое наименование (пр.п.)', 'decl:short_name:loct'),
|
||
|
||
# === Должностные лица ===
|
||
('{{chief_position}}', 'A', 'Должность руководителя', 'company:chief_position'),
|
||
('{{chief_fio_initials}}', 'A', 'И.О. Фамилия руководителя', 'initials:chief'),
|
||
('{{chief_fio_short}}', 'A', 'Фамилия И.О. руководителя', 'short_fio:chief'),
|
||
('{{responsible_position}}', 'A', 'Должность ответственного', 'company:responsible_position'),
|
||
('{{responsible_fio_initials}}', 'A', 'И.О. Фамилия ответственного', 'initials:responsible'),
|
||
('{{responsible_fio_short}}', 'A', 'Фамилия И.О. ответственного', 'short_fio:responsible'),
|
||
('{{admin_position}}', 'A', 'Должность администратора', 'company:admin_position'),
|
||
('{{admin_fio_initials}}', 'A', 'И.О. Фамилия администратора', 'initials:admin'),
|
||
('{{admin_fio_short}}', 'A', 'Фамилия И.О. администратора', 'short_fio:admin'),
|
||
|
||
# === Сведения об организации ===
|
||
('{{company_address}}', 'A', 'Адрес организации', 'company:address'),
|
||
('{{city_name}}', 'A', 'Город', 'company:city_name'),
|
||
('{{company_inn}}', 'A', 'ИНН', 'company:inn'),
|
||
('{{company_ogrn}}', 'A', 'ОГРН', 'company:ogrn'),
|
||
('{{company_ogrn_date}}', 'A', 'Дата ОГРН', 'company:ogrn_date'),
|
||
('{{company_kpp}}', 'A', 'КПП', 'company:kpp'),
|
||
('{{security_department}}', 'A', 'Наименование структурного подразделения по безопасности', 'company:security_department'),
|
||
('{{company_okved}}', 'A', 'Основной ОКВЭД', 'company:okved'),
|
||
('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'),
|
||
('{{company_profit}}', 'A', 'Сумма прибыли за предыдущий год (руб.)', 'company:profit'),
|
||
('{{company_profit_tax}}', 'A', 'Сумма налога с прибыли (руб.)', 'company:profit_tax'),
|
||
('{{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'),
|
||
('{{company_income_year_5}}', 'A', 'Доход за прошлый год (руб.) — показатель 8', 'company:income_year_5'),
|
||
('{{company_income_loss}}', 'A', 'Суммарный экономический ущерб от нарушения критических процессов (руб.) — показатель 8', 'company:income_loss'),
|
||
|
||
# === Комиссия ===
|
||
('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'),
|
||
('{{fstek_district}}', 'A', 'Наименование управления ФСТЭК России (по адресу организации)', 'fstek_district'),
|
||
('{{fstek_recipient_address}}', 'A', 'Адрес управления ФСТЭК России для направления сведений', 'fstek_recipient_address'),
|
||
|
||
# === Специальные переменные ===
|
||
('{{date_year}}', 'A', 'Текущий год', 'date:year'),
|
||
('{{date}}', 'A', 'Текущая дата', 'date:full'),
|
||
('{{document_date}}', 'A', 'Дата документа', 'doc_date'),
|
||
('{{document_number}}', 'A', 'Номер документа', 'doc_number'),
|
||
|
||
# === 187-ФЗ ===
|
||
('{{is_security_requirements}}', 'B', 'Требования к защите объекта КИИ', 'security_reqs'),
|
||
('{{is_list}}', 'B', 'Список всех объектов КИИ (нумерованный)', 'is_list'),
|
||
('{{is_name}}', 'B', 'Наименование первого объекта КИИ', 'is:name'),
|
||
('{{name}}', 'B', 'Название объекта КИИ (алиас)', 'is:name'),
|
||
('{{description}}', 'B', 'Описание объекта КИИ (алиас)', 'is:description'),
|
||
('{{category}}', 'B', 'Категория значимости объекта КИИ (алиас)', 'is:category'),
|
||
('{{number}}', 'B', 'Номер (алиас)', 'doc_number'),
|
||
('{{users}}', 'B', 'Пользователи ОКИИ (алиас)', 'is:users'),
|
||
('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'),
|
||
('{{responsible_fio_accs}}', 'A', 'ФИО ответственного (вин.п.)', 'fio_acl:responsible'),
|
||
('{{commission1}}', 'A', 'Должность члена комиссии 1', 'commission_member:0:position'),
|
||
('{{commission1_fio}}', 'A', 'ФИО члена комиссии 1', 'commission_member:0:fio'),
|
||
('{{commission2}}', 'A', 'Должность члена комиссии 2', 'commission_member:1:position'),
|
||
('{{commission2_fio}}', 'A', 'ФИО члена комиссии 2', 'commission_member:1:fio'),
|
||
('{{commission3}}', 'A', 'Должность члена комиссии 3', 'commission_member:2:position'),
|
||
('{{commission3_fio}}', 'A', 'ФИО члена комиссии 3', 'commission_member:2:fio'),
|
||
]
|
||
|
||
# Маппинг старых переменных <...> → {{...}}
|
||
OLD_VAR_MAP = {
|
||
'<CompanyName1>': '{{company_full_name}}',
|
||
'<CompanyName2>': '{{company_short_name}}',
|
||
'<CompanyName1Gent>': '{{company_full_name_genitive}}',
|
||
'<CompanyName2Gent>': '{{company_short_name_genitive}}',
|
||
'<CompanyName1Datv>': '{{company_full_name_dative}}',
|
||
'<CompanyName2Datv>': '{{company_short_name_dative}}',
|
||
'<CompanyName1Accs>': '{{company_full_name_accs}}',
|
||
'<CompanyName2Accs>': '{{company_short_name_accs}}',
|
||
'<CompanyName1Ablt>': '{{company_full_name_ablt}}',
|
||
'<CompanyName2Ablt>': '{{company_short_name_ablt}}',
|
||
'<CompanyName1Loct>': '{{company_full_name_loct}}',
|
||
'<CompanyName2Loct>': '{{company_short_name_loct}}',
|
||
'<Address>': '{{company_address}}',
|
||
'<INN>': '{{company_inn}}',
|
||
'<OGRN>': '{{company_ogrn}}',
|
||
'<KPP>': '{{company_kpp}}',
|
||
'<ChiefFio>': '{{chief_fio_initials}}',
|
||
'<ChiefFioShort>': '{{chief_fio_short}}',
|
||
'<ChiefPosition>': '{{chief_position}}',
|
||
'<RspFio>': '{{responsible_fio_initials}}',
|
||
'<RspFioShort>': '{{responsible_fio_short}}',
|
||
'<RspPosition>': '{{responsible_position}}',
|
||
'<CommissionList>': '{{commission_list}}',
|
||
'<DocumentNumber>': '{{document_number}}',
|
||
'<DocumentDate>': '{{document_date}}',
|
||
'<Date>': '{{date}}',
|
||
'<SurveyDate>': '{{document_date}}',
|
||
}
|
||
|
||
VAR_PATTERN = re.compile(r'\{\{[^}]+\}\}')
|
||
OLD_VAR_PATTERN = re.compile(r'<[A-Za-z0-9_]+>')
|
||
|
||
|
||
# ============================================================
|
||
# ПОСТРОЕНИЕ СЛОВАРЯ ЗАМЕН
|
||
# ============================================================
|
||
|
||
def build_replacements(
|
||
company: Company,
|
||
is_list: List = None,
|
||
doc_number: str = "",
|
||
doc_date: str = "",
|
||
direction: str = "187fz",
|
||
) -> Dict[str, str]:
|
||
"""Строит полный словарь замен {{переменная}} → значение."""
|
||
if is_list is None:
|
||
is_list = []
|
||
|
||
# ---------- Комиссия ----------
|
||
commission_data = _build_commission_data(company)
|
||
|
||
# ---------- Контекст ----------
|
||
ctx = {
|
||
'company': company,
|
||
'is_list': is_list,
|
||
'commission_data': commission_data,
|
||
'doc_number': doc_number or '',
|
||
'doc_date': doc_date or '',
|
||
'direction': direction,
|
||
}
|
||
|
||
replacements = {}
|
||
for var_name, group, desc, source in VARIABLE_DEFS:
|
||
if direction == '187fz' and group not in ('A', 'B'):
|
||
continue
|
||
value = _resolve(source, ctx)
|
||
replacements[var_name] = str(value) if value else '—'
|
||
|
||
# Индексированные переменные ИС (is_name_1, is_pd_list_2 и т.д.)
|
||
for i, isys in enumerate(is_list, 1):
|
||
_add_is_indexed_vars(replacements, isys, i)
|
||
|
||
# Циклы
|
||
replacements['_loops'] = _build_loop_data(company, is_list)
|
||
return replacements
|
||
|
||
|
||
def _commission_role_display(members) -> List[str]:
|
||
"""Группирует роли комиссии для вывода:
|
||
- Председатель/Секретарь/Заместитель — отдельной строкой со своей ролью;
|
||
- обычные члены — первый получает «Члены комиссии», остальные — пустую роль
|
||
(чтобы не повторять «Член комиссии» в каждой строке)."""
|
||
result = []
|
||
member_used = False
|
||
for m in members:
|
||
role = (m.role or 'член комиссии').strip()
|
||
role_lower = role.lower()
|
||
if ('председател' in role_lower or 'секретар' in role_lower
|
||
or 'заместител' in role_lower or 'зам. председателя' in role_lower):
|
||
result.append(role)
|
||
continue
|
||
# обычный член комиссии
|
||
if not member_used:
|
||
result.append('Члены комиссии')
|
||
member_used = True
|
||
else:
|
||
result.append('')
|
||
return result
|
||
|
||
|
||
def _build_commission_data(company: Company) -> Dict[str, str]:
|
||
data = {}
|
||
members = company.commission or []
|
||
|
||
display_roles = _commission_role_display(members)
|
||
lines = []
|
||
for i, (m, role_disp) in enumerate(zip(members, display_roles), 1):
|
||
if role_disp:
|
||
line = (f"{i}. {role_disp}: {m.position} — {m.fio}" if m.position
|
||
else f"{i}. {role_disp}: {m.fio}")
|
||
else:
|
||
line = (f"{i}. {m.position} — {m.fio}" if m.position
|
||
else f"{i}. {m.fio}")
|
||
lines.append(line)
|
||
data['commission_list'] = '\n'.join(lines) if lines else '—'
|
||
|
||
return data
|
||
|
||
|
||
def _add_is_indexed_vars(replacements: Dict[str, str], isys, idx: int):
|
||
fields = {
|
||
'is_name': isys.name or '—',
|
||
'is_address': getattr(isys, 'address', '') or '—',
|
||
'is_description': isys.description or '—',
|
||
'is_has_internet': 'да' if isys.is_internet else 'нет',
|
||
'is_category': isys.category or '—',
|
||
'is_users': ', '.join(isys.users_list or []) or '—',
|
||
'is_purpose': isys.purpose or '—',
|
||
'is_structure': getattr(isys, 'structure', '') or '—',
|
||
'is_software': getattr(isys, 'software', '') or '—',
|
||
'is_sphere': getattr(isys, 'sphere', '') or '—',
|
||
'is_object_type': getattr(isys, 'object_type', '') or '—',
|
||
'is_architecture': getattr(isys, 'architecture', '') or '—',
|
||
'is_typical_object': getattr(isys, 'typical_object', '') or '—',
|
||
'is_net_category': getattr(isys, 'net_category', '') or '—',
|
||
'is_net_operator': getattr(isys, 'net_operator', '') or '—',
|
||
'is_net_purpose': getattr(isys, 'net_purpose', '') or '—',
|
||
'is_net_method': getattr(isys, 'net_method', '') or '—',
|
||
'is_hw_sw': getattr(isys, 'hw_sw', '') or '—',
|
||
'is_os_sw': getattr(isys, 'os_sw', '') or '—',
|
||
'is_app_sw': getattr(isys, 'app_sw', '') or '—',
|
||
}
|
||
for f, v in fields.items():
|
||
replacements[f'{{{{{f}_{idx}}}}}'] = str(v)
|
||
|
||
|
||
def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
||
loops = {}
|
||
|
||
# Комиссия
|
||
members = company.commission or []
|
||
if members:
|
||
display_roles = _commission_role_display(members)
|
||
items = []
|
||
for m, role_disp in zip(members, display_roles):
|
||
pos = m.position or '—'
|
||
fio = m.fio or '—'
|
||
if role_disp:
|
||
full = f"{role_disp} {pos} — {fio}" if pos != '—' else f"{role_disp} — {fio}"
|
||
else:
|
||
full = f"{pos} — {fio}" if pos != '—' else fio
|
||
# Строка подписи: «___________ / » (только подчёркивание и слэш).
|
||
# Роль НЕ включаем — в шаблонах она выводится отдельной колонкой {{item.role}},
|
||
# иначе получается дубль «Председатель комиссии | Председатель комиссии: ___».
|
||
if 'Председатель' in role_disp:
|
||
sig = f"{'_' * 11} / "
|
||
sig_full = f"{role_disp}: {'_' * 11} / "
|
||
elif role_disp:
|
||
sig = f"{'_' * 17} / "
|
||
sig_full = f"{role_disp}: {'_' * 17} / "
|
||
else:
|
||
sig = f"{'_' * 17} / "
|
||
sig_full = f"{'_' * 17} / "
|
||
initials = dc.get_initials(fio) if fio != '—' else ''
|
||
items.append({
|
||
'role': role_disp,
|
||
'position': pos,
|
||
'fio': fio,
|
||
'fio_initials': dc.get_initials(fio) if fio != '—' else '',
|
||
'fio_short': dc.get_short_fio(fio) if fio != '—' else '',
|
||
'full': full,
|
||
# Рамка подписи без роли и ФИО: «___________ / » (ФИО добавится)
|
||
'signature': sig,
|
||
# Полная подпись с ролью и ФИО: «Роль: ___________ / И.О. Фамилия»
|
||
'signature_full': (sig_full + initials).strip(),
|
||
})
|
||
loops['commission'] = items
|
||
|
||
# ИС
|
||
if is_list:
|
||
items = []
|
||
for isys in is_list:
|
||
# Пользователи ИС для вложенного цикла <!-- loop:users_loop -->
|
||
# ВСЕ пользователи, включая администратора (без дублей по ФИО)
|
||
seen = set()
|
||
users_loop = []
|
||
for u in (isys.users_list or []):
|
||
key = _norm_name(u)
|
||
if key and key in seen:
|
||
continue
|
||
seen.add(key)
|
||
users_loop.append({'fio': u, 'position': ''})
|
||
items.append({
|
||
'name': isys.name or '—',
|
||
'description': isys.description or '—',
|
||
'address': getattr(isys, 'address', '') or '—',
|
||
'is_has_lan': 'Да' if isys.is_local_network else 'Нет',
|
||
'has_internet': 'да' if isys.is_internet else 'нет',
|
||
'category': isys.category or '—',
|
||
'users': ', '.join(isys.users_list or []) or '—',
|
||
'users_loop': users_loop,
|
||
'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—',
|
||
'is_purpose': isys.purpose or '—',
|
||
'structure': getattr(isys, 'structure', '') or '—',
|
||
'software': getattr(isys, 'software', '') or '—',
|
||
# 236 приказ
|
||
'purpose': isys.purpose or '—',
|
||
'sphere': getattr(isys, 'sphere', '') or '—',
|
||
'object_type': getattr(isys, 'object_type', '') or '—',
|
||
'architecture': getattr(isys, 'architecture', '') or '—',
|
||
'typical_object': getattr(isys, 'typical_object', '') or '—',
|
||
'subject_name': getattr(isys, 'subject_name', '') or '—',
|
||
'subject_address': getattr(isys, 'subject_address', '') or '—',
|
||
'subject_element': getattr(isys, 'subject_element', '') or '—',
|
||
'subject_inn': getattr(isys, 'subject_inn', '') or '—',
|
||
'subject_kpp': getattr(isys, 'subject_kpp', '') or '—',
|
||
'net_category': getattr(isys, 'net_category', '') or '—',
|
||
'net_operator': getattr(isys, 'net_operator', '') or '—',
|
||
'net_purpose': getattr(isys, 'net_purpose', '') or '—',
|
||
'net_method': getattr(isys, 'net_method', '') or '—',
|
||
'hw_sw': getattr(isys, 'hw_sw', '') or '—',
|
||
'os_sw': getattr(isys, 'os_sw', '') or '—',
|
||
'app_sw': getattr(isys, 'app_sw', '') or '—',
|
||
'balance_value': getattr(isys, 'balance_value', '') or '—',
|
||
'attacker_category': getattr(isys, 'attacker_category', '') or '—',
|
||
'attacker_level': getattr(isys, 'attacker_level', '') or '—',
|
||
'attacker_types': getattr(isys, 'attacker_types', '') or '—',
|
||
'threats': getattr(isys, 'threats', '') or '—',
|
||
# Алиасы под шаблон «Сведения о результатах категорирования» (ФСТЭК)
|
||
'objective': getattr(isys, 'purpose', '') or '—',
|
||
'standard_name': getattr(isys, 'typical_object', '') or '—',
|
||
'network_category': getattr(isys, 'net_category', '') or '—',
|
||
'provider': getattr(isys, 'net_operator', '') or '—',
|
||
'interaction_purpose': getattr(isys, 'net_purpose', '') or '—',
|
||
'interaction_method': getattr(isys, 'net_method', '') or '—',
|
||
'hardware': getattr(isys, 'hw_sw', '') or '—',
|
||
'system_software': getattr(isys, 'os_sw', '') or '—',
|
||
'defense_tools': ', '.join(isys.defense_tools_list or []) or '—',
|
||
'component': getattr(isys, 'subject_element', '') or '—',
|
||
'exploiting_org': getattr(isys, 'subject_name', '') or '—',
|
||
'exploiting_inn': getattr(isys, 'subject_inn', '') or '—',
|
||
'domain': getattr(isys, 'domain_name', '') or '—',
|
||
'threat_actor': getattr(isys, 'attacker_category', '') or '—',
|
||
'threat_justification': getattr(isys, 'threats', '') or '—',
|
||
'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 '')
|
||
or _build_org_measures_text(company, isys),
|
||
'tech_measures': getattr(isys, 'tech_measures', '') or '—',
|
||
})
|
||
loops['information_systems'] = items
|
||
|
||
# Сотрудники с доступом
|
||
employees = getattr(company, 'employees_access', None) or []
|
||
if employees:
|
||
items = []
|
||
for e in employees:
|
||
items.append({
|
||
'position': e.get('position', ''),
|
||
'fio': e.get('fio', ''),
|
||
})
|
||
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
|
||
|
||
|
||
# ============================================================
|
||
# РЕЗОЛВЕР ЗНАЧЕНИЙ
|
||
# ============================================================
|
||
|
||
def _find_fstek_office(address_text: str):
|
||
"""Определяет управление ФСТЭК России по адресу организации.
|
||
|
||
Ищет город/регион из адреса в словаре fstek_offices.json.
|
||
Возвращает dict {'name': ..., 'address': ...} или None.
|
||
"""
|
||
import json as _json
|
||
import os as _os
|
||
if not address_text:
|
||
return None
|
||
dict_path = _os.path.join(_os.path.dirname(__file__), 'dictionaries', 'fstek_offices.json')
|
||
try:
|
||
with open(dict_path, encoding='utf-8') as _f:
|
||
offices = _json.load(_f)
|
||
except Exception:
|
||
return None
|
||
hay = address_text.lower()
|
||
# Нормализация сокращений: обл.→область, респ.→республика, г.о.→городской округ
|
||
hay = hay.replace('обл.', 'область').replace('обл,', 'область')
|
||
hay = hay.replace('респ.', 'республика').replace('респ,', 'республика')
|
||
hay = hay.replace('г.о.', 'городской округ').replace('г.о,', 'городской округ')
|
||
hay = hay.replace('ао', 'автономный округ')
|
||
best = None
|
||
best_len = 0
|
||
for off in offices:
|
||
# Ищем по городам
|
||
for city in off.get('cities', []):
|
||
if city in hay and len(city) > best_len:
|
||
best = off
|
||
best_len = len(city)
|
||
# Ищем по регионам (область/край/республика)
|
||
for region in off.get('regions', []):
|
||
if region in hay and len(region) > best_len:
|
||
best = off
|
||
best_len = len(region)
|
||
return best
|
||
|
||
|
||
def _resolve(source: str, ctx: Dict) -> str:
|
||
"""Преобразует ключ-источник в значение, подставляя из контекста."""
|
||
company = ctx.get('company')
|
||
is_list = ctx.get('is_list', [])
|
||
commission_data = ctx.get('commission_data', {})
|
||
doc_number = ctx.get('doc_number', '')
|
||
doc_date = ctx.get('doc_date', '')
|
||
|
||
# Прямые поля Company
|
||
if source.startswith('company:'):
|
||
field = source[8:]
|
||
return str(getattr(company, field, '') or '')
|
||
|
||
# Склонение названия
|
||
if source.startswith('decl:'):
|
||
_, field, case = source.split(':')
|
||
name = getattr(company, field, '') or ''
|
||
case_gr = dc._normalize_case(case)
|
||
# Сначала берём ГОТОВОЕ склонение из модели (пользователь мог
|
||
# подставить через Морфер/ИИ или ввести вручную), пересчитываем
|
||
# только если поле пустое
|
||
case_idx = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}.get(case_gr)
|
||
if case_idx:
|
||
if field == 'full_name':
|
||
ready = getattr(company, f'company_name_{case_idx}', '') or ''
|
||
if ready:
|
||
return ready
|
||
elif field == 'short_name':
|
||
ready = getattr(company, f'short_name_{case_idx}', '') or ''
|
||
if ready:
|
||
return ready
|
||
return dc.company_name_decline(name, case_gr) if name else '—'
|
||
|
||
# Форматы ФИО
|
||
if source.startswith('short_fio:'):
|
||
who = source.split(':', 1)[1]
|
||
fio = _get_fio(company, who)
|
||
return dc.get_short_fio(fio) if fio else '—'
|
||
if source.startswith('initials:'):
|
||
who = source.split(':', 1)[1]
|
||
fio = _get_fio(company, who)
|
||
return dc.get_initials(fio) if fio else '—'
|
||
|
||
# Комиссия
|
||
if source in commission_data:
|
||
return str(commission_data[source])
|
||
|
||
# Управление ФСТЭК (адресат отправки сведений о категорировании)
|
||
if source in ('fstek_recipient_address', 'fstek_district', 'fstek_office_name'):
|
||
addr = getattr(company, 'address', '') or ''
|
||
off = _find_fstek_office(addr)
|
||
if off:
|
||
if source == 'fstek_recipient_address':
|
||
return off.get('address', '') or ''
|
||
return off.get('name', '') or ''
|
||
return ''
|
||
|
||
# Даты
|
||
if source.startswith('date:'):
|
||
fmt = source.split(':', 1)[1]
|
||
now = datetime.now()
|
||
if fmt == 'year':
|
||
return str(now.year)
|
||
return now.strftime('%d.%m.%Y')
|
||
if source == 'doc_date':
|
||
return doc_date or datetime.now().strftime('%d.%m.%Y')
|
||
if source == 'doc_number':
|
||
num = str(doc_number).strip()
|
||
return re.sub(r'^[Nn]?[oо]?[№#]\s*', '', num) or '—'
|
||
|
||
# Требования защищённости
|
||
if source == 'security_reqs':
|
||
return _get_security_text(ctx)
|
||
|
||
# IS-алиасы (первая ИС)
|
||
if source.startswith('is:'):
|
||
field = source[3:]
|
||
if is_list:
|
||
first = is_list[0]
|
||
field_map = {
|
||
'name': 'name',
|
||
'description': 'description',
|
||
'category': 'category',
|
||
'users': lambda: ', '.join(first.users_list or []),
|
||
}
|
||
if field in field_map:
|
||
val = field_map[field]
|
||
if callable(val):
|
||
return val() or '—'
|
||
return str(getattr(first, val, '') or '—') if isinstance(val, str) else '—'
|
||
return '—'
|
||
|
||
# Список ИС
|
||
if source == 'is_list':
|
||
if is_list:
|
||
return '\n'.join(f"{i}. {isys.name}" for i, isys in enumerate(is_list, 1))
|
||
return '—'
|
||
if source == 'is_list_comma':
|
||
if is_list:
|
||
return ', '.join(isys.name for isys in is_list)
|
||
return '—'
|
||
|
||
# Члены комиссии
|
||
if source.startswith('commission_member:'):
|
||
parts = source.split(':')
|
||
idx, field = int(parts[1]), parts[2]
|
||
members = getattr(company, 'commission', None) or []
|
||
if idx < len(members):
|
||
return str(getattr(members[idx], field, '') or '—')
|
||
return '—'
|
||
|
||
# Склонение ФИО по падежам
|
||
if source.startswith('fio_acl:'):
|
||
who = source.split(':', 1)[1]
|
||
fio = _get_fio(company, who)
|
||
return dc.decline(fio, 'accs') if fio else '—'
|
||
|
||
return '—'
|
||
|
||
|
||
def _get_fio(company: Company, who: str) -> str:
|
||
mapping = {
|
||
'chief': 'chief_fio',
|
||
'responsible': 'responsible_fio',
|
||
'admin': 'admin_fio',
|
||
}
|
||
field = mapping.get(who, who + '_fio')
|
||
return getattr(company, field, '') or ''
|
||
|
||
|
||
def _get_security_text(ctx: Dict) -> str:
|
||
"""Возвращает текст требований к защите объекта КИИ по категории значимости."""
|
||
is_list = ctx.get('is_list', [])
|
||
category = ''
|
||
if is_list:
|
||
category = str(getattr(is_list[0], 'category', '') or '').strip()
|
||
|
||
TEXTS = {
|
||
'1': """Для значимого объекта КИИ 1-й категории значимости необходимо:
|
||
— создание системы безопасности значимого объекта КИИ;
|
||
— обеспечение защиты информации от компьютерных атак;
|
||
— категорирование объектов КИИ и контроль его результатов.""",
|
||
'2': """Для значимого объекта КИИ 2-й категории значимости необходимо:
|
||
— создание системы безопасности значимого объекта КИИ;
|
||
— обеспечение защиты информации от компьютерных атак.""",
|
||
'3': """Для значимого объекта КИИ 3-й категории значимости необходимо:
|
||
— обеспечение защиты информации от компьютерных атак.""",
|
||
}
|
||
if category in TEXTS:
|
||
return TEXTS[category]
|
||
return """Для объекта КИИ, не являющегося значимым, необходимо:
|
||
— принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ."""
|
||
|