256 lines
14 KiB
Python
256 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""DokoGen — Модели данных (только 187-ФЗ, только нужные поля)"""
|
|
|
|
from dataclasses import dataclass, field, asdict
|
|
from typing import List, Dict, Any
|
|
|
|
|
|
def _clean_single_fio(value):
|
|
"""Оставить одно первое ФИО, если в строке их несколько."""
|
|
if not value:
|
|
return value
|
|
value = str(value).strip()
|
|
for sep in (',', ';', '\n', '\r'):
|
|
if sep in value:
|
|
value = value.split(sep)[0].strip()
|
|
break
|
|
words = [w for w in value.split() if w]
|
|
if len(words) > 3:
|
|
value = ' '.join(words[:3])
|
|
return value.strip()
|
|
|
|
|
|
@dataclass
|
|
class CommissionMember:
|
|
"""Член комиссии по категорированию объектов КИИ"""
|
|
role: str = "член комиссии"
|
|
position: str = ""
|
|
fio: str = ""
|
|
|
|
|
|
@dataclass
|
|
class InformationSystem:
|
|
"""Объект КИИ (вместо информационной системы 152-ФЗ)"""
|
|
name: str = ""
|
|
description: str = ""
|
|
is_local_network: bool = False
|
|
is_internet: bool = False
|
|
category: str = "" # Категория значимости объекта КИИ: 1 / 2 / 3 / без категории
|
|
users_list: List[str] = field(default_factory=list) # Пользователи ОКИИ
|
|
defense_tools_list: List[str] = field(default_factory=list) # Средства защиты
|
|
purpose: str = "" # Назначение объекта (п. 236)
|
|
structure: str = "" # Структура объекта КИИ
|
|
software: str = "" # Программное обеспечение
|
|
|
|
# ---- 236 приказ ФСТЭК: сведения об объекте ----
|
|
sphere: str = "" # Сфера (область) деятельности
|
|
object_type: str = "" # Тип объекта (ИС, АСУ, ИТКС)
|
|
architecture: str = "" # Архитектура объекта
|
|
typical_object: str = "" # Наименование типового отраслевого объекта КИИ
|
|
|
|
# Субъект (организация, эксплуатирующая объект — как в опроснике)
|
|
subject_name: str = "" # Наименование организации, эксплуатирующей объект
|
|
subject_address: str = "" # Адрес местонахождения
|
|
subject_element: str = "" # Элемент (компонент) объекта, который эксплуатируется лицом
|
|
subject_inn: str = "" # ИНН лица, эксплуатирующего объект
|
|
subject_kpp: str = "" # КПП его обособленных подразделений
|
|
|
|
# Сети электросвязи
|
|
net_category: str = "" # Категория сети электросвязи
|
|
net_operator: str = "" # Оператор связи и (или) провайдер хостинга
|
|
net_purpose: str = "" # Цель взаимодействия с сетью электросвязи
|
|
net_method: str = "" # Способ взаимодействия с указанием типа доступа
|
|
|
|
# Программно-аппаратные средства
|
|
hw_sw: str = "" # Наименования программно-аппаратных средств и их количество
|
|
os_sw: str = "" # Общесистемное ПО (ОС, средства виртуализации)
|
|
app_sw: str = "" # Прикладные программы
|
|
|
|
# Балансовая стоимость объекта КИИ (опросник)
|
|
balance_value: str = "" # Балансовая стоимость объекта КИИ в рублях
|
|
|
|
# Безопасность (236 приказ)
|
|
attacker_category: str = "" # Категория нарушителя (внешний/внутренний)
|
|
attacker_level: str = "" # Уровень возможностей нарушителя (Н1-Н4, Методика ФСТЭК 05.02.2021)
|
|
attacker_types: str = "" # Виды нарушителей (\n — разделитель, п. 5.1.3 Методики)
|
|
threats: str = "" # Основные угрозы безопасности информации (\n — разделитель)
|
|
incidents: str = "" # Типы компьютерных инцидентов (\n — разделитель)
|
|
criteria_values: str = "" # Показатели критериев значимости (\n — разделитель)
|
|
criteria_justification: str = "" # Обоснование полученных значений
|
|
org_measures: str = "" # Организационные меры (\n — разделитель)
|
|
tech_measures: str = "" # Технические меры (\n — разделитель)
|
|
|
|
|
|
@dataclass
|
|
class Company:
|
|
"""Организация — основной объект (187-ФЗ)"""
|
|
# Полное название
|
|
full_name: str = ""
|
|
company_name_1: str = "" # именительный
|
|
company_name_2: str = "" # родительный
|
|
company_name_3: str = "" # дательный
|
|
company_name_4: str = "" # винительный
|
|
company_name_5: str = "" # творительный
|
|
company_name_6: str = "" # предложный
|
|
|
|
# Краткое название
|
|
short_name: str = ""
|
|
short_name_1: str = ""
|
|
short_name_2: str = ""
|
|
short_name_3: str = ""
|
|
short_name_4: str = ""
|
|
short_name_5: str = ""
|
|
short_name_6: str = ""
|
|
|
|
# Адрес и реквизиты
|
|
address: str = ""
|
|
city_name: str = ""
|
|
inn: str = ""
|
|
ogrn: str = ""
|
|
ogrn_date: str = ""
|
|
kpp: str = ""
|
|
okved: str = "" # Основной ОКВЭД
|
|
okved_extra: List[str] = field(default_factory=list) # Дополнительные ОКВЭД
|
|
domain_name: str = "" # Доменное имя и внешний сетевой адрес
|
|
|
|
# Должностные лица
|
|
chief_position: str = ""
|
|
chief_fio: str = ""
|
|
responsible_position: str = ""
|
|
responsible_fio: str = ""
|
|
admin_position: str = ""
|
|
admin_fio: str = ""
|
|
|
|
# Документ
|
|
contract_number: str = ""
|
|
contract_date: str = ""
|
|
|
|
# Финансовые показатели организации (опросник)
|
|
profit: str = "" # Сумма прибыли за предыдущий год
|
|
profit_tax: str = "" # Сумма налога с прибыли за предыдущий год
|
|
|
|
# Показатель 8 (ущерб субъекту КИИ): доходы за 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 = "" # Снижение уровня дохода (суммарный ущерб) от нарушения
|
|
# критических процессов (руб.) — ΔДоходов для п. 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)
|
|
employees_access: List[Dict[str, str]] = field(default_factory=list)
|
|
|
|
# Наименование структурного подразделения по безопасности
|
|
security_department: str = ""
|
|
|
|
# Ответы раздела «Физическая безопасность» опросника (да/нет):
|
|
# охрана, пожарная сигнализация, тревожная кнопка, решётки, металлические двери,
|
|
# видеонаблюдение, видеодомофон, турникет, журнал посетителей.
|
|
physical_security: Dict[str, str] = field(default_factory=dict)
|
|
|
|
def update_declensions(self, inflect_func):
|
|
"""Обновить склонения полного названия"""
|
|
if self.full_name:
|
|
inflected = inflect_func(self.full_name)
|
|
self.company_name_1 = self.full_name
|
|
self.company_name_2 = inflected.get("gent", "")
|
|
self.company_name_3 = inflected.get("datv", "")
|
|
self.company_name_4 = inflected.get("accs", "")
|
|
self.company_name_5 = inflected.get("ablt", "")
|
|
self.company_name_6 = inflected.get("loct", "")
|
|
|
|
def update_short_declensions(self, inflect_func):
|
|
"""Обновить склонения сокращённого названия"""
|
|
if self.short_name:
|
|
inflected = inflect_func(self.short_name)
|
|
self.short_name_1 = self.short_name
|
|
self.short_name_2 = inflected.get("gent", "")
|
|
self.short_name_3 = inflected.get("datv", "")
|
|
self.short_name_4 = inflected.get("accs", "")
|
|
self.short_name_5 = inflected.get("ablt", "")
|
|
self.short_name_6 = inflected.get("loct", "")
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "Company":
|
|
d = dict(data)
|
|
if "commission" in d and isinstance(d["commission"], list):
|
|
_cvalid = {f.name for f in CommissionMember.__dataclass_fields__.values()}
|
|
d["commission"] = [
|
|
CommissionMember(**{k: v for k, v in c.items() if k in _cvalid})
|
|
if isinstance(c, dict) else c
|
|
for c in d["commission"]
|
|
]
|
|
if "information_systems" in d and isinstance(d["information_systems"], list):
|
|
_ivalid = {f.name for f in InformationSystem.__dataclass_fields__.values()}
|
|
d["information_systems"] = [
|
|
InformationSystem(**{k: v for k, v in isys.items() if k in _ivalid})
|
|
if isinstance(isys, dict) else isys
|
|
for isys in d["information_systems"]
|
|
]
|
|
if "employees_access" in d and not isinstance(d["employees_access"], list):
|
|
d["employees_access"] = []
|
|
# Отбрасываем плейсхолдеры из старых опросников («Формат: Фамилия Имя Отчество») и пустые строки
|
|
if "employees_access" in d and isinstance(d["employees_access"], list):
|
|
clean = []
|
|
for e in d["employees_access"]:
|
|
if not isinstance(e, dict):
|
|
continue
|
|
fio = str(e.get("fio", "")).strip()
|
|
pos = str(e.get("position", "")).strip()
|
|
if fio.lower().startswith("формат:") or fio.lower().startswith("пример:"):
|
|
continue
|
|
if not fio and not pos:
|
|
continue
|
|
clean.append(e)
|
|
d["employees_access"] = clean
|
|
if "okved_extra" in d and not isinstance(d.get("okved_extra"), list):
|
|
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, '')
|
|
if val:
|
|
setattr(obj, fio_field, _clean_single_fio(val))
|
|
return obj
|
|
|
|
def get_folder_name(self) -> str:
|
|
if self.short_name:
|
|
return self.short_name
|
|
if self.full_name:
|
|
return self.full_name[:30]
|
|
return "Новая_организация"
|