Compare commits
6
Commits
868e0065f3
...
614044a6ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614044a6ca | ||
|
|
9a1058559a | ||
|
|
a8fa3f908c | ||
|
|
033c21fccc | ||
|
|
bf75d67f5e | ||
|
|
5af23ae525 |
@@ -3,3 +3,8 @@ __pycache__/
|
||||
.autosave/
|
||||
Сгенерированные_документы/
|
||||
*.result.docx
|
||||
|
||||
# Секреты и локальные настройки — НЕ коммитить
|
||||
api_settings.json
|
||||
.env
|
||||
.env.local
|
||||
|
||||
+10
-4
@@ -13,11 +13,17 @@ import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# ВНИМАНИЕ: реальные ключи НЕ хранятся в коде (утечка в git-историю — см. аудит).
|
||||
# Источники ключей (по приоритету):
|
||||
# 1) переменные окружения DADATA_TOKEN / DEEPSEEK_API_KEY / DEEPSEEK_MODEL / DEEPSEEK_BASE_URL
|
||||
# 2) файл api_settings.json рядом с программой
|
||||
# 3) DEFAULT_SETTINGS ниже (пустые значения)
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
'dadata_token': '431aa11706bf83f571cef0319259289aa750bcb1',
|
||||
'ai_api_key': 'sk-3c7e6673f245412697d0e14b6096435f',
|
||||
'ai_model': 'deepseek-chat',
|
||||
'ai_base_url': 'https://api.deepseek.com',
|
||||
'dadata_token': os.environ.get('DADATA_TOKEN', ''),
|
||||
'ai_api_key': os.environ.get('DEEPSEEK_API_KEY', ''),
|
||||
'ai_model': os.environ.get('DEEPSEEK_MODEL', 'deepseek-chat'),
|
||||
'ai_base_url': os.environ.get('DEEPSEEK_BASE_URL', 'https://api.deepseek.com'),
|
||||
}
|
||||
|
||||
# Старые ключи (для совместимости со старым файлом api_settings.json)
|
||||
|
||||
Binary file not shown.
@@ -23,9 +23,10 @@
|
||||
"9"
|
||||
],
|
||||
"Транспорт": [
|
||||
"10",
|
||||
"11",
|
||||
"3"
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"11"
|
||||
],
|
||||
"Связь": [
|
||||
"4"
|
||||
@@ -61,6 +62,8 @@
|
||||
"14"
|
||||
],
|
||||
"Банковская сфера и финансовый рынок": [
|
||||
"8",
|
||||
"9",
|
||||
"10"
|
||||
],
|
||||
"Топливно-энергетический комплекс": [
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"Здравоохранение": {
|
||||
"1": "Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания медицинской помощи, проведения медицинских экспертиз, медицинских осмотров и медицинских освидетельствований, санитарно-противоэпидемических (профилактических) мероприятий и не осуществляет управление, контроль или мониторинг данных бизнес-процессов. Объект является вспомогательной информационной системой, целью которой является автоматизация процессов ведения бухгалтерского учета. Объект не осуществляет мониторинг, контроль или управление другими объектами КИИ, обеспечивающими жизненно важные функции организма. Утрата или иное повреждение объекта КИИ не может повлиять на жизненно важные функции организма.",
|
||||
"2": "Объект КИИ не относится к объектам обеспечения жизнедеятельности населения, в том числе объектам водоснабжения и канализации, очистки сточных вод, тепло- и электроснабжения, гидротехнических сооружений.",
|
||||
"3": "Объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры, и субъект КИИ не имеет в своем составе подразделения транспортной инфраструктуры.",
|
||||
"4": "Субъект КИИ не является оператором связи, объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры.",
|
||||
"5": "Организация сферы здравоохранения является юридическим лицом, обеспечивающим эксплуатацию информационной системы, задействованной в процессе оказания государственной услуги, однако нарушение функционирования информационной системы (рассматриваемого объекта КИИ), не участвующей в обработке информации, необходимой для оказания государственной услуги, не влечет за собой нарушение и (или) прекращение доступа к государственной услуге. Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания государственной услуги.",
|
||||
"6": "Организация сферы здравоохранения не является органом государственной власти, в связи с этим критерий неприменим.",
|
||||
"7": "Организация сферы здравоохранения не является субъектом политической сферы деятельности, в связи с этим критерий неприменим.",
|
||||
"8": "Организация сферы здравоохранения не является государственной корпорацией, государственным унитарным предприятием, государственной компанией, стратегическим акционерным обществом, стратегическим предприятием, в связи с этим критерий неприменим.",
|
||||
"9": "Объект КИИ задействован только в предоставлении медицинских услуг, освобожденных от налогообложения, в связи с этим критерий неприменим.",
|
||||
"10": "Организация сферы здравоохранения не является субъектом банковской сферы деятельности.",
|
||||
"10.1": "Организация сферы здравоохранения не является центральным контрагентом.",
|
||||
"10.2": "Организация сферы здравоохранения не является центральным депозитарием.",
|
||||
"10.3": "Организация сферы здравоохранения не является негосударственным пенсионным фондом.",
|
||||
"10.4": "Организация сферы здравоохранения не является страховой организацией.",
|
||||
"10.5": "Организация сферы здравоохранения не является оператором услуг информационного обмена.",
|
||||
"11": "Объекты КИИ организации сферы здравоохранения не относятся к опасным производственным объектам в соответствии с законодательством Российской Федерации.",
|
||||
"12": "Организация сферы здравоохранения не выполняет функции пункта управления (ситуационного центра) государственного органа власти или государственной корпорации.",
|
||||
"13": "Организация сферы здравоохранения не является субъектом обеспечения обороны страны, безопасности государства и правопорядка.",
|
||||
"14": "Организация сферы здравоохранения не имеет информационных систем, функционирующих в области обеспечения обороны страны, безопасности государства и правопорядка."
|
||||
}
|
||||
"Здравоохранение": {
|
||||
"1": "Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания медицинской помощи, проведения медицинских экспертиз, медицинских осмотров и медицинских освидетельствований, санитарно-противоэпидемических (профилактических) мероприятий и не осуществляет управление, контроль или мониторинг данных бизнес-процессов. Объект является вспомогательной информационной системой, целью которой является автоматизация процессов ведения бухгалтерского учета. Объект не осуществляет мониторинг, контроль или управление другими объектами КИИ, обеспечивающими жизненно важные функции организма. Утрата или иное повреждение объекта КИИ не может повлиять на жизненно важные функции организма.",
|
||||
"2": "Объект КИИ не относится к объектам обеспечения жизнедеятельности населения, в том числе объектам водоснабжения и канализации, очистки сточных вод, тепло- и электроснабжения, гидротехнических сооружений.",
|
||||
"3": "Объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры, и субъект КИИ не имеет в своем составе подразделения транспортной инфраструктуры.",
|
||||
"4": "Субъект КИИ не является оператором связи, объекты КИИ организации сферы здравоохранения не относятся к объектам транспортной инфраструктуры.",
|
||||
"5": "Организация сферы здравоохранения является юридическим лицом, обеспечивающим эксплуатацию информационной системы, задействованной в процессе оказания государственной услуги, однако нарушение функционирования информационной системы (рассматриваемого объекта КИИ), не участвующей в обработке информации, необходимой для оказания государственной услуги, не влечет за собой нарушение и (или) прекращение доступа к государственной услуге. Рассматриваемый объект КИИ не участвует в обработке информации, необходимой для оказания государственной услуги.",
|
||||
"6": "Организация сферы здравоохранения не является органом государственной власти, в связи с этим критерий неприменим.",
|
||||
"7": "Организация сферы здравоохранения не является субъектом политической сферы деятельности, в связи с этим критерий неприменим.",
|
||||
"8": "Организация сферы здравоохранения не является государственной корпорацией, государственным унитарным предприятием, государственной компанией, стратегическим акционерным обществом, стратегическим предприятием, в связи с этим критерий неприменим.",
|
||||
"9": "Объект КИИ задействован в предоставлении медицинских услуг. Показатель 9 применим ко всем субъектам КИИ; при расчёте учитывается, что медицинские услуги освобождены от налогообложения (значение показателя может быть минимальным).",
|
||||
"10": "Организация сферы здравоохранения не является субъектом банковской сферы деятельности.",
|
||||
"10.1": "Организация сферы здравоохранения не является центральным контрагентом.",
|
||||
"10.2": "Организация сферы здравоохранения не является центральным депозитарием.",
|
||||
"10.3": "Организация сферы здравоохранения не является негосударственным пенсионным фондом.",
|
||||
"10.4": "Организация сферы здравоохранения не является страховой организацией.",
|
||||
"10.5": "Организация сферы здравоохранения не является оператором услуг информационного обмена.",
|
||||
"11": "Объекты КИИ организации сферы здравоохранения не относятся к опасным производственным объектам в соответствии с законодательством Российской Федерации.",
|
||||
"12": "Организация сферы здравоохранения не выполняет функции пункта управления (ситуационного центра) государственного органа власти или государственной корпорации.",
|
||||
"13": "Организация сферы здравоохранения не является субъектом обеспечения обороны страны, безопасности государства и правопорядка.",
|
||||
"14": "Организация сферы здравоохранения не имеет информационных систем, функционирующих в области обеспечения обороны страны, безопасности государства и правопорядка."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -692,9 +692,6 @@ def _highlight_unmatched(doc):
|
||||
pos = m.end()
|
||||
if pos < len(full):
|
||||
_add_run_with_tabs(para, full[pos:], first)
|
||||
run = para.add_run(full[pos:])
|
||||
if first is not None:
|
||||
_copy_run_style(first, run)
|
||||
|
||||
for para in doc.paragraphs:
|
||||
_process_para(para)
|
||||
|
||||
@@ -83,7 +83,7 @@ def _generate_variables_reference(path):
|
||||
_p(doc, 'Список переменных для шаблонов Word. Переменная подставляется в текст '
|
||||
'как есть: {{имя_переменной}}. Циклы: <!-- loop:имя --> ... <!-- loop_end -->.')
|
||||
|
||||
groups = {'A': 'Общие переменные', 'B': '187-ФЗ (персональные данные)'}
|
||||
groups = {'A': 'Общие переменные', 'B': '187-ФЗ (критическая информационная инфраструктура)'}
|
||||
by_group = {'A': [], 'B': []}
|
||||
for var, group, desc, _ in VARIABLE_DEFS:
|
||||
by_group.setdefault(group, []).append((var, desc))
|
||||
|
||||
+12
-5
@@ -97,13 +97,18 @@ def normalize_address(text: str) -> str:
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def clean_value(val):
|
||||
"""Очистка и нормализация значения ячейки Excel."""
|
||||
def clean_value(val, as_date=False):
|
||||
"""Очистка и нормализация значения ячейки Excel.
|
||||
|
||||
as_date=True — конвертировать серийный номер Excel в дату (только для
|
||||
полей-дат, напр. ogrnDate). Для финансовых полей НЕ вызывается,
|
||||
иначе число 50 000 руб. превратится в «дату» (см. аудит importer.py:105-109).
|
||||
"""
|
||||
if val is None:
|
||||
return ''
|
||||
if isinstance(val, (int, float)):
|
||||
# Попытка распознать дату (серийный номер Excel)
|
||||
if 10000 < val < 80000:
|
||||
# Попытка распознать дату (серийный номер Excel) — только для полей-дат
|
||||
if as_date and 10000 < val < 80000:
|
||||
serial = int(val)
|
||||
if serial > 60:
|
||||
serial -= 1
|
||||
@@ -253,7 +258,7 @@ def _import_187_common(wb, data, settings, log_fn):
|
||||
if sn.lower() == sheet_part.lower():
|
||||
target_ws = wb[sn]
|
||||
break
|
||||
val = clean_value(cell_value(target_ws, coord))
|
||||
val = clean_value(cell_value(target_ws, coord), as_date=('дата' in name.lower() or 'date' in key.lower()))
|
||||
if val:
|
||||
if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
||||
val = _extract_single_fio(val)
|
||||
@@ -273,6 +278,8 @@ def _import_187_common(wb, data, settings, log_fn):
|
||||
if val:
|
||||
if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'):
|
||||
val = _extract_single_fio(val)
|
||||
elif 'дата' in name.lower() or 'date' in key.lower():
|
||||
val = clean_value(val, as_date=True)
|
||||
data[key] = val
|
||||
log_fn(f" {name} [по ключу]: {val}")
|
||||
break
|
||||
|
||||
+7
-1
@@ -135,9 +135,15 @@ class Company:
|
||||
income_year_3: str = "" # Доход за 3 года назад (руб.)
|
||||
income_year_4: str = "" # Доход за 2 года назад (руб.)
|
||||
income_year_5: str = "" # Доход за прошлый год (руб.)
|
||||
income_loss: 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)
|
||||
|
||||
+415
-99
@@ -26,6 +26,11 @@ from .verify import (
|
||||
)
|
||||
from .api_settings import load_api_settings, save_api_settings
|
||||
|
||||
# Бусредн — средний доход федерального бюджета за 3 года (данные Минфина).
|
||||
# ВАЖНО: значение устаревает — обновлять ежегодно по данным Минфина или
|
||||
# запрашивать у оператора в диалоге авторасчёта (поле редактируется).
|
||||
DEFAULT_BUDGET_AVG = 21228053357700.0
|
||||
|
||||
|
||||
class DokoGenApp:
|
||||
"""Главное окно приложения."""
|
||||
@@ -353,7 +358,7 @@ class DokoGenApp:
|
||||
("Доход за 3 года назад (руб.):", "income_year_3"),
|
||||
("Доход за 2 года назад (руб.):", "income_year_4"),
|
||||
("Доход за прошлый год (руб.):", "income_year_5"),
|
||||
("Суммарный экономический ущерб от нарушения критических процессов (руб.):", "income_loss"),
|
||||
("Снижение уровня дохода от нарушения критических процессов (руб.):", "income_loss"),
|
||||
]
|
||||
for j, (label, key) in enumerate(income_fields):
|
||||
ttk.Label(f_fin, text=label).grid(row=len(fin_fields) + 1 + j, column=0, sticky=tk.W, pady=2)
|
||||
@@ -756,9 +761,12 @@ class DokoGenApp:
|
||||
self._company_entries['okved'].insert(0, resolve_okved(comp['okved'], _tok3))
|
||||
if want('okved_extra') and comp.get('okved_extra') and hasattr(self, 'okved_extra_listbox'):
|
||||
self.okved_extra_listbox.delete(0, tk.END)
|
||||
_applied = 0
|
||||
for code in comp['okved_extra']:
|
||||
if str(code).strip():
|
||||
self.okved_extra_listbox.insert(tk.END, resolve_okved(str(code).strip(), _tok3))
|
||||
_applied += 1
|
||||
self.log(f"✅ Доп. ОКВЭД из DaData: применено {_applied} (всего в ответе: {len(comp['okved_extra'])})")
|
||||
if want('chief'):
|
||||
if comp.get('chief_position') and 'chief_position' in self._official_entries:
|
||||
self._official_entries['chief_position'].delete(0, tk.END)
|
||||
@@ -1082,8 +1090,12 @@ class DokoGenApp:
|
||||
command=self._suggest_threats_program_click).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(thr_actions, text="🤖 Подобрать через ИИ",
|
||||
command=self._suggest_threats_ai_click).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(thr_actions, text="⚖️ Сравнить",
|
||||
command=self._compare_threats).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(thr_actions, text="🤖 Проверить через ИИ",
|
||||
command=self._check_threats_ai_click).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(thr_actions, text="🎯 Подобрать угрозы (программа+ИИ)",
|
||||
command=self._open_threats_picker).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(thr_actions, text="⬇️ Загрузить угрозы ФСТЭК",
|
||||
command=self._download_fstec_threats).pack(side=tk.LEFT, padx=2)
|
||||
self._threats_date_label = ttk.Label(thr_actions, text="", foreground='gray')
|
||||
self._threats_date_label.pack(side=tk.LEFT, padx=8)
|
||||
|
||||
@@ -1418,8 +1430,6 @@ class DokoGenApp:
|
||||
('is_os_sw_listbox', 'os_sw'),
|
||||
('is_app_sw_listbox', 'app_sw'),
|
||||
('is_attacker_category_combo', 'attacker_category'),
|
||||
('is_attacker_level_combo', 'attacker_level'),
|
||||
('is_attacker_types_listbox', 'attacker_types'),
|
||||
('is_attacker_level_combo', 'attacker_level'),
|
||||
('is_attacker_types_listbox', 'attacker_types'),
|
||||
('is_threats_listbox', 'threats'),
|
||||
@@ -2079,7 +2089,7 @@ class DokoGenApp:
|
||||
def _open_criteria_127_dialog(self):
|
||||
"""Окно заполнения показателей критериев значимости по ПП РФ № 127.
|
||||
|
||||
Таблица перенесена дословно из Постановления Правительства РФ от 08.02.2019 № 127:
|
||||
Таблица перенесена дословно из Постановления Правительства РФ от 08.02.2018 № 127:
|
||||
5 критериев (социальная, политическая, экономическая, экологическая,
|
||||
значимость для обороны страны), показатели 1-14 с подпунктами,
|
||||
значения сразу привязаны к категориям III / II / I.
|
||||
@@ -2169,7 +2179,7 @@ class DokoGenApp:
|
||||
|
||||
ttk.Label(body, text="Показатели критериев значимости объекта КИИ",
|
||||
font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 2))
|
||||
ttk.Label(body, text="Постановление Правительства РФ от 08.02.2019 № 127 «Об утверждении Правил категорирования "
|
||||
ttk.Label(body, text="Постановление Правительства РФ от 08.02.2018 № 127 «Об утверждении Правил категорирования "
|
||||
"объектов критической информационной инфраструктуры РФ» (приложение). "
|
||||
"По каждому показателю выберите значение, которому соответствует объект. "
|
||||
"Категория определяется по наивысшей из выбранных.",
|
||||
@@ -2242,14 +2252,24 @@ class DokoGenApp:
|
||||
|
||||
e_profit = _row("Прибыль субъекта КИИ за прошедший год (руб.):", 'profit', profit)
|
||||
e_tax = _row("Налог на прибыль за прошедший год (руб.):", 'tax', profit_tax)
|
||||
e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome', '')
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — экспертная оценка снижения доходов субъекта при наихудшем сценарии "
|
||||
"нарушения (в опроснике её нет, вводится вручную; можно приравнять к ущербу из показателя 8).",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
e_budget = _row("Доход федерального бюджета, усреднённый за 3 года Бусредн (руб.):", 'budget',
|
||||
'21228053357700')
|
||||
ttk.Label(frm, text="⚠️ Бусредн — средний доход федерального бюджета за 3 года (данные Минфина, "
|
||||
"подставлено значение по умолчанию; при необходимости уточните).",
|
||||
# ΔДоходов: если есть балансовая стоимость объекта КИИ — предзаполняем её
|
||||
_bv9 = getattr(isys, 'balance_value', '') or ''
|
||||
_bvn9 = _num(_bv9)
|
||||
e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome',
|
||||
f"{_bvn9:,.0f}" if _bvn9 else '')
|
||||
if _bvn9:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — подставлена балансовая стоимость объекта КИИ "
|
||||
f"({_bvn9:,.0f} руб.); можно поправить.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
else:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — не заполнено: введите вручную или заполните "
|
||||
"балансовую стоимость объекта КИИ во вкладке «Объект».",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
e_budget = _row("Прогнозируемый годовой доход федерального бюджета, усреднённый за планируемый трёхлетний период — Бусредн (руб.):", 'budget',
|
||||
f'{DEFAULT_BUDGET_AVG:.0f}')
|
||||
ttk.Label(frm, text="⚠️ Бусредн — прогнозируемый годовой доход федерального бюджета, "
|
||||
"усреднённый за планируемый трёхлетний период (данные Минфина; "
|
||||
"подставлено значение по умолчанию, при необходимости уточните).",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
ttk.Label(frm, text="Прочие составляющие ΔБ (если применимы, иначе 0):",
|
||||
font=('', 9, 'bold')).pack(anchor=tk.W, pady=(8, 0))
|
||||
@@ -2275,19 +2295,34 @@ class DokoGenApp:
|
||||
|
||||
e_profit = e_tax = None
|
||||
# Доходы за 5 лет — из данных организации (поля «Доходы за 5 лет»)
|
||||
_incs = [_num(getattr(self.model, f'income_year_{i}', '') or '') for i in range(1, 6)]
|
||||
_incs = [x for x in _incs if x is not None]
|
||||
_d_avg = (sum(_incs) / len(_incs)) if _incs else None
|
||||
_incs_raw = [getattr(self.model, f'income_year_{i}', '') or '' for i in range(1, 6)]
|
||||
_incs = [_num(x) for x in _incs_raw]
|
||||
_incs_filled = [x for x in _incs if x is not None]
|
||||
_d_avg = (sum(_incs_filled) / len(_incs_filled)) if _incs_filled else None
|
||||
_d_loss = _num(getattr(self.model, 'income_loss', '') or '')
|
||||
e_dincome = _row("Суммарный экономический ущерб от нарушения критических процессов (руб.):",
|
||||
'dincome', f"{_d_loss:,.0f}" if _d_loss else '')
|
||||
ttk.Label(frm, text="⚠️ Ущерб — поле «Суммарный ущерб (показатель 8)» из вкладки «Организация» "
|
||||
"(подставлено автоматически, можно поправить).",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
_bv8 = getattr(isys, 'balance_value', '') or ''
|
||||
_bvn8 = _num(_bv8)
|
||||
_dincome_def = _d_loss or _bvn8
|
||||
e_dincome = _row("Снижение уровня дохода от нарушения критических процессов (руб.):",
|
||||
'dincome', f"{_dincome_def:,.0f}" if _dincome_def else '')
|
||||
if _d_loss:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — подставлено из вкладки «Организация» "
|
||||
f"({_d_loss:,.0f} руб.); можно поправить.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
elif _bvn8:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — подставлена балансовая стоимость объекта КИИ "
|
||||
f"({_bvn8:,.0f} руб.); можно поправить.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
else:
|
||||
ttk.Label(frm, text="⚠️ ΔДоходов — не заполнено: введите вручную или заполните "
|
||||
"«Снижение уровня дохода» во вкладке «Организация».",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
e_budget = _row("Доходы субъекта КИИ, усреднённые за 5 лет (руб.):",
|
||||
'budget', f"{_d_avg:,.0f}" if _d_avg else '')
|
||||
ttk.Label(frm, text="⚠️ Доходы за 5 лет — среднее по полям «Доход за …» из вкладки «Организация» "
|
||||
"(подставлено автоматически, можно поправить).",
|
||||
_n_years = len(_incs_filled)
|
||||
ttk.Label(frm, text=f"⚠️ Доходы за 5 лет — среднее по заполненным годам ({_n_years} из 5). "
|
||||
"ПП-127 требует усреднение за прошедший пятилетний период: "
|
||||
"при неполных данных результат методологически некорректен — заполните все 5 лет.",
|
||||
foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W)
|
||||
result = tk.Text(frm, height=8, wrap=tk.WORD, state=tk.NORMAL)
|
||||
result.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
|
||||
@@ -2302,7 +2337,7 @@ class DokoGenApp:
|
||||
result.insert('1.0', "Заполните оба поля (ущерб и доходы за 5 лет).")
|
||||
return
|
||||
val = d / b * 100
|
||||
lines.append(f"Суммарный экономический ущерб от нарушения критических процессов: {d:,.0f} руб.")
|
||||
lines.append(f"Снижение уровня дохода от нарушения критических процессов: {d:,.0f} руб.")
|
||||
lines.append(f"Доходы субъекта КИИ, усреднённые за 5 лет: {b:,.0f} руб.")
|
||||
lines.append(f"№8 = ущерб / доходы за 5 лет × 100% = {d:,.0f} / {b:,.0f} × 100 = {val:.2f} %")
|
||||
if val > 20:
|
||||
@@ -2344,7 +2379,7 @@ class DokoGenApp:
|
||||
db = dnp + ndpi + div + vso + ns
|
||||
lines.append(f"ΔНДПИ = {ndpi:,.0f} руб., ΔДИВ = {div:,.0f} руб., ΔВСО = {vso:,.0f} руб., ΔНС = {ns:,.0f} руб.")
|
||||
lines.append(f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.")
|
||||
lines.append(f"Бусредн (доход федерального бюджета за 3 года): {b:,.0f} руб.")
|
||||
lines.append(f"Бусредн (прогноз годового дохода федбюджета, усреднённый за планируемый трёхлетний период): {b:,.0f} руб.")
|
||||
val = db / b * 100
|
||||
lines.append(f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {b:,.0f} × 100 = {val:.6f} %")
|
||||
if val > 0.001:
|
||||
@@ -2585,7 +2620,7 @@ class DokoGenApp:
|
||||
if chosen:
|
||||
auto_parts.append(
|
||||
"Категория значимости объекта КИИ определена в соответствии с Правилами категорирования "
|
||||
"объектов критической информационной инфраструктуры РФ (ПП РФ от 08.02.2019 № 127). "
|
||||
"объектов критической информационной инфраструктуры РФ (ПП РФ от 08.02.2018 № 127). "
|
||||
f"Объект отнесён к {cat} категории значимости. Основания: "
|
||||
+ "; ".join(f"показатель {num} ({c} категория)" for num, _, c, _ in chosen) + "."
|
||||
)
|
||||
@@ -2672,25 +2707,35 @@ class DokoGenApp:
|
||||
profit = _num2(getattr(self.model, 'profit', '') or '')
|
||||
tax = _num2(getattr(self.model, 'profit_tax', '') or '')
|
||||
if profit and tax:
|
||||
# Подсказка: балансовая стоимость объекта как ориентир для ΔДоходов
|
||||
_hint = ''
|
||||
# ΔДоходов: если есть балансовая стоимость объекта КИИ —
|
||||
# подставляем её автоматически (наихудший сценарий = объект
|
||||
# выходит из строя, его надо восстанавливать/покупать заново)
|
||||
_bv = getattr(isys, 'balance_value', '') or ''
|
||||
_bvn = _num2(_bv)
|
||||
if _bvn:
|
||||
_hint = f"\n(балансовая стоимость объекта: {_bvn:,.0f} руб. — можно использовать как ориентир)"
|
||||
dincome = simpledialog.askfloat(
|
||||
"Показатель 9 — ущерб бюджетам",
|
||||
"Снижение уровня доходов субъекта КИИ ΔДоходов (руб.):\n"
|
||||
"(наихудший сценарий компьютерной атаки на объект)" + _hint,
|
||||
parent=dialog, minvalue=0)
|
||||
if dincome is None:
|
||||
continue
|
||||
budget = 21228053357700.0 # Бусредн (доход федерального бюджета за 3 года)
|
||||
dincome = _bvn
|
||||
_dincome_note = (f"ΔДоходов = балансовая стоимость объекта КИИ "
|
||||
f"({dincome:,.0f} руб.) — подставлено автоматически")
|
||||
else:
|
||||
dincome = self._ask_amount(
|
||||
dialog, "Показатель 9 — ущерб бюджетам",
|
||||
"Снижение уровня доходов субъекта КИИ ΔДоходов (руб.):\n"
|
||||
"(наихудший сценарий компьютерной атаки на объект)")
|
||||
if dincome is None:
|
||||
continue
|
||||
_dincome_note = f"ΔДоходов (введено оператором) = {dincome:,.0f} руб."
|
||||
budget = DEFAULT_BUDGET_AVG # Бусредн (прогноз годового дохода федбюджета, усреднённый за 3 года)
|
||||
knp = tax / profit
|
||||
dnp = dincome * knp
|
||||
if dincome > profit:
|
||||
dnp = tax
|
||||
val = dnp / budget * 100
|
||||
# Унификация с авторасчётом: ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС
|
||||
ndpi = _num2(getattr(self.model, 'ndpi', '') or '') or 0
|
||||
div = _num2(getattr(self.model, 'div', '') or '') or 0
|
||||
vso = _num2(getattr(self.model, 'vso', '') or '') or 0
|
||||
ns = _num2(getattr(self.model, 'ns', '') or '') or 0
|
||||
db = dnp + ndpi + div + vso + ns
|
||||
val = db / budget * 100
|
||||
if val > 0.001:
|
||||
cat = 'I'
|
||||
elif val > 0.0006:
|
||||
@@ -2706,10 +2751,13 @@ class DokoGenApp:
|
||||
if target:
|
||||
var.set(target)
|
||||
calc_txt = (
|
||||
f"{_dincome_note}\n"
|
||||
f"Коэффициент КНП = налог/прибыль = {tax:,.0f} / {profit:,.0f} = {knp:.4f}\n"
|
||||
f"ΔНП = ΔДоходов × КНП = {dincome:,.0f} × {knp:.4f} = {dnp:,.1f} руб.\n"
|
||||
f"Бусредн (доход федерального бюджета за 3 года) = {budget:,.0f} руб.\n"
|
||||
f"№9 = ΔБ / Бусредн × 100% = {dnp:,.1f} / {budget:,.0f} × 100 = {val:.6f} %\n"
|
||||
f"ΔНДПИ = {ndpi:,.0f}, ΔДИВ = {div:,.0f}, ΔВСО = {vso:,.0f}, ΔНС = {ns:,.0f} руб.\n"
|
||||
f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.\n"
|
||||
f"Бусредн (прогноз годового дохода федбюджета, усреднённый за 3 года) = {budget:,.0f} руб.\n"
|
||||
f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {budget:,.0f} × 100 = {val:.6f} %\n"
|
||||
f"Категория значимости: {cat or '— (не присваивается)'}"
|
||||
)
|
||||
prefix = f"Показатель {num}. {name} — рекомендация по расчёту:"
|
||||
@@ -2923,31 +2971,87 @@ class DokoGenApp:
|
||||
if not threats:
|
||||
self._verify_log("❌ ИИ не вернул угрозы")
|
||||
return
|
||||
lb = self.is_threats_listbox
|
||||
lb.delete(0, tk.END)
|
||||
for t in threats:
|
||||
lb.insert(tk.END, t)
|
||||
self._collect_is() # сохранить в модель — галочки в словаре будут стоять
|
||||
self.log(f"🤖 ИИ подобрал угроз: {len(threats)}")
|
||||
self._verify_log(f"🤖 ИИ: {len(threats)} угроз")
|
||||
# Запоминаем рекомендации ИИ (уже отфильтрованы по словарю УБИ) —
|
||||
# не заменяем список молча, а открываем окно подбора с галочками.
|
||||
self._last_ai_threats = list(threats)
|
||||
self.log(f"🤖 ИИ рекомендует угроз: {len(threats)}")
|
||||
self._verify_log(f"🤖 ИИ: {len(threats)} угроз (отфильтровано по словарю УБИ)")
|
||||
self._open_threats_picker()
|
||||
|
||||
def _compare_threats(self):
|
||||
"""Сравнить угрозы, подобранные программой и ИИ."""
|
||||
def _ask_amount(self, parent, title, prompt, initial=''):
|
||||
"""Свой диалог ввода суммы: принимает «121 118 270 руб», «1 000» и т.п.
|
||||
|
||||
simpledialog.askfloat НЕ умеет парсить такие строки (ошибка
|
||||
«not a floating-point value please try again») — поэтому свой диалог.
|
||||
Возвращает float или None (отмена).
|
||||
"""
|
||||
def _parse(s):
|
||||
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
|
||||
|
||||
result = {'val': None}
|
||||
dlg = tk.Toplevel(parent)
|
||||
dlg.title(title)
|
||||
dlg.geometry("560x200")
|
||||
dlg.transient(parent)
|
||||
dlg.grab_set()
|
||||
ttk.Label(dlg, text=prompt, wraplength=520, justify=tk.LEFT).pack(
|
||||
padx=12, pady=(12, 6), anchor=tk.W)
|
||||
e = ttk.Entry(dlg, width=42)
|
||||
e.insert(0, str(initial or ''))
|
||||
e.pack(padx=12, pady=4, fill=tk.X)
|
||||
err = ttk.Label(dlg, text="", foreground='red')
|
||||
err.pack(anchor=tk.W, padx=12)
|
||||
|
||||
def _ok():
|
||||
v = _parse(e.get())
|
||||
if v is None:
|
||||
err.config(text="Введите число, например: 121 118 270")
|
||||
return
|
||||
result['val'] = v
|
||||
dlg.grab_release()
|
||||
dlg.destroy()
|
||||
|
||||
def _cancel():
|
||||
dlg.grab_release()
|
||||
dlg.destroy()
|
||||
|
||||
btns = ttk.Frame(dlg)
|
||||
btns.pack(pady=8)
|
||||
ttk.Button(btns, text="ОК", command=_ok).pack(side=tk.LEFT, padx=4)
|
||||
ttk.Button(btns, text="Отмена", command=_cancel).pack(side=tk.LEFT, padx=4)
|
||||
e.bind('<Return>', lambda _ev: _ok())
|
||||
dlg.lift()
|
||||
dlg.focus_force()
|
||||
parent.wait_window(dlg)
|
||||
return result['val']
|
||||
|
||||
def _open_threats_picker(self):
|
||||
"""🎯 Окно подбора угроз: программа (синий) + ИИ (красный) + совпавшие (зелёный).
|
||||
|
||||
Оператор отмечает галочками нужные угрозы (как в словаре) и жмёт
|
||||
«✅ Применить выбранные» — список угроз заменяется выбранными.
|
||||
По умолчанию отмечены все подобранные (программа + ИИ + текущие),
|
||||
оператор снимает галочки с неугодных.
|
||||
"""
|
||||
if self._current_is_idx is None:
|
||||
messagebox.showinfo("Сравнение", "Сначала выберите объект КИИ в списке слева")
|
||||
messagebox.showinfo("Угрозы", "Сначала выберите объект КИИ в списке слева")
|
||||
return
|
||||
self._collect_is()
|
||||
isys = self.model.information_systems[self._current_is_idx]
|
||||
from .threat_rules import suggest_threats_program
|
||||
program = suggest_threats_program(isys)
|
||||
current = [str(self.is_threats_listbox.get(i)) for i in range(self.is_threats_listbox.size())]
|
||||
if not current:
|
||||
messagebox.showinfo("Сравнение", "Сначала подберите угрозы (программой или ИИ).")
|
||||
return
|
||||
ai = list(getattr(self, '_last_ai_threats', None) or [])
|
||||
|
||||
# Нормализация строк угроз для сравнения:
|
||||
# «1. УБИ.001 Угроза…», «УБИ.001 Угроза…» и «УБИ.001 Угроза…» — это одна и та же угроза.
|
||||
# Ключ сравнения — код УБИ, если он есть (ИИ может обрезать/перефразировать название);
|
||||
# Нормализация строк угроз: ключ сравнения — код УБИ (если есть),
|
||||
# иначе — нормализованный текст.
|
||||
def _ubi_code(s):
|
||||
m = re.search(r'уби\.?\s*(\d+)', str(s).lower())
|
||||
@@ -2962,49 +3066,261 @@ class DokoGenApp:
|
||||
code = _ubi_code(s)
|
||||
return ('ubi', code) if code else ('name', _norm(s))
|
||||
|
||||
pset_norm = {_key(t) for t in program}
|
||||
cset_norm = {_key(t) for t in current}
|
||||
|
||||
# Показываем окно: слева подобрано программой, справа текущий список
|
||||
dlg = tk.Toplevel(self.root)
|
||||
dlg.title("Сравнение угроз: программа ↔ ИИ/текущий")
|
||||
dlg.geometry("960x600")
|
||||
dlg.transient(self.root)
|
||||
|
||||
frm = ttk.Frame(dlg, padding=8)
|
||||
frm.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
left = ttk.Frame(frm)
|
||||
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
ttk.Label(left, text=f"⚙️ Программа ({len(program)})", font=('Arial', 9, 'bold')).pack(anchor=tk.W)
|
||||
lb_p = tk.Listbox(left, height=18)
|
||||
lb_p.pack(fill=tk.BOTH, expand=True, pady=4)
|
||||
# Объединяем все источники: key → {text, src: {program, ai, current}}
|
||||
merged = {}
|
||||
def _add(t, src):
|
||||
if not str(t or '').strip():
|
||||
return
|
||||
k = _key(t)
|
||||
if k not in merged:
|
||||
merged[k] = {'text': str(t).strip(), 'src': set()}
|
||||
merged[k]['src'].add(src)
|
||||
for t in program:
|
||||
lb_p.insert(tk.END, t)
|
||||
|
||||
right = ttk.Frame(frm)
|
||||
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 0))
|
||||
ttk.Label(right, text=f"🤖 ИИ / текущий ({len(current)})", font=('Arial', 9, 'bold')).pack(anchor=tk.W)
|
||||
lb_c = tk.Listbox(right, height=18)
|
||||
lb_c.pack(fill=tk.BOTH, expand=True, pady=4)
|
||||
_add(t, 'program')
|
||||
for t in current:
|
||||
lb_c.insert(tk.END, t)
|
||||
_add(t, 'current')
|
||||
for t in ai:
|
||||
_add(t, 'ai')
|
||||
|
||||
# Подсветка совпадений (по коду УБИ / нормализованному виду)
|
||||
common = pset_norm & cset_norm
|
||||
for i in range(lb_p.size()):
|
||||
if _key(lb_p.get(i)) in common:
|
||||
lb_p.itemconfig(i, bg='#c8e6c9')
|
||||
for i in range(lb_c.size()):
|
||||
if _key(lb_c.get(i)) in common:
|
||||
lb_c.itemconfig(i, bg='#c8e6c9')
|
||||
items = sorted(merged.values(),
|
||||
key=lambda v: (0, int(_ubi_code(v['text'])))
|
||||
if _ubi_code(v['text']) else (1, _norm(v['text'])))
|
||||
|
||||
info = ttk.Label(frm,
|
||||
text=f"Совпало угроз: {len(common)} (зелёным). "
|
||||
f"Только программа: {len(program) - len(common)}, "
|
||||
f"только ИИ/текущий: {len(current) - len(common)}.",
|
||||
foreground='gray')
|
||||
info.pack(anchor=tk.W, pady=(4, 0))
|
||||
# Окно с чекбоксами (как выбор из словаря)
|
||||
dlg = tk.Toplevel(self.root)
|
||||
dlg.title(f"Подбор угроз: {isys.name or ''}")
|
||||
dlg.geometry("980x700")
|
||||
dlg.transient(self.root)
|
||||
dlg.grab_set()
|
||||
|
||||
frame = ttk.Frame(dlg, padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
legend = ttk.Label(frame,
|
||||
text="🟦 синий — рекомендация программы | 🟥 красный — рекомендация ИИ | "
|
||||
"🟩 зелёный — совпало | ⬜ белый — уже было в списке",
|
||||
foreground='gray')
|
||||
legend.pack(anchor=tk.W, pady=(0, 5))
|
||||
|
||||
# Поиск
|
||||
search_var = tk.StringVar()
|
||||
search_entry = ttk.Entry(frame, textvariable=search_var)
|
||||
search_entry.pack(fill=tk.X, pady=(0, 5))
|
||||
search_entry.focus_set()
|
||||
|
||||
canvas = tk.Canvas(frame, highlightthickness=0)
|
||||
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
|
||||
check_frame = ttk.Frame(canvas)
|
||||
check_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
canvas.create_window((0, 0), window=check_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
vars = {}
|
||||
widgets = {}
|
||||
for v in items:
|
||||
src = v['src']
|
||||
has_p = 'program' in src
|
||||
has_a = 'ai' in src
|
||||
if has_p and has_a:
|
||||
bg = '#c8e6c9' # зелёный — совпало
|
||||
elif has_p:
|
||||
bg = '#b3d4fc' # синий — только программа
|
||||
elif has_a:
|
||||
bg = '#ffd0d0' # красный — только ИИ
|
||||
else:
|
||||
bg = '#ffffff' # белый — только текущий список
|
||||
var = tk.BooleanVar(value=True) # по умолчанию отмечено всё
|
||||
cb = tk.Checkbutton(check_frame, text=v['text'], variable=var, anchor=tk.W,
|
||||
bg=bg, activebackground=bg, selectcolor='white',
|
||||
justify=tk.LEFT, wraplength=900)
|
||||
cb.pack(fill=tk.X, pady=1)
|
||||
vars[v['text']] = var
|
||||
widgets[v['text']] = cb
|
||||
|
||||
def _apply_search(*_):
|
||||
q = search_var.get().strip().lower()
|
||||
for text, cb in widgets.items():
|
||||
if q and q not in text.lower():
|
||||
cb.pack_forget()
|
||||
else:
|
||||
cb.pack(fill=tk.X, pady=1)
|
||||
search_var.trace_add('write', _apply_search)
|
||||
|
||||
btn_frame = ttk.Frame(dlg, padding=10)
|
||||
btn_frame.pack(fill=tk.X)
|
||||
|
||||
def _select_all():
|
||||
for var in vars.values():
|
||||
var.set(True)
|
||||
|
||||
def _clear_all():
|
||||
for var in vars.values():
|
||||
var.set(False)
|
||||
|
||||
def _apply():
|
||||
selected = [t for t, var in vars.items() if var.get()]
|
||||
selected.sort(key=lambda s: (0, int(_ubi_code(s)))
|
||||
if _ubi_code(s) else (1, _norm(s)))
|
||||
lb = self.is_threats_listbox
|
||||
lb.delete(0, tk.END)
|
||||
for t in selected:
|
||||
lb.insert(tk.END, t)
|
||||
self._collect_is()
|
||||
self.log(f"🎯 Применено угроз: {len(selected)}")
|
||||
dlg.destroy()
|
||||
|
||||
ttk.Button(btn_frame, text="Выбрать всё", command=_select_all).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(btn_frame, text="Снять всё", command=_clear_all).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(btn_frame, text="✅ Применить выбранные", command=_apply).pack(side=tk.RIGHT, padx=2)
|
||||
ttk.Button(btn_frame, text="Отмена", command=dlg.destroy).pack(side=tk.RIGHT, padx=2)
|
||||
|
||||
dlg.wait_window()
|
||||
|
||||
def _check_threats_ai_click(self):
|
||||
"""🤖 Проверить текущий список угроз через ИИ (не подбирать заново).
|
||||
|
||||
Программа/оператор уже подобрали угрозы — ИИ проверяет, что лишнее,
|
||||
чего не хватает, и даёт комментарий.
|
||||
"""
|
||||
if self._current_is_idx is None:
|
||||
messagebox.showinfo("Угрозы", "Сначала выберите объект КИИ в списке слева")
|
||||
return
|
||||
settings = load_api_settings()
|
||||
if not settings.get('ai_api_key'):
|
||||
messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
|
||||
return
|
||||
current = [str(self.is_threats_listbox.get(i)) for i in range(self.is_threats_listbox.size())]
|
||||
if not current:
|
||||
messagebox.showinfo("Угрозы", "Сначала подберите угрозы (программой или ИИ).")
|
||||
return
|
||||
self._collect_is()
|
||||
isys = self.model.information_systems[self._current_is_idx]
|
||||
from .threat_rules import _has_internet
|
||||
has_internet = _has_internet(isys)
|
||||
data = {
|
||||
'name': isys.name or '',
|
||||
'description': isys.description or '',
|
||||
'object_type': isys.object_type or '',
|
||||
'sphere': isys.sphere or '',
|
||||
'architecture': isys.architecture or '',
|
||||
'net_category': isys.net_category or '',
|
||||
'attacker_category': isys.attacker_category or '',
|
||||
'has_internet': has_internet,
|
||||
}
|
||||
self.log(f"🤖 Запрос к ИИ: проверка угроз для «{isys.name}» (в списке: {len(current)})")
|
||||
self._verify_log("Проверка угроз через ИИ…")
|
||||
|
||||
def _run():
|
||||
try:
|
||||
from .verify import check_threats_ai
|
||||
res, err = check_threats_ai(
|
||||
data, current, settings['ai_api_key'], settings.get('ai_model'),
|
||||
settings.get('ai_base_url'))
|
||||
self.root.after(0, lambda: self._check_threats_ai_done(res, err, isys.name))
|
||||
except Exception as e:
|
||||
self.root.after(0, lambda: self._check_threats_ai_done(None, str(e), isys.name))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _check_threats_ai_done(self, res, err, obj_name):
|
||||
"""Показ результата проверки угроз ИИ."""
|
||||
if err:
|
||||
self.log(f"❌ Проверка угроз: {err}")
|
||||
self._verify_log(f"❌ {err}")
|
||||
return
|
||||
if not isinstance(res, dict):
|
||||
self.log("❌ ИИ не вернул результат проверки")
|
||||
return
|
||||
extra = res.get('лишние') or []
|
||||
missing = res.get('недостающие') or []
|
||||
comment = res.get('комментарий') or ''
|
||||
if isinstance(extra, str):
|
||||
extra = [extra]
|
||||
if isinstance(missing, str):
|
||||
missing = [missing]
|
||||
lines = [f"🤖 Проверка угроз для «{obj_name}»:"]
|
||||
lines.append(f"Всего в списке: {self.is_threats_listbox.size()}")
|
||||
if extra:
|
||||
lines.append(f"\n❌ Возможно ЛИШНИЕ ({len(extra)}):")
|
||||
for t in extra:
|
||||
lines.append(f" • {t}")
|
||||
else:
|
||||
lines.append("\n❌ Лишних не найдено")
|
||||
if missing:
|
||||
lines.append(f"\n➕ Возможно НЕ ХВАТАЕТ ({len(missing)}):")
|
||||
for t in missing:
|
||||
lines.append(f" • {t}")
|
||||
else:
|
||||
lines.append("\n➕ Недостающих не найдено")
|
||||
if comment:
|
||||
lines.append(f"\n💬 Комментарий: {comment}")
|
||||
lines.append("\nРекомендации ИИ — оператор принимает решение.")
|
||||
|
||||
dlg = tk.Toplevel(self.root)
|
||||
dlg.title(f"Проверка угроз ИИ — {obj_name}")
|
||||
dlg.geometry("860x600")
|
||||
dlg.transient(self.root)
|
||||
txt = tk.Text(dlg, wrap=tk.WORD, font=('Arial', 10))
|
||||
txt.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
|
||||
txt.insert('1.0', '\n'.join(lines))
|
||||
txt.config(state=tk.DISABLED)
|
||||
|
||||
def _ubi_code(s):
|
||||
m = re.search(r'уби\.?\s*(\d+)', str(s).lower())
|
||||
return m.group(1) if m else None
|
||||
|
||||
def _add_missing():
|
||||
"""Добавить недостающие (только из словаря УБИ, полные названия)."""
|
||||
lb = self.is_threats_listbox
|
||||
existing = {(_ubi_code(lb.get(i)) or lb.get(i).strip().lower())
|
||||
for i in range(lb.size())}
|
||||
added = 0
|
||||
for t in missing:
|
||||
code = _ubi_code(t)
|
||||
key = code or str(t).strip().lower()
|
||||
if key in existing:
|
||||
continue
|
||||
lb.insert(tk.END, t)
|
||||
existing.add(key)
|
||||
added += 1
|
||||
if added:
|
||||
self._collect_is()
|
||||
self.log(f"➕ Добавлено недостающих угроз (по словарю): {added}")
|
||||
else:
|
||||
self.log("➕ Недостающие уже есть в списке — ничего не добавлено")
|
||||
dlg.destroy()
|
||||
|
||||
def _remove_extra():
|
||||
"""Убрать лишние (только те, что реально есть в списке, по коду УБИ)."""
|
||||
lb = self.is_threats_listbox
|
||||
extra_codes = {c for c in (_ubi_code(t) for t in extra) if c}
|
||||
removed = 0
|
||||
for i in range(lb.size() - 1, -1, -1):
|
||||
code = _ubi_code(lb.get(i))
|
||||
if code and code in extra_codes:
|
||||
lb.delete(i)
|
||||
removed += 1
|
||||
if removed:
|
||||
self._collect_is()
|
||||
self.log(f"➖ Убрано лишних угроз: {removed}")
|
||||
else:
|
||||
self.log("➖ Лишние не найдены в списке — ничего не убрано")
|
||||
dlg.destroy()
|
||||
|
||||
btns = ttk.Frame(dlg)
|
||||
btns.pack(pady=(0, 8))
|
||||
if missing:
|
||||
ttk.Button(btns, text="➕ Добавить недостающие (по словарю)",
|
||||
command=_add_missing).pack(side=tk.LEFT, padx=4)
|
||||
if extra:
|
||||
ttk.Button(btns, text="➖ Убрать лишние",
|
||||
command=_remove_extra).pack(side=tk.LEFT, padx=4)
|
||||
ttk.Button(btns, text="Закрыть",
|
||||
command=dlg.destroy).pack(side=tk.LEFT, padx=4)
|
||||
self.log(f"🤖 Проверка угроз ИИ: лишних {len(extra)}, недостающих {len(missing)}")
|
||||
self._verify_log('\n'.join(lines))
|
||||
|
||||
def _check_security_ai(self):
|
||||
"""Проверка вкладки «Безопасность» через ИИ (DeepSeek/OpenAI-совместимый API)."""
|
||||
@@ -4136,8 +4452,8 @@ class DokoGenApp:
|
||||
'chief_fio': 'chiefFio',
|
||||
'responsible_position': 'responsiblePosition',
|
||||
'responsible_fio': 'responsibleFio',
|
||||
'admin_position': ('administratorPosition', 'ispdnPosition'),
|
||||
'admin_fio': ('administratorFio', 'ispdnFio'),
|
||||
'admin_position': 'administratorPosition',
|
||||
'admin_fio': 'administratorFio',
|
||||
}
|
||||
for entry_key, data_keys in officials_map.items():
|
||||
entry = self._official_entries.get(entry_key)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# ============================================================
|
||||
# РЕЗОЛВЕР ЗНАЧЕНИЙ
|
||||
# ============================================================
|
||||
|
||||
+114
-3
@@ -340,8 +340,15 @@ def check_company_dadata(inn, token):
|
||||
# ОКВЭД: основной (okved) и дополнительные (okveds — список словарей с 'code')
|
||||
okved = d.get('okved', '') or ''
|
||||
okved_extra = []
|
||||
for ok in (d.get('okveds') or []):
|
||||
code = (ok or {}).get('code') or ''
|
||||
raw_okveds = d.get('okveds') or []
|
||||
if isinstance(raw_okveds, str):
|
||||
# DaData иногда отдаёт доп. ОКВЭД одной строкой через запятую
|
||||
raw_okveds = [x.strip() for x in raw_okveds.split(',') if x.strip()]
|
||||
for ok in raw_okveds:
|
||||
if isinstance(ok, dict):
|
||||
code = (ok.get('code') or '').strip()
|
||||
else:
|
||||
code = str(ok or '').strip()
|
||||
if code:
|
||||
okved_extra.append(code)
|
||||
|
||||
@@ -518,6 +525,66 @@ def compare_company_values(old_data, new_data):
|
||||
return diffs
|
||||
|
||||
|
||||
def _ubi_code(text):
|
||||
"""Извлекает код УБИ из строки: «УБИ.069 Название» → '069'."""
|
||||
m = re.search(r'уби\.?\s*(\d+)', str(text), re.IGNORECASE)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _norm_threat_text(text):
|
||||
"""Нормализация названия угрозы для сравнения: без ведущего номера, лишних пробелов."""
|
||||
s = re.sub(r'^\d+[.)]?\s*', '', str(text or '').strip())
|
||||
s = re.sub(r'\s+', ' ', s).strip()
|
||||
return s.lower()
|
||||
|
||||
|
||||
def filter_threats_by_dict(lines, threats_full=None):
|
||||
"""Приводит строки угроз (от ИИ или оператора) к каноническому виду по словарю УБИ.
|
||||
|
||||
Для каждой строки:
|
||||
- есть код УБИ → берём полную запись «УБИ.xxx Название» из словаря;
|
||||
- кода нет → ищем по названию (точное совпадение или вхождение);
|
||||
- не нашлось в словаре → отбрасываем (мусор от ИИ).
|
||||
|
||||
Возвращает уникальные канонические строки, отсортированные по коду УБИ.
|
||||
"""
|
||||
if threats_full is None:
|
||||
from .threat_rules import load_threats
|
||||
threats_full = load_threats()
|
||||
by_code = {}
|
||||
by_norm = {}
|
||||
for t in threats_full:
|
||||
t = str(t).strip()
|
||||
code = _ubi_code(t)
|
||||
if code:
|
||||
by_code.setdefault(code, t)
|
||||
by_norm.setdefault(_norm_threat_text(t), t)
|
||||
out = []
|
||||
seen = set()
|
||||
for line in lines or []:
|
||||
line = str(line or '').strip()
|
||||
if not line:
|
||||
continue
|
||||
code = _ubi_code(line)
|
||||
canon = None
|
||||
if code and code in by_code:
|
||||
canon = by_code[code]
|
||||
else:
|
||||
n = _norm_threat_text(line)
|
||||
if n in by_norm:
|
||||
canon = by_norm[n]
|
||||
else:
|
||||
for k, v in by_norm.items():
|
||||
if n and (n in k or k in n):
|
||||
canon = v
|
||||
break
|
||||
if canon and canon not in seen:
|
||||
seen.add(canon)
|
||||
out.append(canon)
|
||||
out.sort(key=lambda t: (int(_ubi_code(t)) if _ubi_code(t) else 10 ** 9, t))
|
||||
return out
|
||||
|
||||
|
||||
def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=None,
|
||||
threats=None):
|
||||
"""ИИ предлагает актуальные угрозы БИ для объекта КИИ по его характеристикам.
|
||||
@@ -564,9 +631,52 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N
|
||||
if isinstance(threats_out, str):
|
||||
threats_out = [threats_out]
|
||||
threats_out = [str(t).strip() for t in threats_out if str(t).strip()]
|
||||
# Жёсткий фильтр по словарю: мусор отбрасываем, «УБИ.069» дополняем
|
||||
# полным названием из актуального перечня (если он передан).
|
||||
threats_out = filter_threats_by_dict(threats_out, threats)
|
||||
return threats_out, None
|
||||
|
||||
|
||||
def check_threats_ai(security_data, current_threats, api_key, model='deepseek-chat', base_url=None):
|
||||
"""ИИ проверяет уже подобранный список угроз БИ для объекта КИИ.
|
||||
|
||||
В отличие от suggest_threats_ai (подбор заново), эта функция проверяет
|
||||
текущий список: какие угрозы лишние, каких не хватает, и даёт комментарий.
|
||||
|
||||
Возвращает (dict, ошибка):
|
||||
{"лишние": [...], "недостающие": [...], "комментарий": "..."}
|
||||
"""
|
||||
if not api_key:
|
||||
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
|
||||
if not current_threats:
|
||||
return None, "Список угроз пуст — сначала подберите угрозы (программой или ИИ)."
|
||||
|
||||
system = (
|
||||
"Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры "
|
||||
"(187-ФЗ, приказ ФСТЭК № 236). Проверь, корректно ли подобран перечень угроз "
|
||||
"безопасности информации для объекта КИИ."
|
||||
)
|
||||
user = (
|
||||
"Характеристики объекта КИИ:\n"
|
||||
f"{json.dumps(security_data, ensure_ascii=False)}\n\n"
|
||||
"Текущий перечень угроз (подобран программой или оператором):\n"
|
||||
+ "\n".join(f"{i+1}. {t}" for i, t in enumerate(current_threats))
|
||||
+ "\n\nВерни ТОЛЬКО JSON: "
|
||||
'{"лишние": ["угрозы, которые НЕ актуальны для этого объекта"], '
|
||||
'"недостающие": ["актуальные угрозы, которых в списке не хватает"], '
|
||||
'"комментарий": "краткий вывод по перечню"}'
|
||||
)
|
||||
js, err = _ai_json(api_key, model, base_url, system, user)
|
||||
if err:
|
||||
return None, err
|
||||
# «Недостающие»/«лишние» — тоже через словарь: мусор отбрасываем,
|
||||
# неполные строки («УБИ.069») дополняем полными названиями.
|
||||
missing = filter_threats_by_dict(js.get('недостающие') or [])
|
||||
extra = filter_threats_by_dict(js.get('лишние') or [])
|
||||
comment = js.get('комментарий') or ''
|
||||
return {'лишние': extra, 'недостающие': missing, 'комментарий': comment}, None
|
||||
|
||||
|
||||
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
|
||||
typical_objects=None, spheres=None, object_types=None,
|
||||
architectures=None):
|
||||
@@ -694,7 +804,8 @@ def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None
|
||||
if not api_key:
|
||||
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
|
||||
system = (
|
||||
"Ты — юрист по защите персональных данных (187-ФЗ). Проверь реквизиты организации. "
|
||||
"Ты — специалист по проверке реквизитов организации (ИНН/КПП/ОГРН). "
|
||||
"Проверь реквизиты организации. "
|
||||
"Верни ТОЛЬКО JSON: "
|
||||
'{"оценка": "ok|warning|error", '
|
||||
'"замечания": ["..."], '
|
||||
|
||||
+8
-8
@@ -30,8 +30,8 @@ def create_test_template():
|
||||
doc.add_paragraph('<!-- loop:information_systems -->')
|
||||
doc.add_paragraph('=== IS: {{item.name}} ===')
|
||||
doc.add_paragraph('Category: {{item.category}}')
|
||||
doc.add_paragraph('Threat type: {{item.threat_type}}')
|
||||
doc.add_paragraph('Defence level: {{item.defence_level}}')
|
||||
doc.add_paragraph('Sphere: {{item.sphere}}')
|
||||
doc.add_paragraph('Object type: {{item.object_type}}')
|
||||
doc.add_paragraph('')
|
||||
|
||||
# Inner loop: commission
|
||||
@@ -72,12 +72,12 @@ def test_zip_loop_expansion():
|
||||
CommissionMember(role='Член комиссии', position='Бухгалтер', fio='Сидорова А.А.'),
|
||||
],
|
||||
information_systems=[
|
||||
InformationSystem(name='ИС-1 "Бухгалтерия"', category='специальные',
|
||||
pd_count='менее 100 000', threat_type='1', defence_level='2'),
|
||||
InformationSystem(name='ИС-2 "Кадры"', category='иные',
|
||||
pd_count='более 100 000', threat_type='3', defence_level='4'),
|
||||
InformationSystem(name='ИС-3 "Склад"', category='общедоступные',
|
||||
pd_count='менее 100 000', threat_type='2', defence_level='3'),
|
||||
InformationSystem(name='ОКИИ-1 "Платёжный шлюз"', category='1',
|
||||
sphere='Банковская сфера и финансовый рынок', object_type='АСУ'),
|
||||
InformationSystem(name='ОКИИ-2 "Кадры"', category='2',
|
||||
sphere='Наука', object_type='ИС'),
|
||||
InformationSystem(name='ОКИИ-3 "Склад"', category='3',
|
||||
sphere='Здравоохранение', object_type='ИС'),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user