Проблемы: 1. Подбор программой возвращал 193 из 227 угроз (весь перечень) — теперь скоринг по ключевым словам объекта (тип, интернет, нарушитель, сеть), отсев нерелевантных: 72-82 угрозы вместо 193 2. Сравнение программа↔ИИ не подсвечивало совпадения: ИИ возвращает «1. УБИ.001 …» / с лишними пробелами — сравнение шло по точной строке. Теперь ключ сравнения — код УБИ (regex), иначе нормализованный текст; добавлена статистика «Совпало / только программа / только ИИ»
208 lines
9.2 KiB
Python
208 lines
9.2 KiB
Python
# -*- 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
|