Files
dokogen187/dokogen/models.py
T
LocoAgent 5af23ae525 fix: аудит dokogen187 — критические и высокие замечания
- api_settings: убраны реальные API-ключи из кода (env/файл) — 1.1
- verify: промт ИИ для проверки реквизитов больше не ссылается на 187-ФЗ
  как на закон о ПДн («юрист по ПДн (187-ФЗ)» → специалист по реквизитам)
- ui: дата ПП РФ №127 исправлена 08.02.2019 → 08.02.2018 (3 места)
- help_docs: группа B переименована «187-ФЗ (персональные данные)» →
  «187-ФЗ (критическая информационная инфраструктура)»
- generator: убран дубль хвоста параграфа в _highlight_unmatched
- importer: «магия дат» (числа 10001-79999 → дата Excel) теперь только
  для полей-дат (as_date), финансовые поля не ломаются
- ui (_collect_is): убраны дубли пар маппинга attacker_level/attacker_types
- ui (импорт): убрано наследие 152-ФЗ ispdnPosition/ispdnFio из маппинга
- test_generator: переписан под модель 187-ФЗ (category/sphere/object_type
  вместо pd_count/threat_type/defence_level)
- criteria_justifications: п.9 для здравоохранения согласован с кодом
  (убран «критерий неприменим» — противоречие с «применим ко всем»)
- criteria_applicability: Транспорт (добавлены п.1, п.2; убран п.10),
  Банковская сфера (добавлены п.8, п.9) — требуется экспертная сверка
- ui: Бусредн вынесен в константу DEFAULT_BUDGET_AVG (данные Минфина,
  обновлять ежегодно); автозаполнение п.9 унифицировано с авторасчётом
  (ΔБ = ΔНП+ΔНДПИ+ΔДИВ+ΔВСО+ΔНС)
- ui: п.8 — предупреждение при неполных данных (менее 5 лет доходов)
- models: поля ndpi/div/vso/ns для п.9; комментарий income_loss уточнён
- .gitignore: api_settings.json/.env не попадают в git
2026-08-05 09:16:16 +04:00

230 lines
12 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 лет и суммарный ущерб
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 = "" # Снижение сборов ΔНС (руб.)
# Списки
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 = ""
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})
# Если в поле ФИО несколько человек — оставить одного (старые опросники)
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 "Новая_организация"