feat: подбор угроз — инвертированная логика (все угрозы минус неактуальные)
- threat_rules: по умолчанию актуальны ВСЕ угрозы БДУ ФСТЭК, исключаются заведомо неактуальные: нет интернета → убраны удалённый доступ/веб/почта/ беспроводные/DDoS/облачные/грид; АСУ → убраны BIOS/грид/виртуализация/браузер (теперь 170-200 угроз на объект вместо 15 «самых похожих») - _has_internet уточнён: «Отсутствует» у оператора / «изолирован» / «не требуется» → внешней сети нет; сеть общего пользования или реальный оператор → есть - verify.suggest_threats_ai: свежий перечень ФСТЭК (227 угроз) передаётся в промт, ИИ выбирает из него, а не из устаревших знаний; + человекочитаемая сводка объекта - ui: кнопки программа/ИИ без лимита 20; после подбора угрозы сохраняются в модель (_collect_is) — при открытии словаря галочки выбранных угроз стоят - models.from_dict: фильтрация неизвестных полей вложенных объектов — старые файлы с удалёнными полями (attacker_characteristics) больше не падают
This commit is contained in:
+6
-2
@@ -157,13 +157,17 @@ class Company:
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "Company":
|
||||
d = dict(data)
|
||||
if "commission" in d and isinstance(d["commission"], list):
|
||||
_cvalid = {f.name for f in CommissionMember.__dataclass_fields__.values()}
|
||||
d["commission"] = [
|
||||
CommissionMember(**c) if isinstance(c, dict) else c
|
||||
CommissionMember(**{k: v for k, v in c.items() if k in _cvalid})
|
||||
if isinstance(c, dict) else c
|
||||
for c in d["commission"]
|
||||
]
|
||||
if "information_systems" in d and isinstance(d["information_systems"], list):
|
||||
_ivalid = {f.name for f in InformationSystem.__dataclass_fields__.values()}
|
||||
d["information_systems"] = [
|
||||
InformationSystem(**isys) if isinstance(isys, dict) else isys
|
||||
InformationSystem(**{k: v for k, v in isys.items() if k in _ivalid})
|
||||
if isinstance(isys, dict) else isys
|
||||
for isys in d["information_systems"]
|
||||
]
|
||||
if "employees_access" in d and not isinstance(d["employees_access"], list):
|
||||
|
||||
+54
-37
@@ -95,16 +95,30 @@ def _has_internet(isys) -> bool:
|
||||
"""Есть ли подключение к сети Интернет / внешним сетям."""
|
||||
if getattr(isys, 'is_internet', False):
|
||||
return True
|
||||
# По описанию / сетям: «отсутствует подключение к сети Интернет» → нет
|
||||
desc = ((getattr(isys, 'description', '') or '') + ' ' +
|
||||
(getattr(isys, 'net_operator', '') or '')).lower()
|
||||
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
|
||||
net_category = (getattr(isys, 'net_category', '') or '').lower()
|
||||
if net_category:
|
||||
if 'не требуется' in desc and ('доменн' in desc or 'интернет' in desc):
|
||||
return False
|
||||
if 'изолирован' in desc:
|
||||
return False
|
||||
|
||||
# Сеть общего пользования — это и есть внешняя сеть (Интернет)
|
||||
if 'общего пользован' in net_category:
|
||||
return True
|
||||
# По умолчанию считаем, что сеть есть (осторожная оценка)
|
||||
return True
|
||||
|
||||
# Указан реальный оператор/провайдер — внешняя связь есть
|
||||
if operator and operator not in ('нет', 'отсутствует', 'не требуется', '-'):
|
||||
return True
|
||||
|
||||
# Консервативно: без явных признаков внешней сети считаем, что её нет
|
||||
return False
|
||||
|
||||
|
||||
def _is_insider(isys) -> bool:
|
||||
@@ -117,8 +131,16 @@ 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=15):
|
||||
"""Подбор угроз по характеристикам объекта КИИ.
|
||||
def suggest_threats_program(isys, threats=None, limit=None):
|
||||
"""Подбор угроз по характеристикам объекта КИИ (инвертированная логика).
|
||||
|
||||
По умолчанию актуальны ВСЕ угрозы из перечня (БДУ ФСТЭК).
|
||||
Исключаются только заведомо неактуальные для данного объекта:
|
||||
- нет сети Интернет → убираем угрозы удалённого доступа, веб, почты,
|
||||
беспроводных сетей, DDoS, облачных и т.п.;
|
||||
- объект типа АСУ → убираем угрозы BIOS, грид-систем, виртуализации
|
||||
(у технологического оборудования их, как правило, нет).
|
||||
Такой подход даёт полный список, а не «15 самых похожих».
|
||||
|
||||
Возвращает список строк (с кодами УБИ, если они есть в перечне).
|
||||
"""
|
||||
@@ -130,39 +152,34 @@ def suggest_threats_program(isys, threats=None, limit=15):
|
||||
has_internet = _has_internet(isys)
|
||||
is_asu = _is_asu(isys)
|
||||
is_network = _is_network(isys)
|
||||
insider = _is_insider(isys)
|
||||
|
||||
# Собираем активные наборы ключевых слов
|
||||
active = list(BASE_KEYWORDS)
|
||||
if has_internet:
|
||||
active += INTERNET_KEYWORDS
|
||||
else:
|
||||
active += ISOLATED_KEYWORDS
|
||||
# --- Слова-исключения (если встретились в названии угрозы — она неактуальна) ---
|
||||
exclude = []
|
||||
|
||||
if not has_internet:
|
||||
# Объект изолирован: угрозы, реализуемые только через внешние сети
|
||||
exclude += [
|
||||
'удалённ', 'удаленн', 'дистанционн', 'веб', 'электронн', 'почт',
|
||||
'фишинг', 'беспроводн', 'wi-fi', 'wifi', 'интернет', 'ddos', 'vpn',
|
||||
'облачн', 'грид', 'внешн',
|
||||
]
|
||||
|
||||
if is_asu:
|
||||
active += ASU_KEYWORDS
|
||||
if is_network:
|
||||
active += NETWORK_KEYWORDS
|
||||
if insider:
|
||||
active += INSIDER_KEYWORDS
|
||||
else:
|
||||
active += EXTERNAL_KEYWORDS
|
||||
# АСУ: нет BIOS/виртуализации/браузера в привычном понимании
|
||||
exclude += ['биос', 'грид', 'гипервизор', 'виртуальн', 'браузер']
|
||||
|
||||
scored = []
|
||||
if not is_network and not is_asu:
|
||||
# Обычная ИС: неактуальны чисто сетевые/телеком-угрозы без указания
|
||||
# — оставляем всё, кроме специфики каналов связи, если их нет
|
||||
pass
|
||||
|
||||
result = []
|
||||
for t in threats:
|
||||
tl = str(t).lower()
|
||||
s = _score(tl, active)
|
||||
if s > 0:
|
||||
scored.append((s, t))
|
||||
|
||||
# Сортировка: больше совпадений — выше; убираем дубликаты, сохраняя порядок
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
result = []
|
||||
seen = set()
|
||||
for s, t in scored:
|
||||
if t in seen:
|
||||
if any(kw in tl for kw in exclude):
|
||||
continue
|
||||
seen.add(t)
|
||||
result.append(t)
|
||||
if len(result) >= limit:
|
||||
break
|
||||
|
||||
if limit and len(result) > limit:
|
||||
result = result[:limit]
|
||||
return result
|
||||
|
||||
+19
-6
@@ -1900,7 +1900,7 @@ class DokoGenApp:
|
||||
isys = self.model.information_systems[self._current_is_idx]
|
||||
try:
|
||||
from .threat_rules import suggest_threats_program
|
||||
threats = suggest_threats_program(isys, limit=20)
|
||||
threats = suggest_threats_program(isys)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", str(e))
|
||||
return
|
||||
@@ -1912,6 +1912,7 @@ class DokoGenApp:
|
||||
lb.delete(0, tk.END)
|
||||
for t in threats:
|
||||
lb.insert(tk.END, t)
|
||||
self._collect_is() # сохранить в модель — галочки в словаре будут стоять
|
||||
self.log(f"⚙️ Угрозы подобраны программой: {len(threats)} шт.")
|
||||
|
||||
def _suggest_threats_ai_click(self):
|
||||
@@ -1925,7 +1926,8 @@ class DokoGenApp:
|
||||
return
|
||||
self._collect_is()
|
||||
isys = self.model.information_systems[self._current_is_idx]
|
||||
from .threat_rules import _has_internet
|
||||
from .threat_rules import _has_internet, load_threats
|
||||
has_internet = _has_internet(isys)
|
||||
data = {
|
||||
'name': isys.name or '',
|
||||
'description': isys.description or '',
|
||||
@@ -1934,16 +1936,26 @@ class DokoGenApp:
|
||||
'architecture': isys.architecture or '',
|
||||
'net_category': isys.net_category or '',
|
||||
'attacker_category': isys.attacker_category or '',
|
||||
'has_internet': _has_internet(isys),
|
||||
'has_internet': has_internet,
|
||||
'summary': (
|
||||
f"Объект КИИ: {isys.name or '—'}. "
|
||||
f"Тип: {isys.object_type or '—'}. "
|
||||
f"Сфера: {isys.sphere or '—'}. "
|
||||
f"Выход в интернет: {'есть' if has_internet else 'нет'}. "
|
||||
f"Категория нарушителя: {isys.attacker_category or '—'}. "
|
||||
f"Сети: {isys.net_category or '—'}"
|
||||
),
|
||||
}
|
||||
self.log(f"🤖 Запрос к ИИ: подбор угроз для «{isys.name}»")
|
||||
threats_full = load_threats() # свежий перечень с сайта ФСТЭК
|
||||
self.log(f"🤖 Запрос к ИИ: подбор угроз для «{isys.name}» (перечень: {len(threats_full)} угроз)")
|
||||
self._verify_log("Подбор угроз через ИИ…")
|
||||
|
||||
def _run():
|
||||
try:
|
||||
from .verify import suggest_threats_ai
|
||||
threats, err = suggest_threats_ai(
|
||||
data, settings['ai_api_key'], settings.get('ai_model'), settings.get('ai_base_url'))
|
||||
data, settings['ai_api_key'], settings.get('ai_model'),
|
||||
settings.get('ai_base_url'), threats=threats_full)
|
||||
self.root.after(0, lambda: self._suggest_threats_ai_done(threats, err))
|
||||
except Exception as e:
|
||||
self.root.after(0, lambda: self._suggest_threats_ai_done(None, str(e)))
|
||||
@@ -1962,6 +1974,7 @@ class DokoGenApp:
|
||||
lb.delete(0, tk.END)
|
||||
for t in threats:
|
||||
lb.insert(tk.END, t)
|
||||
self._collect_is() # сохранить в модель — галочки в словаре будут стоять
|
||||
self.log(f"🤖 ИИ подобрал угроз: {len(threats)}")
|
||||
self._verify_log(f"🤖 ИИ: {len(threats)} угроз")
|
||||
|
||||
@@ -1973,7 +1986,7 @@ class DokoGenApp:
|
||||
self._collect_is()
|
||||
isys = self.model.information_systems[self._current_is_idx]
|
||||
from .threat_rules import suggest_threats_program
|
||||
program = suggest_threats_program(isys, limit=20)
|
||||
program = suggest_threats_program(isys)
|
||||
current = [str(self.is_threats_listbox.get(i)) for i in range(self.is_threats_listbox.size())]
|
||||
if not current:
|
||||
messagebox.showinfo("Сравнение", "Сначала подберите угрозы (программой или ИИ).")
|
||||
|
||||
+29
-13
@@ -474,33 +474,49 @@ def compare_company_values(old_data, new_data):
|
||||
return diffs
|
||||
|
||||
|
||||
def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=None):
|
||||
def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=None,
|
||||
threats=None):
|
||||
"""ИИ предлагает актуальные угрозы БИ для объекта КИИ по его характеристикам.
|
||||
|
||||
security_data — dict: name, description, object_type, sphere, architecture,
|
||||
attacker_category, has_internet (True/False), etc.
|
||||
threats — актуальный перечень угроз БДУ ФСТЭК (список строк «УБИ.xxx …»).
|
||||
Передаётся в промт целиком, чтобы ИИ выбирал из свежего списка,
|
||||
а не из своего устаревшего.
|
||||
|
||||
Возвращает (список_угроз, ошибка).
|
||||
"""
|
||||
if not api_key:
|
||||
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
|
||||
internet = 'да' if security_data.get('has_internet', True) else 'нет'
|
||||
|
||||
if threats:
|
||||
threat_list_text = "\n".join(f"{i+1}. {t}" for i, t in enumerate(threats))
|
||||
else:
|
||||
threat_list_text = "(перечень не предоставлен — используй знания об актуальном перечне БДУ ФСТЭК)"
|
||||
|
||||
system = (
|
||||
"Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры "
|
||||
"(187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ составь "
|
||||
"перечень АКТУАЛЬНЫХ угроз безопасности информации из БДУ ФСТЭК "
|
||||
"(перечень угроз УБИ.xxx). Верни ТОЛЬКО JSON: "
|
||||
'{"threats": ["УБИ.xxx Название угрозы", ...]} — от 8 до 15 угроз, '
|
||||
"без пояснений и комментариев."
|
||||
"(187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ выбери из "
|
||||
"предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации и верни "
|
||||
"ТОЛЬКО JSON: "
|
||||
'{"threats": ["УБИ.xxx Название угрозы", ...]} — от 10 до 40 угроз, '
|
||||
"без пояснений и комментариев. Не выдумывай угрозы, которых нет в перечне; "
|
||||
"используй полные строки из перечня (код + название) как они есть."
|
||||
)
|
||||
user = (
|
||||
"Характеристики объекта КИИ:\n"
|
||||
f"{json.dumps(security_data, ensure_ascii=False)}\n\n"
|
||||
"Актуальный перечень угроз БДУ ФСТЭК:\n"
|
||||
f"{threat_list_text}"
|
||||
)
|
||||
user = json.dumps(security_data, ensure_ascii=False)
|
||||
js, err = _ai_json(api_key, model, base_url, system, user)
|
||||
if err:
|
||||
return None, err
|
||||
threats = js.get('threats') or []
|
||||
if isinstance(threats, str):
|
||||
threats = [threats]
|
||||
threats = [str(t).strip() for t in threats if str(t).strip()]
|
||||
return threats, None
|
||||
threats_out = js.get('threats') or []
|
||||
if isinstance(threats_out, str):
|
||||
threats_out = [threats_out]
|
||||
threats_out = [str(t).strip() for t in threats_out if str(t).strip()]
|
||||
return threats_out, None
|
||||
|
||||
|
||||
def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=None):
|
||||
|
||||
Reference in New Issue
Block a user