diff --git a/dokogen/variables.py b/dokogen/variables.py index 4dd640f..8d4f174 100644 --- a/dokogen/variables.py +++ b/dokogen/variables.py @@ -373,9 +373,204 @@ 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 лет + 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 + + # ============================================================ # РЕЗОЛВЕР ЗНАЧЕНИЙ # ============================================================