# -*- coding: utf-8 -*- """DokoGen — загрузка перечня угроз безопасности информации с сайта ФСТЭК (БДУ). Скачивает https://bdu.fstec.ru/threat (таблица угроз, с пагинацией), парсит названия угроз и сохраняет в dictionaries/threats_fstec.json. Рядом хранится дата актуализации (threats_fstec_date.txt). """ import os import re import json import ssl import urllib.request import urllib.parse from datetime import datetime BDU_URL = 'https://bdu.fstec.ru/threat' USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' def _ssl_ctx(): """SSL-контекст: сначала обычная проверка, при проблемах — без проверки.""" try: ctx = ssl.create_default_context() return ctx except Exception: pass ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE return ctx def _fetch(url, timeout=30): """GET с User-Agent, возвращает текст или None.""" req = urllib.request.Request(url, headers={'User-Agent': USER_AGENT}) try: with urllib.request.urlopen(req, timeout=timeout, context=_ssl_ctx()) as resp: return resp.read().decode('utf-8', errors='ignore') except Exception: # повтор с отключённой проверкой сертификата (некоторые сети/прокси) try: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp: return resp.read().decode('utf-8', errors='ignore') except Exception: return None def _normalize_ubi(code): """«УБИ. 001» / «УБИ.001» → «УБИ.001» (без пробела после точки).""" m = re.search(r'(УБИ\.?)\s*([\d.]+)', code or '', re.I) if m: return f"{m.group(1).replace('.', '').upper()}.{m.group(2)}" return (code or '').strip() def _parse_threats(html): """Извлекает пары (код УБИ, название) из HTML-таблицы БДУ. Строки вида: ...УБИ. 001Угроза ...... Возвращает список строк: «УБИ.001 Угроза ...» (код + название). """ items = [] if not html: return items for row in re.findall(r']*>(.*?)', html, re.S): cells = re.findall(r']*>(.*?)', row, re.S) cleaned = [re.sub(r'<[^>]+>', '', c).strip() for c in cells] if not cleaned: continue # первая ячейка — код (УБИ.001), вторая — название code = _normalize_ubi(cleaned[0]) if cleaned else '' name = '' for cell in cleaned[1:]: if cell and cell not in ('№', 'Идентификатор', 'Наименование', 'Дата', 'Уровень опасности'): name = cell break if not name: continue if code.startswith('УБИ.'): items.append(f"{code} {name}") else: items.append(name) return items def download_fstec_threats(dict_dir=None, max_pages=8, page_size=100): """Скачивает перечень угроз с bdu.fstec.ru. Возвращает (ok, сообщение, список_угроз). При успехе сохраняет: - dictionaries/threats_fstec.json — список названий угроз - dictionaries/threats_fstec_date.txt — дата актуализации (ГГГГ-ММ-ДД) """ if dict_dir is None: dict_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries') threats = [] for page in range(1, max_pages + 1): url = f"{BDU_URL}?size={page_size}&page={page}" html = _fetch(url) if not html: break names = _parse_threats(html) if not names: break for n in names: if n not in threats: threats.append(n) # если на странице меньше page_size — это последняя if len(names) < page_size: break if not threats: return False, "Не удалось загрузить угрозы с сайта ФСТЭК (bdu.fstec.ru)", [] try: os.makedirs(dict_dir, exist_ok=True) with open(os.path.join(dict_dir, 'threats_fstec.json'), 'w', encoding='utf-8') as f: json.dump(threats, f, ensure_ascii=False, indent=2) today = datetime.now().strftime('%Y-%m-%d') with open(os.path.join(dict_dir, 'threats_fstec_date.txt'), 'w', encoding='utf-8') as f: f.write(today) except Exception as e: return False, f"Ошибка сохранения: {e}", threats return True, f"Загружено угроз: {len(threats)}", threats def get_threats_date(dict_dir=None): """Дата последней актуализации списка угроз (или None).""" if dict_dir is None: dict_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries') path = os.path.join(dict_dir, 'threats_fstec_date.txt') if os.path.exists(path): try: with open(path, encoding='utf-8') as f: return f.read().strip() except Exception: pass return None def load_threats(dict_dir=None): """Загружает список угроз: сначала скачанный с ФСТЭК, иначе базовый threats.json.""" 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: return json.load(f) 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 []