- угрозы ФСТЭК теперь с кодами: «УБИ.001 Угроза …» (парсинг кода из БДУ, 227 шт) - threat_rules.py — логика программы: по характеристикам объекта (тип ИС/АСУ/ИТКС, интернет да/нет, нарушитель внутр./внешн.) фильтрует перечень по ключевым словам - verify.py: suggest_threats_ai — ИИ предлагает актуальные угрозы по описанию объекта - вкладка перестроена: компактная 2-колоночная раскладка, панель действий сверху: «⬇️ Загрузить угрозы ФСТЭК» | «⚙️ Подобрать (программа)» | «🤖 Подобрать (ИИ)» | «⚖️ Сравнить» | «🤖 Проверить безопасность через ИИ» - окно выбора из словаря: 720x680 + поле поиска (фильтр по тексту, важно для 227 угроз) - сравнение программа↔ИИ: окно с двумя списками, совпавшие подсвечены зелёным
163 lines
6.3 KiB
Python
163 lines
6.3 KiB
Python
# -*- 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-таблицы БДУ.
|
||
|
||
Строки вида: <tr>...<td>УБИ. 001</td><td>Угроза ...</td>...</tr>
|
||
Возвращает список строк: «УБИ.001 Угроза ...» (код + название).
|
||
"""
|
||
items = []
|
||
if not html:
|
||
return items
|
||
for row in re.findall(r'<tr[^>]*>(.*?)</tr>', html, re.S):
|
||
cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', 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 []
|