Files
dokogen187/dokogen/threat_rules.py
T
prog1764 68caaf9b63 feat: автозаполнение вкладки «Безопасность» по данным объекта
Новая кнопка « Автозаполнить» в панели действий — заполняет пустые поля
логически, без выдумок:
- категория нарушителя: есть интернет → внешний, нет → внутренний
- уровень возможностей: Н1 (внутренний) / Н2 (внешний)
- виды нарушителей: по категории из словаря attacker_types.json
- угрозы: подбор программой (скоринг по характеристикам объекта)
- инциденты: выводятся из подобранных угроз по ключевым словам
- орг. меры: базовый набор (контролируемая зона, доступ, документы,
  ответственные, обучение, инциденты, перечень лиц)
- тех. меры: базовые + доп. по средствам защиты объекта (СЗИ:
  межсетевой экран → МЭ, антивирус, СОВ, СКЗИ, виртуализация)
Незаполненное пользователем не перезаписывает.
2026-08-04 20:35:29 +04:00

345 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""DokoGen — автоматический подбор угроз безопасности информации (236 приказ).
Логика программы: по характеристикам объекта КИИ (тип, сеть, нарушитель)
подбирает актуальные угрозы из перечня БДУ ФСТЭК (threats_fstec.json).
Если перечень не скачан — использует базовый threats.json.
Правила эвристические; при наличии методического документа (например,
«Методика оценки угроз безопасности информации» ФСТЭК) — адаптируются под него.
"""
import os
import json
def load_threats(dict_dir=None):
"""Список угроз: сначала скачанный с ФСТЭК (с кодами УБИ), иначе базовый."""
if dict_dir is None:
dict_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries')
fstec = os.path.join(dict_dir, 'threats_fstec.json')
if os.path.exists(fstec):
try:
with open(fstec, encoding='utf-8') as f:
data = json.load(f)
if data:
return data
except Exception:
pass
base = os.path.join(dict_dir, 'threats.json')
try:
with open(base, encoding='utf-8') as f:
return json.load(f)
except Exception:
return []
# ---------------------------------------------------------------
# Ключевые слова (в названии угрозы) по признакам объекта
# ---------------------------------------------------------------
# Базовый набор — актуален для любого объекта КИИ
BASE_KEYWORDS = [
'вредоносн', 'несанкционированн', 'утечк', 'перехват', 'модификац',
'уничтожен', 'отказ в обслуживании', 'нсд', 'биос', 'пароль',
'криптографическ', 'шифрован', 'аутентификац',
]
# Объект имеет подключение к сети Интернет / сеть электросвязи
INTERNET_KEYWORDS = [
'удалённ', 'удаленн', 'сетев', 'фишинг', 'ddos', 'отказ в обслуживании',
'веб', 'электронн', 'почт', 'беспроводн', 'wi-fi', 'wifi', 'vpn',
'маршрутизац', 'протокол', 'интернет',
]
# Объект изолирован (нет подключения к Интернету)
ISOLATED_KEYWORDS = [
'съёмн', 'съемн', 'физическ', 'локальн', 'инсайд', 'носител',
'биос', 'желез', 'аппаратн',
]
# АСУ / технологический процесс
ASU_KEYWORDS = [
'технологическ', 'асутп', 'промышленн', 'scada', 'датчик',
'исполнительн', 'технологическ', 'производств', 'процесс',
]
# ИТКС / сети связи
NETWORK_KEYWORDS = [
'сеть', 'канал', 'протокол', 'маршрутизац', 'связ', 'передач',
'коммутац', 'трафик',
]
# Внутренний нарушитель
INSIDER_KEYWORDS = [
'инсайд', 'сотрудник', 'привилегированн', 'администратор',
'внутренн', 'персонал',
]
# Внешний нарушитель
EXTERNAL_KEYWORDS = [
'внешн', 'удалённ', 'удаленн', 'дистанционн',
]
def _is_asu(isys) -> bool:
t = (getattr(isys, 'object_type', '') or '').lower()
return 'асу' in t or 'автоматизированн' in t or 'технологическ' in t
def _is_network(isys) -> bool:
t = (getattr(isys, 'object_type', '') or '').lower()
return 'телекоммуникац' in t or 'сеть' in t
def _has_internet(isys) -> bool:
"""Есть ли подключение к сети Интернет / внешним сетям."""
if getattr(isys, 'is_internet', False):
return True
desc = ((getattr(isys, 'description', '') or '') + ' ').lower()
operator = (getattr(isys, 'net_operator', '') or '').lower()
net_category = (getattr(isys, 'net_category', '') or '').lower()
# Явные признаки ОТСУТСТВИЯ внешней сети
if 'отсутств' in operator: # оператор «Отсутствует»
return False
if 'отсутств' in desc and ('интернет' in desc or 'подключ' in desc):
return False
if 'не требуется' in desc and ('доменн' in desc or 'интернет' in desc):
return False
if 'изолирован' in desc:
return False
# Сеть общего пользования — это и есть внешняя сеть (Интернет)
if 'общего пользован' in net_category:
return True
# Указан реальный оператор/провайдер — внешняя связь есть
if operator and operator not in ('нет', 'отсутствует', 'не требуется', '-'):
return True
# Консервативно: без явных признаков внешней сети считаем, что её нет
return False
def _is_insider(isys) -> bool:
a = (getattr(isys, 'attacker_category', '') or '').lower()
return 'внутрен' in a
def _score(threat_lower, keywords):
"""Сколько ключевых слов встретилось в названии угрозы."""
return sum(1 for kw in keywords if kw in threat_lower)
def suggest_threats_program(isys, threats=None, limit=None):
"""Подбор угроз по характеристикам объекта КИИ (инвертированная логика).
По умолчанию актуальны ВСЕ угрозы из перечня (БДУ ФСТЭК).
Исключаются только заведомо неактуальные для данного объекта:
- нет сети Интернет → убираем угрозы удалённого доступа, веб, почты,
беспроводных сетей, DDoS, облачных и т.п.;
- объект типа АСУ → убираем угрозы BIOS, грид-систем, виртуализации
(у технологического оборудования их, как правило, нет).
Такой подход даёт полный список, а не «15 самых похожих».
Возвращает список строк (с кодами УБИ, если они есть в перечне).
"""
if threats is None:
threats = load_threats()
if not threats:
return []
has_internet = _has_internet(isys)
is_asu = _is_asu(isys)
is_network = _is_network(isys)
# --- Слова-исключения (если встретились в названии угрозы — она неактуальна) ---
exclude = []
if not has_internet:
# Объект изолирован: угрозы, реализуемые только через внешние сети
exclude += [
'удалённ', 'удаленн', 'дистанционн', 'веб', 'электронн', 'почт',
'фишинг', 'беспроводн', 'wi-fi', 'wifi', 'интернет', 'ddos', 'vpn',
'облачн', 'грид', 'внешн',
]
if is_asu:
# АСУ: нет BIOS/виртуализации/браузера в привычном понимании
exclude += ['биос', 'грид', 'гипервизор', 'виртуальн', 'браузер']
if not is_network and not is_asu:
# Обычная ИС: неактуальны чисто сетевые/телеком-угрозы без указания
# — оставляем всё, кроме специфики каналов связи, если их нет
pass
# --- Положительные ключевые слова: чем больше совпало — тем угроза актуальнее ---
pos = list(BASE_KEYWORDS)
if has_internet:
pos += INTERNET_KEYWORDS
else:
pos += ISOLATED_KEYWORDS
if is_asu:
pos += ASU_KEYWORDS
if is_network:
pos += NETWORK_KEYWORDS
if _is_insider(isys):
pos += INSIDER_KEYWORDS
else:
pos += EXTERNAL_KEYWORDS
# Убираем дубликаты
pos = list(dict.fromkeys(pos))
scored = []
for t in threats:
tl = str(t).lower()
if any(kw in tl for kw in exclude):
continue
sc = sum(1 for kw in pos if kw in tl)
scored.append((sc, t))
# Сортируем по релевантности (сначала — с наибольшим совпадением)
scored.sort(key=lambda x: -x[0])
result = [t for sc, t in scored if sc > 0]
if limit and len(result) > limit:
result = result[:limit]
return result
# ---------------------------------------------------------------
# Автозаполнение вкладки «Безопасность» по данным объекта (логическое)
# ---------------------------------------------------------------
# Ключевые слова: угроза → тип инцидента (словарь incidents.json)
INCIDENT_RULES = [
('отказ в обслуживании', 'отказ в обслуживании'),
('несанкционированн', 'несанкционированный доступ'),
('утечк', 'утечка данных (нарушение конфиденциальности)'),
('модификац', 'модификация (подмена) данных'),
('подмен', 'модификация (подмена) данных'),
('функционирован', 'нарушение функционирования технических средств'),
('техническ', 'нарушение функционирования технических средств'),
('использован', 'несанкционированное использование вычислительных ресурсов объекта'),
]
# Базовый набор орг. мер (всегда актуальны для значимого объекта)
BASE_ORG_MEASURES = [
'Установление контролируемой зоны',
'Контроль физического доступа к объекту',
'Разработка документов (регламентов, инструкций, руководств) по обеспечению безопасности объекта',
'Назначение ответственных за обеспечение безопасности объекта',
'Обучение и инструктаж персонала',
'Регламентация действий при компьютерных инцидентах',
'Утверждение перечня лиц, имеющих доступ к объекту',
]
# Базовый набор тех. мер
BASE_TECH_MEASURES = [
'Идентификация и аутентификация пользователей',
'Управление доступом',
'Ограничение программной среды',
'Антивирусная защита',
'Защита информации от несанкционированного доступа (СЗИ от НСД)',
'Регистрация и мониторинг событий безопасности',
'Резервное копирование и восстановление',
'Защита технических средств и линий связи',
]
# Средства защиты → доп. тех. меры
DEFENSE_TOOL_MEASURES = [
('межсетев', 'Межсетевое экранирование'),
('firewall', 'Межсетевое экранирование'),
('сетев', 'Межсетевое экранирование'),
('обнаруж', 'Обнаружение вторжений (СОВ)'),
('ids', 'Обнаружение вторжений (СОВ)'),
('ips', 'Обнаружение вторжений (СОВ)'),
('криптограф', 'Криптографическая защита информации (СКЗИ)'),
('vipnet', 'Криптографическая защита информации (СКЗИ)'),
('континент', 'Криптографическая защита информации (СКЗИ)'),
('виртуализ', 'Защита среды виртуализации'),
('vmware', 'Защита среды виртуализации'),
('hyper-v', 'Защита среды виртуализации'),
]
def auto_fill_security(isys, threats=None):
"""Логическое автозаполнение вкладки «Безопасность» по данным объекта КИИ.
Заполняет (только если поле пустое):
attacker_category — по наличию интернета (внешний/внутренний нарушитель);
attacker_level — Н1/Н2 (внутренний — базовые, внешний — базовые повышенные);
attacker_types — виды нарушителей по категории/интернету;
threats — подбор программой (скоринг по характеристикам);
incidents — типы инцидентов, выводимые из подобранных угроз;
org_measures / tech_measures — базовый набор + по СЗИ объекта.
Возвращает dict {поле: новое_значение} только для пустых полей.
"""
from .models import InformationSystem
res = {}
has_internet = _has_internet(isys)
# 1. Категория нарушителя
if not (getattr(isys, 'attacker_category', '') or '').strip():
res['attacker_category'] = 'Внешний нарушитель' if has_internet else 'Внутренний нарушитель'
# 2. Уровень возможностей
if not (getattr(isys, 'attacker_level', '') or '').strip():
res['attacker_level'] = 'Н2 — Базовые повышенные возможности' if has_internet else 'Н1 — Базовые возможности'
# 3. Виды нарушителей
if not (getattr(isys, 'attacker_types', '') or '').strip():
if has_internet:
types = [
'Отдельные физические лица (хакеры)',
'Преступные группы (криминальные структуры)',
'Конкурирующие организации',
'Авторизованные пользователи систем и сетей',
'Системные администраторы и администраторы безопасности',
'Бывшие (уволенные) работники (пользователи)',
]
else:
types = [
'Авторизованные пользователи систем и сетей',
'Системные администраторы и администраторы безопасности',
'Бывшие (уволенные) работники (пользователи)',
'Лица, обеспечивающие функционирование систем и сетей или обеспечивающих систем оператора (администрация, охрана, уборщики и др.)',
]
res['attacker_types'] = '\n'.join(types)
# 4. Угрозы — подбор программой
if not (getattr(isys, 'threats', '') or '').strip():
prog = suggest_threats_program(isys, threats=threats)
if prog:
res['threats'] = '\n'.join(prog)
# 5. Инциденты — из подобранных угроз
if not (getattr(isys, 'incidents', '') or '').strip():
threats_text = res.get('threats') or (getattr(isys, 'threats', '') or '')
if threats_text:
incidents = []
low = threats_text.lower()
for kw, incident in INCIDENT_RULES:
if kw in low and incident not in incidents:
incidents.append(incident)
if incidents:
res['incidents'] = '\n'.join(incidents)
# 6. Орг. меры — базовый набор
if not (getattr(isys, 'org_measures', '') or '').strip():
res['org_measures'] = '\n'.join(BASE_ORG_MEASURES)
# 7. Тех. меры — базовые + по СЗИ объекта
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)
return res