# -*- 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 [] # Фильтр по сфере (важно: не подбирать ОПК-объекту сферу из здравоохранения): # 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