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()