# -*- 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_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 = { '': '{{company_full_name}}', '': '{{company_short_name}}', '': '{{company_full_name_genitive}}', '': '{{company_short_name_genitive}}', '': '{{company_full_name_dative}}', '': '{{company_short_name_dative}}', '': '{{company_full_name_accs}}', '': '{{company_short_name_accs}}', '': '{{company_full_name_ablt}}', '': '{{company_short_name_ablt}}', '': '{{company_full_name_loct}}', '': '{{company_short_name_loct}}', '
': '{{company_address}}', '': '{{company_inn}}', '': '{{company_ogrn}}', '': '{{company_kpp}}', '': '{{chief_fio_initials}}', '': '{{chief_fio_short}}', '': '{{chief_position}}', '': '{{responsible_fio_initials}}', '': '{{responsible_fio_short}}', '': '{{responsible_position}}', '': '{{commission_list}}', '': '{{document_number}}', '': '{{document_date}}', '': '{{date}}', '': '{{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: # Пользователи ИС для вложенного цикла # ВСЕ пользователи, включая администратора (без дублей по ФИО) 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 '—', '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 лет 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': 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} руб. В случае выхода из строя объекта КИИ " f"потребуется замена, установка и настройка, стоимость которых составит около {d:,.2f} руб. " 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 _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 = [] 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 not applicable: value = 'Показатель не применим.' justification = (just.get(num) or just.get(grp_num) or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».") elif grp_num in ('8', '9'): text, cat = _calc_criterion_8_9(company, isys, grp_num) if text: value = text if cat: category = f"{cat}-я категория" else: category = '—' else: # Применимый показатель без авторасчёта — оператор заполняет значение value = '—' justification = (just.get(num) or just.get(grp_num) or 'Показатель применим к объекту КИИ; значение определяется по данным объекта.') 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 """Для объекта КИИ, не являющегося значимым, необходимо: — принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ."""