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"
|
||||
],
|
||||
"Топливно-энергетический комплекс": [
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"6": "Организация сферы здравоохранения не является органом государственной власти, в связи с этим критерий неприменим.",
|
||||
"7": "Организация сферы здравоохранения не является субъектом политической сферы деятельности, в связи с этим критерий неприменим.",
|
||||
"8": "Организация сферы здравоохранения не является государственной корпорацией, государственным унитарным предприятием, государственной компанией, стратегическим акционерным обществом, стратегическим предприятием, в связи с этим критерий неприменим.",
|
||||
"9": "Объект КИИ задействован только в предоставлении медицинских услуг, освобожденных от налогообложения, в связи с этим критерий неприменим.",
|
||||
"9": "Объект КИИ задействован в предоставлении медицинских услуг. Показатель 9 применим ко всем субъектам КИИ; при расчёте учитывается, что медицинские услуги освобождены от налогообложения (значение показателя может быть минимальным).",
|
||||
"10": "Организация сферы здравоохранения не является субъектом банковской сферы деятельности.",
|
||||
"10.1": "Организация сферы здравоохранения не является центральным контрагентом.",
|
||||
"10.2": "Организация сферы здравоохранения не является центральным депозитарием.",
|
||||
|
||||
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)
|
||||
|
||||
+409
-93
@@ -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)
|
||||
|
||||
@@ -1419,8 +1431,6 @@ class DokoGenApp:
|
||||
('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'),
|
||||
('is_incidents_listbox', 'incidents'),
|
||||
@@ -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).",
|
||||
# ΔДоходов: если есть балансовая стоимость объекта КИИ — предзаполняем её
|
||||
_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)
|
||||
e_budget = _row("Доход федерального бюджета, усреднённый за 3 года Бусредн (руб.):", 'budget',
|
||||
'21228053357700')
|
||||
ttk.Label(frm, text="⚠️ Бусредн — средний доход федерального бюджета за 3 года (данные Минфина, "
|
||||
"подставлено значение по умолчанию; при необходимости уточните).",
|
||||
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)» из вкладки «Организация» "
|
||||
"(подставлено автоматически, можно поправить).",
|
||||
_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 — ущерб бюджетам",
|
||||
dincome = _bvn
|
||||
_dincome_note = (f"ΔДоходов = балансовая стоимость объекта КИИ "
|
||||
f"({dincome:,.0f} руб.) — подставлено автоматически")
|
||||
else:
|
||||
dincome = self._ask_amount(
|
||||
dialog, "Показатель 9 — ущерб бюджетам",
|
||||
"Снижение уровня доходов субъекта КИИ ΔДоходов (руб.):\n"
|
||||
"(наихудший сценарий компьютерной атаки на объект)" + _hint,
|
||||
parent=dialog, minvalue=0)
|
||||
"(наихудший сценарий компьютерной атаки на объект)")
|
||||
if dincome is None:
|
||||
continue
|
||||
budget = 21228053357700.0 # Бусредн (доход федерального бюджета за 3 года)
|
||||
_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)}.",
|
||||
# Окно с чекбоксами (как выбор из словаря)
|
||||
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')
|
||||
info.pack(anchor=tk.W, pady=(4, 0))
|
||||
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