|
|
@@ -0,0 +1,341 @@
|
|
|
+import customtkinter as ctk
|
|
|
+from PIL import Image, ImageDraw, ImageFont
|
|
|
+import os
|
|
|
+import random # // НОВОЕ: Импортируем модуль для генерации случайных чисел и перемешивания
|
|
|
+
|
|
|
+# Настройка темы в стиле «Терминал»
|
|
|
+ctk.set_appearance_mode("Dark")
|
|
|
+
|
|
|
+BG_COLOR = "#050505"
|
|
|
+FRAME_COLOR = "#0D0F12"
|
|
|
+TEXT_COLOR = "#E0E0E0"
|
|
|
+MATRIX_GREEN = "#00FF41"
|
|
|
+DARK_GREEN = "#008F11"
|
|
|
+HOVER_GREEN = "#00FF41"
|
|
|
+DANGER_RED = "#FF003C"
|
|
|
+FONT_FAMILY = "Consolas" # Хорошо подходит для русского терминала
|
|
|
+
|
|
|
+class SecurityQuizApp(ctk.CTk):
|
|
|
+ def __init__(self):
|
|
|
+ super().__init__()
|
|
|
+ self.title("ТЕРМИНАЛ ЦЕНТРА БЕЗОПАСНОСТИ v3.0")
|
|
|
+ self.geometry("1000x800")
|
|
|
+ self.configure(fg_color=BG_COLOR)
|
|
|
+ self.resizable(False, False)
|
|
|
+
|
|
|
+ self.score = 0
|
|
|
+ self.step_counter = 0
|
|
|
+ self.total_steps = 12 # Увеличено количество шагов
|
|
|
+ self.type_id = None
|
|
|
+
|
|
|
+ self.generate_default_images()
|
|
|
+ self.scenes = self.define_scenes()
|
|
|
+ self.show_boot_screen()
|
|
|
+
|
|
|
+ def generate_default_images(self):
|
|
|
+ """Создание простых заглушек для фото с русскими надписями"""
|
|
|
+ if not os.path.exists("images"):
|
|
|
+ os.makedirs("images")
|
|
|
+
|
|
|
+ img_size = (250, 250)
|
|
|
+ bg_color = (13, 15, 18)
|
|
|
+ accent = (0, 255, 65)
|
|
|
+
|
|
|
+ try:
|
|
|
+ font = ImageFont.load_default()
|
|
|
+ except:
|
|
|
+ font = None
|
|
|
+
|
|
|
+ # Расширенный список изображений для новых сцен
|
|
|
+ configs = [
|
|
|
+ ("intro", "ЗАЩИТА"), ("network", "СЕТЬ"), ("phishing", "ФИШИНГ"),
|
|
|
+ ("physical", "ДОСТУП"), ("anomaly", "АНОМАЛИЯ"), ("malware", "ВИРУС"),
|
|
|
+ ("remote", "УДАЛЕНКА"), ("backup", "БЭКАП"), ("usb", "USB-УГРОЗА"),
|
|
|
+ ("end", "ГОТОВО")
|
|
|
+ ]
|
|
|
+
|
|
|
+ for name, label in configs:
|
|
|
+ path = f"images/{name}.png"
|
|
|
+ if not os.path.exists(path):
|
|
|
+ img = Image.new('RGB', img_size, bg_color)
|
|
|
+ draw = ImageDraw.Draw(img)
|
|
|
+ draw.rectangle([10, 10, 240, 240], outline=accent, width=2)
|
|
|
+ draw.text((70, 110), label, fill=accent, font=font)
|
|
|
+ img.save(path)
|
|
|
+
|
|
|
+ def define_scenes(self):
|
|
|
+ scenes = {
|
|
|
+ "scene_1": {
|
|
|
+ "title": "ВВОДНЫЙ ИНСТРУКТАЖ",
|
|
|
+ "text": "Вы — старший инженер по кибербезопасности на химическом заводе. Ваша задача — обеспечить защиту систем управления (АСУ ТП). Начинается ваша смена.",
|
|
|
+ "image": "images/intro.png",
|
|
|
+ "actions": [{"text": ">>> АВТОРИЗОВАТЬСЯ И НАЧАТЬ РАБОТУ", "next": "scene_2"}],
|
|
|
+ },
|
|
|
+ "scene_2": {
|
|
|
+ "title": "СЕТЕВАЯ ТОПОЛОГИЯ",
|
|
|
+ "text": "Вы обнаружили, что контроллер (ПЛК), управляющий давлением в реакторе, находится в одной сети с офисными компьютерами бухгалтерии.",
|
|
|
+ "image": "images/network.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> НАСТРОИТЬ МЕЖСЕТЕВОЙ ЭКРАН (FIREWALL) МЕЖДУ СЕТЯМИ", "next": "scene_3", "points": 10},
|
|
|
+ {"text": ">>> ИЗОЛИРОВАТЬ ПЛК В ОТДЕЛЬНУЮ ПОДСЕТЬ (VLAN)", "next": "scene_3", "points": 8},
|
|
|
+ {"text": ">>> ОСТАВИТЬ КАК ЕСТЬ (НАСТРОИТЬ ТОЛЬКО ПАРОЛЬ)", "next": "scene_3", "points": 0},
|
|
|
+ {"text": ">>> ФИЗИЧЕСКИ ОТКЛЮЧИТЬ ПЛК ОТ СЕТИ", "next": "scene_3", "points": -5}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_3": {
|
|
|
+ "title": "СОЦИАЛЬНАЯ ИНЖЕНЕРИЯ",
|
|
|
+ "text": "Оператор получил письмо: 'Срочно! Обновите экран управления. Запустите файл во вложении'. Адрес отправителя кажется подозрительным.",
|
|
|
+ "image": "images/phishing.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ПЕРЕДАТЬ ПИСЬМО В ОТДЕЛ БЕЗОПАСНОСТИ НА АНАЛИЗ", "next": "scene_4", "points": 10},
|
|
|
+ {"text": ">>> ПРОСТО УДАЛИТЬ ПИСЬМО И ПРЕДУПРЕДИТЬ ОПЕРАТОРА", "next": "scene_4", "points": 5},
|
|
|
+ {"text": ">>> ОТВЕТИТЬ ОТПРАВИТЕЛЮ И СПРОСИТЬ, КТО ОН", "next": "scene_4", "points": -5},
|
|
|
+ {"text": ">>> РАЗРЕШИТЬ ЗАПУСК (ВДРУГ ЭТО ВАЖНО)", "next": "scene_4", "points": -15}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_4": {
|
|
|
+ "title": "ФИЗИЧЕСКИЙ ДОСТУП",
|
|
|
+ "text": "В цеху вы видите человека с ноутбуком, который подключился напрямую к серверу управления (СКАДА). На нем нет бейджа.",
|
|
|
+ "image": "images/physical.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ПОТРЕБОВАТЬ ДОКУМЕНТЫ И НАРЯД-ДОПУСК НА РАБОТЫ", "next": "scene_5", "points": 10},
|
|
|
+ {"text": ">>> НЕМЕДЛЕННО ВЫЗВАТЬ ОХРАНУ И ЗАБЛОКИРОВАТЬ ВЫХОД", "next": "scene_5", "points": 8},
|
|
|
+ {"text": ">>> ВЫДЕРНУТЬ ЕГО КАБЕЛЬ ИЗ СЕРВЕРА", "next": "scene_5", "points": -5},
|
|
|
+ {"text": ">>> ВЕЖЛИВО СПРОСИТЬ, ЧЕМ ОН ТУТ ЗАНИМАЕТСЯ", "next": "scene_5", "points": 0}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_5": {
|
|
|
+ "title": "БЕЗОПАСНОСТЬ ПАРОЛЕЙ",
|
|
|
+ "text": "Выяснилось, что на промышленных коммутаторах стоит заводской пароль 'admin:admin'. Оборудование работает непрерывно.",
|
|
|
+ "image": None,
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ЗАПЛАНИРОВАТЬ СМЕНУ ПАРОЛЕЙ В БЛИЖАЙШЕЕ ТЕХ. ОКНО", "next": "scene_6", "points": 10},
|
|
|
+ {"text": ">>> ПРИНУДИТЕЛЬНО СМЕНИТЬ ВСЕ ПАРОЛИ ПРЯМО СЕЙЧАС", "next": "scene_6", "points": 5},
|
|
|
+ {"text": ">>> НАСТРОИТЬ ФИЛЬТРАЦИЮ ПО IP ВМЕСТО СМЕНЫ ПАРОЛЯ", "next": "scene_6", "points": 2},
|
|
|
+ {"text": ">>> ОСТАВИТЬ ДО СЛЕДУЮЩЕГО ГОДА", "next": "scene_6", "points": -10}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_6": {
|
|
|
+ "title": "ЦЕПОЧКА ПОСТАВОК",
|
|
|
+ "text": "Подрядчик прислал новую версию программы для контроллеров. Как вы поступите перед установкой?",
|
|
|
+ "image": None,
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ПРОВЕРИТЬ ЦИФРОВУЮ ПОДПИСЬ И ПРОТЕСТИРОВАТЬ НА СТЕНДЕ", "next": "scene_7", "points": 10},
|
|
|
+ {"text": ">>> ПРОВЕРИТЬ ФАЙЛ АНТИВИРУСОМ НА СВОЕМ ПК", "next": "scene_7", "points": 5},
|
|
|
+ {"text": ">>> СРАЗУ УСТАНОВИТЬ, ТАК КАК ПОДРЯДЧИК НАДЕЖНЫЙ", "next": "scene_7", "points": -10},
|
|
|
+ {"text": ">>> ЗАПРОСИТЬ У НИХ ИСХОДНЫЙ КОД ДЛЯ ПРОВЕРКИ", "next": "scene_7", "points": 2}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_7": {
|
|
|
+ "title": "АНОМАЛИИ В ДАННЫХ",
|
|
|
+ "text": "На экране СКАДА температура скачет от -100 до +500 градусов за секунду. Датчики явно врут или система взломана.",
|
|
|
+ "image": "images/anomaly.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> СВЕРИТЬ ДАННЫЕ С РЕЗЕРВНЫМИ АНАЛОГОВЫМИ ПРИБОРАМИ", "next": "scene_8", "points": 10},
|
|
|
+ {"text": ">>> ПЕРЕЙТИ НА РУЧНОЕ УПРАВЛЕНИЕ КЛАПАНАМИ", "next": "scene_8", "points": 5},
|
|
|
+ {"text": ">>> ПЕРЕЗАГРУЗИТЬ СЕРВЕР СКАДА", "next": "scene_8", "points": -5},
|
|
|
+ {"text": ">>> ОТКЛЮЧИТЬ ПИТАНИЕ РЕАКТОРА", "next": "scene_8", "points": -15}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_8": {
|
|
|
+ "title": "УДАЛЕННЫЙ ДОСТУП",
|
|
|
+ "text": "Производителю насоса нужен срочный удаленный доступ для диагностики вибраций оборудования.",
|
|
|
+ "image": "images/remote.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ВЫДАТЬ ВРЕМЕННЫЙ VPN С 2FA ПОД КОНТРОЛЕМ ПЕРСОНАЛА", "next": "scene_9", "points": 10},
|
|
|
+ {"text": ">>> ПОДНЯТЬ СЕАНС TEAMVIEWER НА СТАНЦИИ ОПЕРАТОРА", "next": "scene_9", "points": -10},
|
|
|
+ {"text": ">>> ОТКРЫТЬ RDP ДОСТУП ИЗ ИНТЕРНЕТА НА ОДИН ЧАС", "next": "scene_9", "points": -15},
|
|
|
+ {"text": ">>> ОТКАЗАТЬ В ДОСТУПЕ. ПУСТЬ ПРИЕЗЖАЮТ ЛИЧНО", "next": "scene_9", "points": 0}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_9": {
|
|
|
+ "title": "ОБНОВЛЕНИЕ СИСТЕМ",
|
|
|
+ "text": "Вышло критическое обновление ОС Windows для серверов управления. Уязвимость активно эксплуатируется.",
|
|
|
+ "image": None,
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> УСТАНОВИТЬ В ТЕСТОВОЙ ЗОНЕ, ЗАТЕМ СОГЛАСОВАТЬ ПРОСТОЙ", "next": "scene_10", "points": 10},
|
|
|
+ {"text": ">>> СОГЛАСОВАТЬ ПРОСТОЙ И НАКАТИТЬ ОБНОВЛЕНИЕ СРАЗУ", "next": "scene_10", "points": 5},
|
|
|
+ {"text": ">>> ВКЛЮЧИТЬ АВТОМАТИЧЕСКОЕ ОБНОВЛЕНИЕ WINDOWS", "next": "scene_10", "points": -15},
|
|
|
+ {"text": ">>> ИГНОРИРОВАТЬ. АСУ ТП ВООБЩЕ НЕЛЬЗЯ ОБНОВЛЯТЬ", "next": "scene_10", "points": -5}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_10": {
|
|
|
+ "title": "НАХОДКА В ЦЕХУ",
|
|
|
+ "text": "Оператор нашел красивую флешку (USB) возле проходной и хочет вставить ее в АРМ, чтобы найти владельца.",
|
|
|
+ "image": "images/usb.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ИЗЪЯТЬ ФЛЕШКУ И ОТДАТЬ В ИНФОРМАЦИОННУЮ БЕЗОПАСНОСТЬ", "next": "scene_11", "points": 10},
|
|
|
+ {"text": ">>> ЗАПРЕТИТЬ ВСТАВЛЯТЬ. СКАЗАТЬ, ЧТОБЫ ВЫКИНУЛ", "next": "scene_11", "points": 5},
|
|
|
+ {"text": ">>> ВСТАВИТЬ В ОТДЕЛЬНЫЙ НОУТБУК БЕЗ СЕТИ ДЛЯ ПРОВЕРКИ", "next": "scene_11", "points": 8},
|
|
|
+ {"text": ">>> РАЗРЕШИТЬ, НО ТОЛЬКО С ВКЛЮЧЕННЫМ АНТИВИРУСОМ", "next": "scene_11", "points": -15}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_11": {
|
|
|
+ "title": "ВИРУСНАЯ АТАКА",
|
|
|
+ "text": "Несмотря на защиту, антивирус на станции АРМ сообщает о заражении шифровальщиком. Файлы начали блокироваться.",
|
|
|
+ "image": "images/malware.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ФИЗИЧЕСКИ ОТКЛЮЧИТЬ ПАТЧ-КОРД ОТ СЕТИ (ИЗОЛИРОВАТЬ)", "next": "scene_12", "points": 10},
|
|
|
+ {"text": ">>> СРОЧНО ВЫКЛЮЧИТЬ ПИТАНИЕ СТАНЦИИ", "next": "scene_12", "points": 5},
|
|
|
+ {"text": ">>> ЗАПУСТИТЬ ПОЛНОЕ СКАНИРОВАНИЕ АНТИВИРУСОМ", "next": "scene_12", "points": -10},
|
|
|
+ {"text": ">>> ПОПЫТАТЬСЯ НАЙТИ ПРОЦЕСС В ДИСПЕТЧЕРЕ ЗАДАЧ", "next": "scene_12", "points": -5}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_12": {
|
|
|
+ "title": "РЕЗЕРВНОЕ КОПИРОВАНИЕ",
|
|
|
+ "text": "Шифровальщик уничтожил базу данных истории (Historian). Злоумышленники требуют выкуп.",
|
|
|
+ "image": "images/backup.png",
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ВОССТАНОВИТЬ ИЗ ОФФЛАЙН-БЭКАПА НА НОВОМ СЕРВЕРЕ", "next": "scene_13", "points": 10},
|
|
|
+ {"text": ">>> ПЕРЕУСТАНОВИТЬ СИСТЕМУ С НУЛЯ (ПОТЕРЯТЬ ИСТОРИЮ)", "next": "scene_13", "points": 0},
|
|
|
+ {"text": ">>> ПОПРОБОВАТЬ РАСШИФРОВАТЬ ФАЙЛЫ САМОСТОЯТЕЛЬНО", "next": "scene_13", "points": -5},
|
|
|
+ {"text": ">>> ЗАПЛАТИТЬ ВЫКУП", "next": "scene_13", "points": -20}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "scene_13": {
|
|
|
+ "title": "ФИНАЛЬНЫЙ ЭКЗАМЕН",
|
|
|
+ "text": "Какой международный стандарт является основным ориентиром для кибербезопасности АСУ ТП?",
|
|
|
+ "image": None,
|
|
|
+ "actions": [
|
|
|
+ {"text": ">>> ISA/IEC 62443", "next": "end", "points": 10},
|
|
|
+ {"text": ">>> ISO/IEC 27001", "next": "end", "points": 5},
|
|
|
+ {"text": ">>> ГОСТ Р 50922-2006", "next": "end", "points": 0},
|
|
|
+ {"text": ">>> PCI-DSS", "next": "end", "points": -5}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "end": {
|
|
|
+ "title": "ИТОГИ СМЕНЫ",
|
|
|
+ "text": "Ваша смена в центре мониторинга безопасности завершена. Система проанализировала все принятые вами решения.",
|
|
|
+ "image": "images/end.png",
|
|
|
+ "actions": [{"text": ">>> СГЕНЕРИРОВАТЬ ОТЧЕТ И ВЫЙТИ", "next": None}],
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ # // НОВОЕ: Перемешиваем ответы для каждой сцены
|
|
|
+ for scene_key, scene_data in scenes.items():
|
|
|
+ if "actions" in scene_data:
|
|
|
+ random.shuffle(scene_data["actions"])
|
|
|
+
|
|
|
+ return scenes
|
|
|
+
|
|
|
+ def show_boot_screen(self):
|
|
|
+ self.clear_frame()
|
|
|
+ boot_frame = ctk.CTkFrame(self, fg_color=BG_COLOR)
|
|
|
+ boot_frame.pack(fill="both", expand=True)
|
|
|
+ self.boot_label = ctk.CTkLabel(boot_frame, text="", font=(FONT_FAMILY, 16), text_color=MATRIX_GREEN, justify="left", anchor="sw")
|
|
|
+ self.boot_label.pack(side="bottom", fill="both", expand=True, padx=20, pady=20)
|
|
|
+
|
|
|
+ lines = [
|
|
|
+ "ИНИЦИАЛИЗАЦИЯ СИСТЕМЫ...",
|
|
|
+ "ЗАГРУЗКА БАЗ ДАННЫХ И СЦЕНАРИЕВ...",
|
|
|
+ "ПОДКЛЮЧЕНИЕ К СЕТИ ЗАВОДА [ОК]",
|
|
|
+ "ПРОВЕРКА ДАННЫХ СТАРШЕГО ИНЖЕНЕРА...",
|
|
|
+ "РАСШИФРОВКА ПРОТОКОЛОВ...",
|
|
|
+ "ДОСТУП РАЗРЕШЕН."
|
|
|
+ ]
|
|
|
+ self.boot_seq(lines, 0)
|
|
|
+
|
|
|
+ def boot_seq(self, lines, idx):
|
|
|
+ if idx < len(lines):
|
|
|
+ current_text = self.boot_label.cget("text")
|
|
|
+ self.boot_label.configure(text=current_text + lines[idx] + "\n")
|
|
|
+ self.after(300, self.boot_seq, lines, idx+1)
|
|
|
+ else:
|
|
|
+ self.after(500, lambda: self.load_scene("scene_1"))
|
|
|
+
|
|
|
+ def clear_frame(self):
|
|
|
+ for widget in self.winfo_children():
|
|
|
+ widget.destroy()
|
|
|
+
|
|
|
+ def type_text_effect(self, text, index=0):
|
|
|
+ if index < len(text):
|
|
|
+ self.text_label.configure(text=text[:index+1] + "█")
|
|
|
+ self.type_id = self.after(15, self.type_text_effect, text, index+1)
|
|
|
+ else:
|
|
|
+ self.text_label.configure(text=text)
|
|
|
+
|
|
|
+ def load_scene(self, scene_id):
|
|
|
+ if self.type_id: self.after_cancel(self.type_id)
|
|
|
+ self.clear_frame()
|
|
|
+
|
|
|
+ if scene_id not in self.scenes: return
|
|
|
+ scene = self.scenes[scene_id]
|
|
|
+ if scene_id != "end" and scene_id != "scene_1":
|
|
|
+ self.step_counter += 1
|
|
|
+
|
|
|
+ # Индикатор прогресса
|
|
|
+ if scene_id not in ["end", "scene_1"]:
|
|
|
+ prog = ctk.CTkProgressBar(self, progress_color=MATRIX_GREEN, fg_color=FRAME_COLOR, height=10)
|
|
|
+ prog.pack(fill="x", padx=40, pady=(20, 0))
|
|
|
+ prog.set(self.step_counter / self.total_steps)
|
|
|
+
|
|
|
+ main_frame = ctk.CTkFrame(self, fg_color=FRAME_COLOR, border_width=1, border_color=MATRIX_GREEN)
|
|
|
+ main_frame.pack(pady=20, padx=40, fill="both", expand=True)
|
|
|
+
|
|
|
+ ctk.CTkLabel(main_frame, text=f"// {scene['title']}", font=(FONT_FAMILY, 28, "bold"), text_color=MATRIX_GREEN).pack(pady=20, padx=30, anchor="w")
|
|
|
+
|
|
|
+ content = ctk.CTkFrame(main_frame, fg_color="transparent")
|
|
|
+ content.pack(pady=10, padx=30, fill="both", expand=True)
|
|
|
+
|
|
|
+ self.text_label = ctk.CTkLabel(content, text="", font=(FONT_FAMILY, 18), text_color=TEXT_COLOR, wraplength=550, justify="left")
|
|
|
+ self.text_label.pack(side="left", fill="both", expand=True)
|
|
|
+ self.type_text_effect(scene["text"])
|
|
|
+
|
|
|
+ if scene.get("image") and os.path.exists(scene["image"]):
|
|
|
+ try:
|
|
|
+ img = ctk.CTkImage(Image.open(scene["image"]), size=(250, 250))
|
|
|
+ ctk.CTkLabel(content, image=img, text="").pack(side="right", padx=10)
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ actions_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
|
|
+ actions_frame.pack(pady=20, padx=30, fill="x")
|
|
|
+
|
|
|
+ for a in scene["actions"]:
|
|
|
+ btn = ctk.CTkButton(actions_frame, text=a["text"], font=(FONT_FAMILY, 15),
|
|
|
+ fg_color=FRAME_COLOR, hover_color=HOVER_GREEN,
|
|
|
+ text_color=MATRIX_GREEN, border_width=1, border_color=MATRIX_GREEN,
|
|
|
+ height=45, anchor="w", command=lambda x=a: self.make_choice(x))
|
|
|
+ btn.pack(fill="x", pady=5)
|
|
|
+
|
|
|
+ if scene_id not in ["end", "scene_1"]:
|
|
|
+ # Ограничиваем визуал очков процентами до 100
|
|
|
+ display_score = min(100, self.score)
|
|
|
+ ctk.CTkLabel(main_frame, text=f"ТЕКУЩАЯ ЗАЩИТА: {display_score}%", font=(FONT_FAMILY, 14), text_color=MATRIX_GREEN).pack(side="bottom", pady=10)
|
|
|
+
|
|
|
+ def make_choice(self, action):
|
|
|
+ # Добавляем очки, не позволяем упасть ниже 0. Максимум ограничен логически в UI, но копим реальные очки.
|
|
|
+ self.score = max(0, self.score + action.get("points", 0))
|
|
|
+ next_s = action.get("next")
|
|
|
+ if next_s:
|
|
|
+ self.load_scene(next_s)
|
|
|
+ else:
|
|
|
+ self.show_end_screen()
|
|
|
+
|
|
|
+ def show_end_screen(self):
|
|
|
+ self.clear_frame()
|
|
|
+ f = ctk.CTkFrame(self, fg_color=FRAME_COLOR, border_width=2, border_color=MATRIX_GREEN)
|
|
|
+ f.pack(pady=60, padx=60, fill="both", expand=True)
|
|
|
+
|
|
|
+ ctk.CTkLabel(f, text="ОТЧЕТ ПО БЕЗОПАСНОСТИ", font=(FONT_FAMILY, 34, "bold"), text_color=MATRIX_GREEN).pack(pady=40)
|
|
|
+
|
|
|
+ # Пересчет финального рейтинга (макс возможных баллов ~120, приводим к 100%)
|
|
|
+ final_percentage = min(100, int((self.score / 120) * 100))
|
|
|
+
|
|
|
+ if final_percentage >= 80:
|
|
|
+ status = "ЗАВОД В ПОЛНОЙ БЕЗОПАСНОСТИ"
|
|
|
+ color = MATRIX_GREEN
|
|
|
+ elif final_percentage >= 50:
|
|
|
+ status = "ЕСТЬ УЯЗВИМОСТИ (ТРЕБУЕТСЯ АУДИТ)"
|
|
|
+ color = "orange"
|
|
|
+ else:
|
|
|
+ status = "КРИТИЧЕСКИЙ ВЗЛОМ СИСТЕМЫ"
|
|
|
+ color = DANGER_RED
|
|
|
+
|
|
|
+ ctk.CTkLabel(f, text=f"СТАТУС: {status}", font=(FONT_FAMILY, 24), text_color=TEXT_COLOR).pack(pady=10)
|
|
|
+ ctk.CTkLabel(f, text=f"РЕЙТИНГ ЭФФЕКТИВНОСТИ: {final_percentage}%", font=(FONT_FAMILY, 30, "bold"), text_color=color).pack(pady=20)
|
|
|
+
|
|
|
+ ctk.CTkButton(f, text=">>> ВЫХОД", fg_color=BG_COLOR, border_width=1, border_color=DANGER_RED, text_color=DANGER_RED, command=self.destroy).pack(pady=40)
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ SecurityQuizApp().mainloop()
|