Тех. меры: только при присвоенной категории и по факту СЗИ (без СЗИ нет «Регистрации и мониторинга»/«Антивируса»); промт СЗИ: при «без категории» — базовый минимум + провести категорирование, переданы характеристики объекта (интернет/ПО/архитектура); показатель 10 и 10.1–10.7: авторасчёт по методичке ФСТЭК (операции за год / дни) и порогам ПП-127

This commit is contained in:
LocoAgent
2026-08-05 14:39:03 +04:00
parent 07bf25ebc3
commit 9f8bf3999d
5 changed files with 218 additions and 18 deletions
@@ -1,4 +1,4 @@
{
"system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). По категории значимости объекта КИИ, актуальным угрозам и уже выбранным средствам защиты информации (СЗИ) определи, каких средств не хватает, и дай рекомендации. Учитывай требования по категориям: для I категории — максимальный набор (антивирус, СКЗИ, SIEM, межсетевые экраны, средства анти-APT и т.д.), для II — расширенный набор, для III — базовый набор. Выбирай СЗИ ТОЛЬКО из реально существующих классов средств защиты (антивирусная защита, СКЗИ, межсетевой экран, SIEM, DLP, IDS/IPS, средства доверенной загрузки, контроля целостности, управления доступом и т.п.).",
"system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). По категории значимости объекта КИИ, характеристикам объекта, актуальным угрозам и уже выбранным средствам защиты информации (СЗИ) определи, каких средств не хватает, и дай рекомендации. Обязательно учитывай следующее:\n1) КАТЕГОРИЯ ЗНАЧИМОСТИ: если категория «без категории», «—», пусто или не присвоена — объект НЕ является значимым объектом КИИ, полный набор СЗИ по приказу № 239 для него НЕ требуется. В этом случае рекомендую только минимальный базовый набор (антивирусная защита, резервное копирование, управление доступом) и главный совет — провести категорирование объекта. НЕ перечисляй средства для I категории (СКЗИ, SIEM, анти-APT, доверенная загрузка и т.п.), если категория не присвоена. Для I категории — максимальный набор, для II — расширенный, для III — базовый набор по приказу № 239.\n2) ХАРАКТЕРИСТИКИ ОБЪЕКТА: внимательно читай переданные характеристики (наличие интернета, архитектура, ПО, тип объекта). НЕ рекомендуй СЗИ для технологий, которых нет у объекта: нет интернета/внешних сетей → не рекомендуй DDoS-защиту, VPN, веб-защиту, защиту от удалённых атак из интернета; нет виртуализации/контейнеров → не рекомендуй защиту виртуальной инфраструктуры и контейнеров; нет мобильных устройств → не рекомендуй защиту мобильных; объект не АСУ ТП → не рекомендуй защиту АСУ ТП; нет гостайны → не рекомендуй средства для гостайны. Отмечай только реально применимые классы СЗИ.\n3) ОБЪЁМ: максимум 5–7 рекомендаций, только самые важные для данной категории и объекта. Если текущий набор СЗИ достаточен — верни пустой список рекомендаций и «достаточно»: true.\nВыбирай СЗИ ТОЛЬКО из реально существующих классов средств защиты (антивирусная защита, СКЗИ, межсетевой экран, SIEM, DLP, IDS/IPS, средства доверенной загрузки, контроля целостности, управления доступом и т.п.).",
"response_format": "Верни ТОЛЬКО JSON: {\"рекомендации\": [{\"сзи\": \"название класса/средства\", \"зачем\": \"для чего нужно\"}], \"общие_советы\": [\"...\"], \"достаточно\": true|false}. Поле «достаточно» — true, если текущий набор СЗИ покрывает требования категории. Не придумывай конкретные торговые марки, если их нет в списке — указывай класс средства."
}
+5
View File
@@ -144,6 +144,11 @@ class Company:
vso: str = "" # Снижение выплат сторонним организациям ΔВСО (руб.)
ns: str = "" # Снижение сборов ΔНС (руб.)
# Показатель 10 (прекращение финансовых операций): операции за год (млн ед.)
ops_count: str = "" # Количество операций за прошедший год (млн ед.)
calendar_days: str = "" # Количество календарных дней в году (по умолчанию 365)
criterion10_value: str = "" # Значение показателя 10.1–10.7 (в своих единицах)
# Списки
commission: List[CommissionMember] = field(default_factory=list)
information_systems: List[InformationSystem] = field(default_factory=list)
+35 -13
View File
@@ -298,19 +298,18 @@ BASE_ORG_MEASURES = [
'Утверждение перечня лиц, имеющих доступ к объекту',
]
# Базовый набор тех. мер
# Базовый набор тех. мер — только универсальные, не требующие конкретных СЗИ.
# Меры, зависящие от средств защиты (антивирус, СЗИ от НСД, регистрация событий
# и т.п.), добавляются отдельно по факту наличия СЗИ (DEFENSE_TOOL_MEASURES).
BASE_TECH_MEASURES = [
'Идентификация и аутентификация пользователей',
'Управление доступом',
'Ограничение программной среды',
'Антивирусная защита',
'Защита информации от несанкционированного доступа (СЗИ от НСД)',
'Регистрация и мониторинг событий безопасности',
'Резервное копирование и восстановление',
'Защита технических средств и линий связи',
]
# Средства защиты → доп. тех. меры
# Средства защиты → доп. тех. меры (по факту наличия СЗИ у объекта)
DEFENSE_TOOL_MEASURES = [
('межсетев', 'Межсетевое экранирование'),
('firewall', 'Межсетевое экранирование'),
@@ -324,6 +323,23 @@ DEFENSE_TOOL_MEASURES = [
('виртуализ', 'Защита среды виртуализации'),
('vmware', 'Защита среды виртуализации'),
('hyper-v', 'Защита среды виртуализации'),
('антивирус', 'Антивирусная защита'),
('kaspersky', 'Антивирусная защита'),
('eset', 'Антивирусная защита'),
('dr.web', 'Антивирусная защита'),
('аваст', 'Антивирусная защита'),
('avast', 'Антивирусная защита'),
('сзи', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
('от нсд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
('dallas', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
('secret net', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
('аккорд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
('siem', 'Регистрация и мониторинг событий безопасности'),
('dlp', 'Регистрация и мониторинг событий безопасности'),
('мониторинг', 'Регистрация и мониторинг событий безопасности'),
('регистрац', 'Регистрация и мониторинг событий безопасности'),
('доверенная загрузка', 'Контроль (доверенная) загрузки'),
('резервн', 'Резервное копирование и восстановление'),
]
@@ -395,15 +411,21 @@ def auto_fill_security(isys, threats=None):
if not (getattr(isys, 'org_measures', '') or '').strip():
res['org_measures'] = '\n'.join(BASE_ORG_MEASURES)
# 7. Тех. меры — базовые + по СЗИ объекта
# 7. Тех. меры — базовые + по СЗИ объекта.
# Только если объекту присвоена категория значимости (1/2/3):
# для незначимого объекта технические меры не требуются.
if not (getattr(isys, 'tech_measures', '') or '').strip():
tech = list(BASE_TECH_MEASURES)
defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
dlow = defense.lower()
for kw, measure in DEFENSE_TOOL_MEASURES:
if kw in dlow and measure not in tech:
tech.append(measure)
res['tech_measures'] = '\n'.join(tech)
cat = (getattr(isys, 'category', '') or '').strip()
has_category = cat in ('1', '2', '3')
if has_category:
tech = list(BASE_TECH_MEASURES)
defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
dlow = defense.lower()
for kw, measure in DEFENSE_TOOL_MEASURES:
if kw in dlow and measure not in tech:
tech.append(measure)
res['tech_measures'] = '\n'.join(tech)
# else: категория не присвоена — технические меры не заполняем
return res
+91 -4
View File
@@ -2241,7 +2241,52 @@ class DokoGenApp:
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
if num == '9':
if num == '10' or str(num).startswith('10.'):
# Показатель 10 — прекращение финансовых операций (банковская сфера)
is_base = str(num) == '10'
if is_base:
ttk.Label(frm, text="Показатель 10 — прекращение (нарушение) проведения клиентами операций "
"по переводу денежных средств",
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
ttk.Label(frm, text="Применим для: системно значимых кредитных организаций, операторов услуг "
"платёжной инфраструктуры значимых платёжных систем, операторов платформы "
"цифрового рубля и др.\n"
"Формула: №10 = операции за год (млн ед.) / календарные дни в году "
"(среднедневное количество операций).\n"
"Пороги: ≤ 70 млн/день → III; > 70 и ≤ 120 → II; > 120 → I.",
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
else:
ttk.Label(frm, text=f"Показатель {num} — прекращение (нарушение) проведения операций "
"(подпоказатель 10.x)",
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
ttk.Label(frm, text="Значение показателя вводится в единицах, установленных ПП-127 "
"(трлн руб., тыс. шт., млрд руб., количество договоров, млн ед.); "
"категория определяется по порогам ПП-127.",
foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
def _row(label, key, default=''):
ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0))
e = ttk.Entry(frm, width=80)
e.insert(0, str(default or ''))
e.pack(fill=tk.X)
return e
_ops = getattr(self.model, 'ops_count', '') or ''
_days = getattr(self.model, 'calendar_days', '') or '365'
_v10 = getattr(self.model, 'criterion10_value', '') or ''
if is_base:
e_ops = _row("Количество операций за прошедший год (млн ед.):", 'ops', _ops)
e_days = _row("Количество календарных дней в году (по умолчанию 365):", 'days', _days)
else:
e_val10 = _row("Значение показателя (в единицах ПП-127):", 'val10', _v10)
result_frame = ttk.Frame(frm)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))
result = tk.Text(result_frame, height=12, wrap=tk.WORD, state=tk.NORMAL)
result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview)
result.configure(yscrollcommand=result_scroll.set)
result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
result_scroll.pack(side=tk.RIGHT, fill=tk.Y)
elif num == '9':
ttk.Label(frm, text="Показатель 9 — ущерб бюджетам РФ (применим ко всем субъектам КИИ)",
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
ttk.Label(frm, text="Формула: №9 = ΔБ / Бусредн × 100%, где ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС\n"
@@ -2353,7 +2398,34 @@ class DokoGenApp:
def _calc():
lines = []
if num == '8':
if num == '10' or str(num).startswith('10.'):
from .variables import _criterion_10_cat
if str(num) == '10':
ops = _num(e_ops.get())
days = _num(e_days.get())
if days is None or days <= 0:
days = 365
if ops is None:
result.delete('1.0', tk.END)
result.insert('1.0', "Заполните количество операций за год (млн ед.).")
return
val = ops / days
lines.append(f"Количество операций за прошедший год: {ops:,.0f} млн ед.")
lines.append(f"Количество календарных дней в году: {days:.0f}")
lines.append(f"№10 = операции / дни = {ops:,.0f} / {days:.0f} = {val:.6f} млн ед./день")
else:
val = _num(e_val10.get())
if val is None:
result.delete('1.0', tk.END)
result.insert('1.0', "Заполните значение показателя.")
return
lines.append(f"Значение показателя {num}: {val:,.6f}")
cat = _criterion_10_cat(num, val)
if cat:
lines.append(f"Категория значимости: {cat} (по порогам ПП-127)")
else:
lines.append("Категория значимости: — (значение ниже порога III)")
elif num == '8':
d = _num(e_dincome.get())
b = _num(e_budget.get())
if d is None or b is None or b <= 0:
@@ -2505,8 +2577,9 @@ class DokoGenApp:
combo.pack(side=tk.RIGHT, padx=6)
_combos[num] = combo
# Кнопка «🧮» — авторасчёт показателя (п. 8 «ущерб субъекту КИИ» и п. 9 «ущерб бюджетам»)
if grp_num in ('8', '9'):
# Кнопка «🧮» — авторасчёт показателя (п. 8 «ущерб субъекту КИИ»,
# п. 9 «ущерб бюджетам», п. 10 «прекращение финансовых операций»)
if grp_num in ('8', '9', '10'):
ttk.Button(row, text='🧮', width=3,
command=lambda n=num, nm=name, v=var, c=combo:
_open_calc_dialog(n, nm, v, c)
@@ -3773,6 +3846,13 @@ class DokoGenApp:
'category': isys.category or '',
'sphere': isys.sphere or '',
'object_type': isys.object_type or '',
'description': isys.description or '',
'architecture': isys.architecture or '',
'hw_sw': isys.hw_sw or '',
'os_sw': isys.os_sw or '',
'app_sw': isys.app_sw or '',
'net_category': isys.net_category or '',
'is_internet': bool(getattr(isys, 'is_internet', False)),
'threats': isys.threats or '',
'attacker_category': isys.attacker_category or '',
'defense_tools': list(self.defense_tools_listbox.get(0, tk.END)) or [],
@@ -3838,6 +3918,13 @@ class DokoGenApp:
'category': isys.category or '',
'sphere': isys.sphere or '',
'object_type': isys.object_type or '',
'description': isys.description or '',
'architecture': isys.architecture or '',
'hw_sw': isys.hw_sw or '',
'os_sw': isys.os_sw or '',
'app_sw': isys.app_sw or '',
'net_category': isys.net_category or '',
'is_internet': bool(getattr(isys, 'is_internet', False)),
'threats': isys.threats or '',
'attacker_category': isys.attacker_category or '',
'defense_tools': list(self.defense_tools_listbox.get(0, tk.END)) or [],
+86
View File
@@ -518,6 +518,84 @@ def _calc_criterion_8_9(company, isys, num):
return text, cat
# Пороги показателя 10 и подпоказателей 10.1–10.7 (ПП-127, «Значение показателя»)
CRITERION_10_THRESHOLDS = {
# num: (категория → (нижняя граница, верхняя граница)), границы «включительно»
# Показатель 10 — среднедневное количество операций (млн ед.):
# III — менее или равно 70, II — более 70 но менее или равно 120, I — более 120.
'10': {'I': (120, None), 'II': (70, 120), 'III': (None, 70)},
'10.1': {'I': (10, None), 'II': (1, 10), 'III': (None, 1)}, # трлн руб.
'10.2': {'I': (25, None), 'II': (10, 25), 'III': (None, 10)}, # тыс. шт.
'10.3': {'I': (2000, None), 'II': (1000, 2000), 'III': (50, 1000)}, # млрд руб.
'10.4': {'I': (5000, None), 'II': (1500, 5000), 'III': (100, 1500)}, # млрд руб.
'10.5': {'I': (150, None), 'II': (100, 150), 'III': (25, 100)}, # договоров
'10.6': {'I': (5000, None), 'II': (1000, 5000), 'III': (100, 1000)}, # млрд руб.
'10.7': {'I': (500, None), 'II': (200, 500), 'III': (30, 200)}, # млн ед.
}
def _criterion_10_cat(num, value):
"""Категория значимости по показателю 10 / 10.x (ПП-127).
value численное значение показателя. Возвращает 'I'/'II'/'III' или None.
"""
thr = CRITERION_10_THRESHOLDS.get(str(num))
if not thr or value is None:
return None
for cat in ('I', 'II', 'III'):
lo, hi = thr[cat]
if lo is not None and value < lo:
continue
if hi is not None and value > hi:
continue
return cat
return None
def _calc_criterion_10(company, isys, num):
"""Расчёт показателя 10 (прекращение финансовых операций) и подпоказателей 10.1–10.7.
Показатель 10: среднедневное количество операций = операции за год (млн ед.) / дни.
Подпоказатели 10.x: значение показателя (в своих единицах) сравнивается с порогами.
Возвращает (текст_обоснования, категория или None).
"""
num = str(num)
if num not in CRITERION_10_THRESHOLDS:
return None, None
if num == '10':
ops = _num(getattr(company, 'ops_count', '') or '')
days = _num(getattr(company, 'calendar_days', '') or '')
if days is None or days <= 0:
days = 365
if ops is None:
return ("Показатель применим. Для расчёта заполните данные организации: "
"«Количество операций за прошедший год (млн ед.)».", None)
val = ops / days
cat = _criterion_10_cat('10', val)
text = (f"Показатель прекращения финансовых операций оценивается среднедневным количеством "
f"операций, проведенных клиентами по банковским счетам и (или) без открытия банковского "
f"счета. Количество операций за прошедший год: {ops:,.2f} млн ед. Количество календарных "
f"дней в году: {days:.0f}. Среднедневное количество операций: {ops:,.2f} / {days:.0f} "
f"= {val:.6f} млн ед./день.")
else:
value = _num(getattr(company, 'criterion10_value', '') or '')
if value is None:
return (f"Показатель применим. Для расчёта заполните значение показателя "
f"«{num}» в данных организации.", None)
val = value
cat = _criterion_10_cat(num, val)
text = (f"Показатель {num} оценивается значением показателя: {val:,.6f} "
f"(в единицах, установленных ПП-127).")
if cat:
text += (f" Согласно Постановлению № 127 полученное значение соответствует "
f"{cat}-й категории значимости, по данному показателю объекту присваивается "
f"{cat}-я категория значимости.")
else:
text += (" Значение не достигает порога III категории — по данному показателю "
"категория значимости не присваивается.")
return text, cat
def _build_org_measures_text(company, isys=None):
"""Формирует текст организационных мер из словаря org_measures.json
с учётом ответов раздела «Физическая безопасность» опросника.
@@ -617,6 +695,14 @@ def _build_categorization_indicators(company, is_list):
category = f"{cat}-я категория"
else:
category = ''
elif grp_num == '10':
text, cat = _calc_criterion_10(company, isys, num)
if text:
value = text
if cat:
category = f"{cat}-я категория"
else:
category = ''
else:
# Применимый показатель без авторасчёта — оператор заполняет значение
value = ''