Files
dokogen187/dokogen/threat_rules.py
T
prog1764 dadec02665 feat: подбор характеристик объекта КИИ — программа и ИИ
Вкладка «Основное», кнопки под полем типового объекта:
- ⚙️ «Подобрать типовой объект (программа)»: скоринг по совпадению
  значимых слов названия/назначения объекта с перечнем 392 типовых
  отраслевых объектов (Распоряжение 360-р), IDF-веса (редкие слова
  важнее общих) → окно с вариантами, оператор выбирает и применяет
  (заполняет сферу + типовой объект)
- 🤖 «Подобрать характеристики (ИИ)»: DeepSeek предлагает назначение,
  сферу, тип, архитектуру и типовой объект (из перечня 360-р) как
  рекомендацию → окно с полями, оператор подтверждает/правит

Примеры: СКУД → «ИС, АСУ, ИТКС, обеспечивающие контроль и управление
доступом на территорию»; 1С:УПП → «ИС для ведения бухгалтерского учета»
2026-08-04 22:43:04 +04:00

477 lines
25 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
# ---------------------------------------------------------------
# Подбор типового отраслевого объекта КИИ по характеристикам объекта
# ---------------------------------------------------------------
# Стоп-слова: не несут смысла при сопоставлении названия объекта и типового объекта
STOPWORDS = {
'информационные', 'информационно', 'телекоммуникационные', 'автоматизированные',
'системы', 'система', 'сети', 'сеть', 'обеспечивающие', 'обеспечивающих',
'предназначенные', 'предназначенных', 'управление', 'контроль', 'мониторинг',
'объекта', 'объектов', 'объект', 'объекты', 'кии', 'рф', 'российской',
'федерации', 'исполнительной', 'власти', 'органов', 'организаций', 'организации',
'подведомственных', 'функций', 'функции', 'осуществление', 'полномочий',
'обеспечение', 'процессов', 'процесса', 'процессы', 'данных', 'информации',
'сбор', 'хранение', 'обработка', 'представление', 'использование', 'передача',
'автоматизации', 'средств', 'программных', 'программно', 'аппаратных',
'технических', 'деятельность', 'деятельности', 'осуществляющих',
'размещенные', 'размещенных', 'расположенных', 'используемых', 'используемые',
'проведении', 'проведения', 'выполнении', 'выполнения', 'реализация', 'реализации',
'электронных', 'электронного', 'информационных', 'информационного',
# Общие словоформы, встречающиеся почти у всех типовых объектов:
'контроль', 'контроля', 'контролю', 'контролем', 'контроле',
'управление', 'управления', 'управлению', 'управлением', 'управлении',
'безопасность', 'безопасности', 'безопасностью', 'безопасном',
'обеспечение', 'обеспечения', 'обеспечению', 'обеспечением',
'предназначен', 'предназначена', 'предназначено', 'предназначены',
'предоставление', 'предоставления', 'предоставлению',
'взаимодействие', 'взаимодействия', 'взаимодействию',
'функционирование', 'функционирования', 'функционированию',
'деятельность', 'деятельности', 'деятельностью',
'автоматизирован', 'автоматизированной', 'автоматизированных',
'телекоммуникацион', 'телекоммуникационной', 'телекоммуникационных',
'информационн', 'информационной', 'информационных', 'информационное',
'осуществление', 'осуществления', 'осуществлению',
'для', 'систем', 'системы', 'система', 'ресурсов', 'ресурсы',
'целью', 'цели', 'помощи', 'помощь', 'также', 'или', 'иных', 'иное',
'при', 'на', 'по', 'в', 'с', 'и', 'не', 'то', 'числе',
}
def _tokenize(text):
"""Значимые слова текста (без стоп-слов)."""
import re as _re
words = _re.findall(r'[а-яёa-z0-9]+', (text or '').lower())
return [w for w in words if len(w) > 2 and w not in STOPWORDS]
def _load_all_typical(dict_dir=None):
"""Все типовые отраслевые объекты из typical_industry_objects.json (по сферам)."""
import os as _os
if dict_dir is None:
dict_dir = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'dictionaries')
path = _os.path.join(dict_dir, 'typical_industry_objects.json')
try:
with open(path, encoding='utf-8') as f:
data = json.load(f)
except Exception:
return []
out = []
for sphere, items in data.items():
if sphere == 'Иная сфера деятельности':
continue
for it in items:
out.append({'sphere': sphere, 'num': it.get('num', ''),
'name': it.get('name', '')})
return out
_IDF_CACHE = None
def _idf_weights(cands):
"""IDF-веса слов: чем реже слово в перечне типовых объектов, тем больше вес.
Общие слова («информационные», «управление», «контроль») встречаются почти
у всех 392 объектов и почти ничего не значат; редкие («доступом», «СКУД»,
«сигнализации») — сильно различают объекты.
"""
global _IDF_CACHE
if _IDF_CACHE is not None:
return _IDF_CACHE
import math
df = {}
for c in cands:
for w in set(_tokenize(c['name'])):
df[w] = df.get(w, 0) + 1
n = max(len(cands), 1)
weights = {w: math.log((n + 1) / (df.get(w, 0) + 1)) + 1.0 for w in df}
_IDF_CACHE = weights
return weights
def suggest_typical_object_program(isys, limit=10, sphere=None):
"""Подбор типового отраслевого объекта КИИ по характеристикам объекта.
Логика: значимые слова из названия/назначения/описания объекта КИИ
сопоставляются со значимыми словами названий типовых объектов
(перечень Распоряжения ПРФ № 360-р, 392 объекта). Скоринг по IDF:
редкие слова (например, «доступом», «сигнализации») весят больше общих.
Пример: объект «СКУД» / «контроль и управление доступом» →
«ИС, АСУ, ИТКС, обеспечивающие контроль и управление доступом на территорию».
Возвращает список dict: {sphere, num, name, score} — по убыванию релевантности.
"""
text = ' '.join([
getattr(isys, 'name', '') or '',
getattr(isys, 'purpose', '') or '',
getattr(isys, 'description', '') or '',
])
obj_words = set(_tokenize(text))
if not obj_words:
return []
cands = _load_all_typical()
if sphere:
cands = [c for c in cands if c['sphere'] == sphere]
weights = _idf_weights(cands)
scored = []
for c in cands:
tw = set(_tokenize(c['name']))
common = tw & obj_words
if common:
score = sum(weights.get(w, 1.0) for w in common)
scored.append({'sphere': c['sphere'], 'num': c['num'],
'name': c['name'], 'score': round(score, 2),
'matched': sorted(common)})
scored.sort(key=lambda x: (-x['score'], x['name']))
return scored[:limit]