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