597 lines
34 KiB
Python
597 lines
34 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""DokoGen — автоматический подбор угроз безопасности информации (236 приказ).
|
||
|
||
Логика программы: по характеристикам объекта КИИ (тип, сеть, нарушитель)
|
||
подбирает актуальные угрозы из перечня БДУ ФСТЭК (threats_fstec.json).
|
||
Если перечень не скачан — использует базовый threats.json.
|
||
|
||
Правила эвристические; при наличии методического документа (например,
|
||
«Методика оценки угроз безопасности информации» ФСТЭК) — адаптируются под него.
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import re
|
||
|
||
|
||
def _ubi_num(text):
|
||
"""Код УБИ из строки → int: «УБИ.069 Название» → 69. None, если кода нет."""
|
||
m = re.search(r'уби\.?\s*(\d+)', str(text), re.IGNORECASE)
|
||
return int(m.group(1)) if m else None
|
||
|
||
|
||
def sort_threats_by_ubi(items):
|
||
"""Сортировка угроз по возрастанию кода УБИ (УБИ.001, УБИ.002, …).
|
||
|
||
Строки без кода УБИ уходят в конец (по алфавиту).
|
||
"""
|
||
def _key(t):
|
||
n = _ubi_num(t)
|
||
return (0, n) if n is not None else (1, str(t).lower())
|
||
return sorted(items, key=_key)
|
||
|
||
|
||
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 = [
|
||
'вредоносн', 'несанкционированн', 'утечк', 'перехват', 'модификац',
|
||
'уничтожен', 'отказ в обслуживании', 'нсд', 'биос', 'пароль',
|
||
'криптографическ', 'шифрован', 'аутентификац',
|
||
# базовые угрозы локальных сетей / клиент-серверных архитектур
|
||
'доступ', 'данн', 'систем', 'конфигурац', 'целостност', 'информац',
|
||
'программн', 'пользовател', 'авторизац', 'сеанс', 'журнал', 'реестр',
|
||
'файл', 'память', 'буфер', 'ресурс', 'сервер', 'клиент', 'учётн',
|
||
'учетн', 'идентификац', 'привилег', 'средств', 'сетев', 'локальн',
|
||
]
|
||
|
||
# Технологии, которые по умолчанию НЕ используются на объекте.
|
||
# Если в характеристиках объекта есть явные упоминания — группа возвращается.
|
||
TECH_GROUPS = {
|
||
'виртуализац': ['виртуал', 'гипервизор', 'контейнер'],
|
||
'облако': ['облачн', 'облак'],
|
||
'мобильн': ['мобильн', 'смартфон', 'планшет'],
|
||
'грид': ['грид', 'суперкомпьютер'],
|
||
'большие данные': ['больших данных', 'хранилищ'],
|
||
'беспроводн': ['беспроводн', 'wi-fi', 'wifi'],
|
||
'smart-карты': ['smart-карт', 'смарт-карт'],
|
||
'машинное обучение': ['машинного обучения', 'искусственного интеллекта', 'нейросет'],
|
||
}
|
||
|
||
# Объект имеет подключение к сети Интернет / сеть электросвязи
|
||
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)
|
||
|
||
# --- Технологии, которые явно НЕ используются на объекте ---
|
||
# Если в характеристиках объекта (описание, архитектура, ПО, сфера) нет
|
||
# упоминаний технологии — угрозы этой группы исключаем.
|
||
tech_context = ' '.join([
|
||
str(getattr(isys, 'description', '') or ''),
|
||
str(getattr(isys, 'architecture', '') or ''),
|
||
str(getattr(isys, 'hw_sw', '') or ''),
|
||
str(getattr(isys, 'os_sw', '') or ''),
|
||
str(getattr(isys, 'app_sw', '') or ''),
|
||
str(getattr(isys, 'object_type', '') or ''),
|
||
str(getattr(isys, 'sphere', '') or ''),
|
||
str(getattr(isys, 'net_category', '') or ''),
|
||
str(getattr(isys, 'typical_object', '') or ''),
|
||
]).lower()
|
||
|
||
# --- Слова-исключения (если встретились в названии угрозы — она неактуальна) ---
|
||
exclude = []
|
||
|
||
if not has_internet:
|
||
# Объект изолирован: угрозы, реализуемые только через внешние сети
|
||
exclude += [
|
||
'удалённ', 'удаленн', 'дистанционн', 'веб', 'электронн', 'почт',
|
||
'фишинг', 'беспроводн', 'wi-fi', 'wifi', 'интернет', 'ddos', 'vpn',
|
||
'облачн', 'грид', 'внешн',
|
||
]
|
||
|
||
if is_asu:
|
||
# АСУ: нет BIOS/виртуализации/браузера в привычном понимании
|
||
exclude += ['биос', 'грид', 'гипервизор', 'виртуальн', 'браузер']
|
||
else:
|
||
# Не АСУ — исключаем угрозы АСУ ТП / технологических процессов
|
||
exclude += ['технологическ', 'датчик', 'исполнительн', 'асутп', 'scada',
|
||
'промышленн', 'контроллер', 'числовым программным управлением',
|
||
'программируемых логических']
|
||
|
||
# Технологии, не упомянутые в характеристиках объекта → исключаем
|
||
for tech_name, kws in TECH_GROUPS.items():
|
||
if any(kw in tech_context for kw in kws):
|
||
continue # технология используется — угрозы оставляем
|
||
# Технология не упомянута — исключаем её угрозы
|
||
for kw in kws:
|
||
exclude.append(kw)
|
||
|
||
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))
|
||
|
||
# Сортируем по возрастанию кода УБИ (УБИ.001 → УБИ.211…)
|
||
scored = [t for sc, t in scored if sc > 0]
|
||
result = sort_threats_by_ubi(scored)
|
||
|
||
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 []
|
||
|
||
# Фильтр по сфере (важно: не подбирать ОПК-объекту сферу из здравоохранения):
|
||
# 1) сфера, переданная явно (выбрана в комбобоксе);
|
||
# 2) сфера, уже заполненная у объекта (isys.sphere);
|
||
# 3) автодетект по названию/назначению/описанию;
|
||
# 4) иначе — все сферы (подбор без фильтра).
|
||
used_sphere = sphere
|
||
if not used_sphere:
|
||
used_sphere = (getattr(isys, 'sphere', '') or '').strip() or None
|
||
if not used_sphere:
|
||
used_sphere = detect_sphere(isys)
|
||
|
||
cands = _load_all_typical()
|
||
if used_sphere:
|
||
cands = [c for c in cands if c['sphere'] == used_sphere]
|
||
if not cands:
|
||
# Сфера определена, но объектов нет — не расширяем на другие сферы,
|
||
# возвращаем пусто (оператор выберет сферу вручную)
|
||
return []
|
||
|
||
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]
|
||
|
||
|
||
# ---------------------------------------------------------------
|
||
# Автоопределение сферы деятельности объекта по ключевым словам
|
||
# ---------------------------------------------------------------
|
||
SPHERE_KEYWORDS = [
|
||
('Здравоохранение', ['медицин', 'здрав', 'поликлиник', 'больниц', 'лечеб', 'клиник', 'фармацевт', 'аптек', 'пациент', 'врач', 'диагностик', 'лабораторн', 'медпомощ']),
|
||
('Наука', ['научн', 'исследован', 'разработк', 'лаборатор', 'академи', 'институт', 'ниокр', 'наука']),
|
||
('Транспорт', ['транспорт', 'перевоз', 'автомобил', 'железнодорожн', 'авиа', 'морск', 'речн', 'метрополитен', 'вокзал', 'логистик', 'движени']),
|
||
('Связь', ['связ', 'телекоммуникац', 'оператор связи', 'интернет-провайдер', 'передач данных', 'сети связи', 'телефон']),
|
||
('Энергетика', ['энерг', 'электро', 'теплоснабж', 'электроснабж', 'генерац', 'подстанц', 'котельн', 'энергосистем']),
|
||
('Государственная регистрация прав', ['регистрац', 'кадастр', 'недвижим', 'реестр']),
|
||
('Банковская сфера и финансовый рынок', ['банк', 'финанс', 'кредит', 'платеж', 'расчет', 'страхов', 'клирин', 'депозитар', 'пенсионн', 'фондов']),
|
||
('Топливно-энергетический комплекс', ['нефт', 'газ', 'топлив', 'трубопровод', 'месторожден', 'переработк', 'уголь', 'нефте']),
|
||
('Атомная энергетика', ['атомн', 'ядерн', 'радиацион', 'аэс', 'реактор', 'атом']),
|
||
('Оборонная промышленность', ['оборон', 'военн', 'вооружен', 'оружи', 'оборонно', 'опк', 'госзаказ', 'госконтракт', 'боеприпас', 'ввт', 'минобороны']),
|
||
('Ракетно-космическая промышленность', ['ракет', 'космическ', 'спутник', 'космодром', 'орбит', 'космос', 'пуск']),
|
||
('Горнодобывающая промышленность', ['горн', 'добыч', 'шахт', 'рудник', 'карьер', 'месторожден']),
|
||
('Металлургическая промышленность', ['металлург', 'прокат', 'стал', 'чугун', 'цветн', 'сплав']),
|
||
('Химическая промышленность', ['химическ', 'хим', 'нефтехим', 'взрывчат', 'кислот', 'реактив', 'удобрен']),
|
||
]
|
||
|
||
|
||
def detect_sphere(isys):
|
||
"""Определяет сферу деятельности объекта КИИ по названию/назначению/описанию.
|
||
|
||
Возвращает название сферы или None, если определить не удалось.
|
||
"""
|
||
text = ' '.join([
|
||
getattr(isys, 'name', '') or '',
|
||
getattr(isys, 'purpose', '') or '',
|
||
getattr(isys, 'description', '') or '',
|
||
]).lower()
|
||
best = None
|
||
best_score = 0
|
||
for sphere, keywords in SPHERE_KEYWORDS:
|
||
score = sum(1 for kw in keywords if kw in text)
|
||
if score > best_score:
|
||
best = sphere
|
||
best_score = score
|
||
return best if best_score > 0 else None
|