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