game.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import customtkinter as ctk
  2. from PIL import Image, ImageDraw, ImageFont
  3. import os
  4. import random # // НОВОЕ: Импортируем модуль для генерации случайных чисел и перемешивания
  5. # Настройка темы в стиле «Терминал»
  6. ctk.set_appearance_mode("Dark")
  7. BG_COLOR = "#050505"
  8. FRAME_COLOR = "#0D0F12"
  9. TEXT_COLOR = "#E0E0E0"
  10. MATRIX_GREEN = "#00FF41"
  11. DARK_GREEN = "#008F11"
  12. HOVER_GREEN = "#00FF41"
  13. DANGER_RED = "#FF003C"
  14. FONT_FAMILY = "Consolas" # Хорошо подходит для русского терминала
  15. class SecurityQuizApp(ctk.CTk):
  16. def __init__(self):
  17. super().__init__()
  18. self.title("ТЕРМИНАЛ ЦЕНТРА БЕЗОПАСНОСТИ v3.0")
  19. self.geometry("1000x800")
  20. self.configure(fg_color=BG_COLOR)
  21. self.resizable(False, False)
  22. self.score = 0
  23. self.step_counter = 0
  24. self.total_steps = 12 # Увеличено количество шагов
  25. self.type_id = None
  26. self.generate_default_images()
  27. self.scenes = self.define_scenes()
  28. self.show_boot_screen()
  29. def generate_default_images(self):
  30. """Создание простых заглушек для фото с русскими надписями"""
  31. if not os.path.exists("images"):
  32. os.makedirs("images")
  33. img_size = (250, 250)
  34. bg_color = (13, 15, 18)
  35. accent = (0, 255, 65)
  36. try:
  37. font = ImageFont.load_default()
  38. except:
  39. font = None
  40. # Расширенный список изображений для новых сцен
  41. configs = [
  42. ("intro", "ЗАЩИТА"), ("network", "СЕТЬ"), ("phishing", "ФИШИНГ"),
  43. ("physical", "ДОСТУП"), ("anomaly", "АНОМАЛИЯ"), ("malware", "ВИРУС"),
  44. ("remote", "УДАЛЕНКА"), ("backup", "БЭКАП"), ("usb", "USB-УГРОЗА"),
  45. ("end", "ГОТОВО")
  46. ]
  47. for name, label in configs:
  48. path = f"images/{name}.png"
  49. if not os.path.exists(path):
  50. img = Image.new('RGB', img_size, bg_color)
  51. draw = ImageDraw.Draw(img)
  52. draw.rectangle([10, 10, 240, 240], outline=accent, width=2)
  53. draw.text((70, 110), label, fill=accent, font=font)
  54. img.save(path)
  55. def define_scenes(self):
  56. scenes = {
  57. "scene_1": {
  58. "title": "ВВОДНЫЙ ИНСТРУКТАЖ",
  59. "text": "Вы — старший инженер по кибербезопасности на химическом заводе. Ваша задача — обеспечить защиту систем управления (АСУ ТП). Начинается ваша смена.",
  60. "image": "images/intro.png",
  61. "actions": [{"text": ">>> АВТОРИЗОВАТЬСЯ И НАЧАТЬ РАБОТУ", "next": "scene_2"}],
  62. },
  63. "scene_2": {
  64. "title": "СЕТЕВАЯ ТОПОЛОГИЯ",
  65. "text": "Вы обнаружили, что контроллер (ПЛК), управляющий давлением в реакторе, находится в одной сети с офисными компьютерами бухгалтерии.",
  66. "image": "images/network.png",
  67. "actions": [
  68. {"text": ">>> НАСТРОИТЬ МЕЖСЕТЕВОЙ ЭКРАН (FIREWALL) МЕЖДУ СЕТЯМИ", "next": "scene_3", "points": 10},
  69. {"text": ">>> ИЗОЛИРОВАТЬ ПЛК В ОТДЕЛЬНУЮ ПОДСЕТЬ (VLAN)", "next": "scene_3", "points": 8},
  70. {"text": ">>> ОСТАВИТЬ КАК ЕСТЬ (НАСТРОИТЬ ТОЛЬКО ПАРОЛЬ)", "next": "scene_3", "points": 0},
  71. {"text": ">>> ФИЗИЧЕСКИ ОТКЛЮЧИТЬ ПЛК ОТ СЕТИ", "next": "scene_3", "points": -5}
  72. ],
  73. },
  74. "scene_3": {
  75. "title": "СОЦИАЛЬНАЯ ИНЖЕНЕРИЯ",
  76. "text": "Оператор получил письмо: 'Срочно! Обновите экран управления. Запустите файл во вложении'. Адрес отправителя кажется подозрительным.",
  77. "image": "images/phishing.png",
  78. "actions": [
  79. {"text": ">>> ПЕРЕДАТЬ ПИСЬМО В ОТДЕЛ БЕЗОПАСНОСТИ НА АНАЛИЗ", "next": "scene_4", "points": 10},
  80. {"text": ">>> ПРОСТО УДАЛИТЬ ПИСЬМО И ПРЕДУПРЕДИТЬ ОПЕРАТОРА", "next": "scene_4", "points": 5},
  81. {"text": ">>> ОТВЕТИТЬ ОТПРАВИТЕЛЮ И СПРОСИТЬ, КТО ОН", "next": "scene_4", "points": -5},
  82. {"text": ">>> РАЗРЕШИТЬ ЗАПУСК (ВДРУГ ЭТО ВАЖНО)", "next": "scene_4", "points": -15}
  83. ],
  84. },
  85. "scene_4": {
  86. "title": "ФИЗИЧЕСКИЙ ДОСТУП",
  87. "text": "В цеху вы видите человека с ноутбуком, который подключился напрямую к серверу управления (СКАДА). На нем нет бейджа.",
  88. "image": "images/physical.png",
  89. "actions": [
  90. {"text": ">>> ПОТРЕБОВАТЬ ДОКУМЕНТЫ И НАРЯД-ДОПУСК НА РАБОТЫ", "next": "scene_5", "points": 10},
  91. {"text": ">>> НЕМЕДЛЕННО ВЫЗВАТЬ ОХРАНУ И ЗАБЛОКИРОВАТЬ ВЫХОД", "next": "scene_5", "points": 8},
  92. {"text": ">>> ВЫДЕРНУТЬ ЕГО КАБЕЛЬ ИЗ СЕРВЕРА", "next": "scene_5", "points": -5},
  93. {"text": ">>> ВЕЖЛИВО СПРОСИТЬ, ЧЕМ ОН ТУТ ЗАНИМАЕТСЯ", "next": "scene_5", "points": 0}
  94. ],
  95. },
  96. "scene_5": {
  97. "title": "БЕЗОПАСНОСТЬ ПАРОЛЕЙ",
  98. "text": "Выяснилось, что на промышленных коммутаторах стоит заводской пароль 'admin:admin'. Оборудование работает непрерывно.",
  99. "image": None,
  100. "actions": [
  101. {"text": ">>> ЗАПЛАНИРОВАТЬ СМЕНУ ПАРОЛЕЙ В БЛИЖАЙШЕЕ ТЕХ. ОКНО", "next": "scene_6", "points": 10},
  102. {"text": ">>> ПРИНУДИТЕЛЬНО СМЕНИТЬ ВСЕ ПАРОЛИ ПРЯМО СЕЙЧАС", "next": "scene_6", "points": 5},
  103. {"text": ">>> НАСТРОИТЬ ФИЛЬТРАЦИЮ ПО IP ВМЕСТО СМЕНЫ ПАРОЛЯ", "next": "scene_6", "points": 2},
  104. {"text": ">>> ОСТАВИТЬ ДО СЛЕДУЮЩЕГО ГОДА", "next": "scene_6", "points": -10}
  105. ],
  106. },
  107. "scene_6": {
  108. "title": "ЦЕПОЧКА ПОСТАВОК",
  109. "text": "Подрядчик прислал новую версию программы для контроллеров. Как вы поступите перед установкой?",
  110. "image": None,
  111. "actions": [
  112. {"text": ">>> ПРОВЕРИТЬ ЦИФРОВУЮ ПОДПИСЬ И ПРОТЕСТИРОВАТЬ НА СТЕНДЕ", "next": "scene_7", "points": 10},
  113. {"text": ">>> ПРОВЕРИТЬ ФАЙЛ АНТИВИРУСОМ НА СВОЕМ ПК", "next": "scene_7", "points": 5},
  114. {"text": ">>> СРАЗУ УСТАНОВИТЬ, ТАК КАК ПОДРЯДЧИК НАДЕЖНЫЙ", "next": "scene_7", "points": -10},
  115. {"text": ">>> ЗАПРОСИТЬ У НИХ ИСХОДНЫЙ КОД ДЛЯ ПРОВЕРКИ", "next": "scene_7", "points": 2}
  116. ],
  117. },
  118. "scene_7": {
  119. "title": "АНОМАЛИИ В ДАННЫХ",
  120. "text": "На экране СКАДА температура скачет от -100 до +500 градусов за секунду. Датчики явно врут или система взломана.",
  121. "image": "images/anomaly.png",
  122. "actions": [
  123. {"text": ">>> СВЕРИТЬ ДАННЫЕ С РЕЗЕРВНЫМИ АНАЛОГОВЫМИ ПРИБОРАМИ", "next": "scene_8", "points": 10},
  124. {"text": ">>> ПЕРЕЙТИ НА РУЧНОЕ УПРАВЛЕНИЕ КЛАПАНАМИ", "next": "scene_8", "points": 5},
  125. {"text": ">>> ПЕРЕЗАГРУЗИТЬ СЕРВЕР СКАДА", "next": "scene_8", "points": -5},
  126. {"text": ">>> ОТКЛЮЧИТЬ ПИТАНИЕ РЕАКТОРА", "next": "scene_8", "points": -15}
  127. ],
  128. },
  129. "scene_8": {
  130. "title": "УДАЛЕННЫЙ ДОСТУП",
  131. "text": "Производителю насоса нужен срочный удаленный доступ для диагностики вибраций оборудования.",
  132. "image": "images/remote.png",
  133. "actions": [
  134. {"text": ">>> ВЫДАТЬ ВРЕМЕННЫЙ VPN С 2FA ПОД КОНТРОЛЕМ ПЕРСОНАЛА", "next": "scene_9", "points": 10},
  135. {"text": ">>> ПОДНЯТЬ СЕАНС TEAMVIEWER НА СТАНЦИИ ОПЕРАТОРА", "next": "scene_9", "points": -10},
  136. {"text": ">>> ОТКРЫТЬ RDP ДОСТУП ИЗ ИНТЕРНЕТА НА ОДИН ЧАС", "next": "scene_9", "points": -15},
  137. {"text": ">>> ОТКАЗАТЬ В ДОСТУПЕ. ПУСТЬ ПРИЕЗЖАЮТ ЛИЧНО", "next": "scene_9", "points": 0}
  138. ],
  139. },
  140. "scene_9": {
  141. "title": "ОБНОВЛЕНИЕ СИСТЕМ",
  142. "text": "Вышло критическое обновление ОС Windows для серверов управления. Уязвимость активно эксплуатируется.",
  143. "image": None,
  144. "actions": [
  145. {"text": ">>> УСТАНОВИТЬ В ТЕСТОВОЙ ЗОНЕ, ЗАТЕМ СОГЛАСОВАТЬ ПРОСТОЙ", "next": "scene_10", "points": 10},
  146. {"text": ">>> СОГЛАСОВАТЬ ПРОСТОЙ И НАКАТИТЬ ОБНОВЛЕНИЕ СРАЗУ", "next": "scene_10", "points": 5},
  147. {"text": ">>> ВКЛЮЧИТЬ АВТОМАТИЧЕСКОЕ ОБНОВЛЕНИЕ WINDOWS", "next": "scene_10", "points": -15},
  148. {"text": ">>> ИГНОРИРОВАТЬ. АСУ ТП ВООБЩЕ НЕЛЬЗЯ ОБНОВЛЯТЬ", "next": "scene_10", "points": -5}
  149. ],
  150. },
  151. "scene_10": {
  152. "title": "НАХОДКА В ЦЕХУ",
  153. "text": "Оператор нашел красивую флешку (USB) возле проходной и хочет вставить ее в АРМ, чтобы найти владельца.",
  154. "image": "images/usb.png",
  155. "actions": [
  156. {"text": ">>> ИЗЪЯТЬ ФЛЕШКУ И ОТДАТЬ В ИНФОРМАЦИОННУЮ БЕЗОПАСНОСТЬ", "next": "scene_11", "points": 10},
  157. {"text": ">>> ЗАПРЕТИТЬ ВСТАВЛЯТЬ. СКАЗАТЬ, ЧТОБЫ ВЫКИНУЛ", "next": "scene_11", "points": 5},
  158. {"text": ">>> ВСТАВИТЬ В ОТДЕЛЬНЫЙ НОУТБУК БЕЗ СЕТИ ДЛЯ ПРОВЕРКИ", "next": "scene_11", "points": 8},
  159. {"text": ">>> РАЗРЕШИТЬ, НО ТОЛЬКО С ВКЛЮЧЕННЫМ АНТИВИРУСОМ", "next": "scene_11", "points": -15}
  160. ],
  161. },
  162. "scene_11": {
  163. "title": "ВИРУСНАЯ АТАКА",
  164. "text": "Несмотря на защиту, антивирус на станции АРМ сообщает о заражении шифровальщиком. Файлы начали блокироваться.",
  165. "image": "images/malware.png",
  166. "actions": [
  167. {"text": ">>> ФИЗИЧЕСКИ ОТКЛЮЧИТЬ ПАТЧ-КОРД ОТ СЕТИ (ИЗОЛИРОВАТЬ)", "next": "scene_12", "points": 10},
  168. {"text": ">>> СРОЧНО ВЫКЛЮЧИТЬ ПИТАНИЕ СТАНЦИИ", "next": "scene_12", "points": 5},
  169. {"text": ">>> ЗАПУСТИТЬ ПОЛНОЕ СКАНИРОВАНИЕ АНТИВИРУСОМ", "next": "scene_12", "points": -10},
  170. {"text": ">>> ПОПЫТАТЬСЯ НАЙТИ ПРОЦЕСС В ДИСПЕТЧЕРЕ ЗАДАЧ", "next": "scene_12", "points": -5}
  171. ],
  172. },
  173. "scene_12": {
  174. "title": "РЕЗЕРВНОЕ КОПИРОВАНИЕ",
  175. "text": "Шифровальщик уничтожил базу данных истории (Historian). Злоумышленники требуют выкуп.",
  176. "image": "images/backup.png",
  177. "actions": [
  178. {"text": ">>> ВОССТАНОВИТЬ ИЗ ОФФЛАЙН-БЭКАПА НА НОВОМ СЕРВЕРЕ", "next": "scene_13", "points": 10},
  179. {"text": ">>> ПЕРЕУСТАНОВИТЬ СИСТЕМУ С НУЛЯ (ПОТЕРЯТЬ ИСТОРИЮ)", "next": "scene_13", "points": 0},
  180. {"text": ">>> ПОПРОБОВАТЬ РАСШИФРОВАТЬ ФАЙЛЫ САМОСТОЯТЕЛЬНО", "next": "scene_13", "points": -5},
  181. {"text": ">>> ЗАПЛАТИТЬ ВЫКУП", "next": "scene_13", "points": -20}
  182. ],
  183. },
  184. "scene_13": {
  185. "title": "ФИНАЛЬНЫЙ ЭКЗАМЕН",
  186. "text": "Какой международный стандарт является основным ориентиром для кибербезопасности АСУ ТП?",
  187. "image": None,
  188. "actions": [
  189. {"text": ">>> ISA/IEC 62443", "next": "end", "points": 10},
  190. {"text": ">>> ISO/IEC 27001", "next": "end", "points": 5},
  191. {"text": ">>> ГОСТ Р 50922-2006", "next": "end", "points": 0},
  192. {"text": ">>> PCI-DSS", "next": "end", "points": -5}
  193. ],
  194. },
  195. "end": {
  196. "title": "ИТОГИ СМЕНЫ",
  197. "text": "Ваша смена в центре мониторинга безопасности завершена. Система проанализировала все принятые вами решения.",
  198. "image": "images/end.png",
  199. "actions": [{"text": ">>> СГЕНЕРИРОВАТЬ ОТЧЕТ И ВЫЙТИ", "next": None}],
  200. }
  201. }
  202. # // НОВОЕ: Перемешиваем ответы для каждой сцены
  203. for scene_key, scene_data in scenes.items():
  204. if "actions" in scene_data:
  205. random.shuffle(scene_data["actions"])
  206. return scenes
  207. def show_boot_screen(self):
  208. self.clear_frame()
  209. boot_frame = ctk.CTkFrame(self, fg_color=BG_COLOR)
  210. boot_frame.pack(fill="both", expand=True)
  211. self.boot_label = ctk.CTkLabel(boot_frame, text="", font=(FONT_FAMILY, 16), text_color=MATRIX_GREEN, justify="left", anchor="sw")
  212. self.boot_label.pack(side="bottom", fill="both", expand=True, padx=20, pady=20)
  213. lines = [
  214. "ИНИЦИАЛИЗАЦИЯ СИСТЕМЫ...",
  215. "ЗАГРУЗКА БАЗ ДАННЫХ И СЦЕНАРИЕВ...",
  216. "ПОДКЛЮЧЕНИЕ К СЕТИ ЗАВОДА [ОК]",
  217. "ПРОВЕРКА ДАННЫХ СТАРШЕГО ИНЖЕНЕРА...",
  218. "РАСШИФРОВКА ПРОТОКОЛОВ...",
  219. "ДОСТУП РАЗРЕШЕН."
  220. ]
  221. self.boot_seq(lines, 0)
  222. def boot_seq(self, lines, idx):
  223. if idx < len(lines):
  224. current_text = self.boot_label.cget("text")
  225. self.boot_label.configure(text=current_text + lines[idx] + "\n")
  226. self.after(300, self.boot_seq, lines, idx+1)
  227. else:
  228. self.after(500, lambda: self.load_scene("scene_1"))
  229. def clear_frame(self):
  230. for widget in self.winfo_children():
  231. widget.destroy()
  232. def type_text_effect(self, text, index=0):
  233. if index < len(text):
  234. self.text_label.configure(text=text[:index+1] + "█")
  235. self.type_id = self.after(15, self.type_text_effect, text, index+1)
  236. else:
  237. self.text_label.configure(text=text)
  238. def load_scene(self, scene_id):
  239. if self.type_id: self.after_cancel(self.type_id)
  240. self.clear_frame()
  241. if scene_id not in self.scenes: return
  242. scene = self.scenes[scene_id]
  243. if scene_id != "end" and scene_id != "scene_1":
  244. self.step_counter += 1
  245. # Индикатор прогресса
  246. if scene_id not in ["end", "scene_1"]:
  247. prog = ctk.CTkProgressBar(self, progress_color=MATRIX_GREEN, fg_color=FRAME_COLOR, height=10)
  248. prog.pack(fill="x", padx=40, pady=(20, 0))
  249. prog.set(self.step_counter / self.total_steps)
  250. main_frame = ctk.CTkFrame(self, fg_color=FRAME_COLOR, border_width=1, border_color=MATRIX_GREEN)
  251. main_frame.pack(pady=20, padx=40, fill="both", expand=True)
  252. ctk.CTkLabel(main_frame, text=f"// {scene['title']}", font=(FONT_FAMILY, 28, "bold"), text_color=MATRIX_GREEN).pack(pady=20, padx=30, anchor="w")
  253. content = ctk.CTkFrame(main_frame, fg_color="transparent")
  254. content.pack(pady=10, padx=30, fill="both", expand=True)
  255. self.text_label = ctk.CTkLabel(content, text="", font=(FONT_FAMILY, 18), text_color=TEXT_COLOR, wraplength=550, justify="left")
  256. self.text_label.pack(side="left", fill="both", expand=True)
  257. self.type_text_effect(scene["text"])
  258. if scene.get("image") and os.path.exists(scene["image"]):
  259. try:
  260. img = ctk.CTkImage(Image.open(scene["image"]), size=(250, 250))
  261. ctk.CTkLabel(content, image=img, text="").pack(side="right", padx=10)
  262. except:
  263. pass
  264. actions_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
  265. actions_frame.pack(pady=20, padx=30, fill="x")
  266. for a in scene["actions"]:
  267. btn = ctk.CTkButton(actions_frame, text=a["text"], font=(FONT_FAMILY, 15),
  268. fg_color=FRAME_COLOR, hover_color=HOVER_GREEN,
  269. text_color=MATRIX_GREEN, border_width=1, border_color=MATRIX_GREEN,
  270. height=45, anchor="w", command=lambda x=a: self.make_choice(x))
  271. btn.pack(fill="x", pady=5)
  272. if scene_id not in ["end", "scene_1"]:
  273. # Ограничиваем визуал очков процентами до 100
  274. display_score = min(100, self.score)
  275. ctk.CTkLabel(main_frame, text=f"ТЕКУЩАЯ ЗАЩИТА: {display_score}%", font=(FONT_FAMILY, 14), text_color=MATRIX_GREEN).pack(side="bottom", pady=10)
  276. def make_choice(self, action):
  277. # Добавляем очки, не позволяем упасть ниже 0. Максимум ограничен логически в UI, но копим реальные очки.
  278. self.score = max(0, self.score + action.get("points", 0))
  279. next_s = action.get("next")
  280. if next_s:
  281. self.load_scene(next_s)
  282. else:
  283. self.show_end_screen()
  284. def show_end_screen(self):
  285. self.clear_frame()
  286. f = ctk.CTkFrame(self, fg_color=FRAME_COLOR, border_width=2, border_color=MATRIX_GREEN)
  287. f.pack(pady=60, padx=60, fill="both", expand=True)
  288. ctk.CTkLabel(f, text="ОТЧЕТ ПО БЕЗОПАСНОСТИ", font=(FONT_FAMILY, 34, "bold"), text_color=MATRIX_GREEN).pack(pady=40)
  289. # Пересчет финального рейтинга (макс возможных баллов ~120, приводим к 100%)
  290. final_percentage = min(100, int((self.score / 120) * 100))
  291. if final_percentage >= 80:
  292. status = "ЗАВОД В ПОЛНОЙ БЕЗОПАСНОСТИ"
  293. color = MATRIX_GREEN
  294. elif final_percentage >= 50:
  295. status = "ЕСТЬ УЯЗВИМОСТИ (ТРЕБУЕТСЯ АУДИТ)"
  296. color = "orange"
  297. else:
  298. status = "КРИТИЧЕСКИЙ ВЗЛОМ СИСТЕМЫ"
  299. color = DANGER_RED
  300. ctk.CTkLabel(f, text=f"СТАТУС: {status}", font=(FONT_FAMILY, 24), text_color=TEXT_COLOR).pack(pady=10)
  301. ctk.CTkLabel(f, text=f"РЕЙТИНГ ЭФФЕКТИВНОСТИ: {final_percentage}%", font=(FONT_FAMILY, 30, "bold"), text_color=color).pack(pady=20)
  302. 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)
  303. if __name__ == "__main__":
  304. SecurityQuizApp().mainloop()