Spaces:
Sleeping
Sleeping
| """ | |
| steganography_generator.py | |
| ========================== | |
| Generates dynamic steganography (information hiding) challenges for the CyberArena pool. | |
| Hiding methods: | |
| 1. EOF: Append the flag bytes to the end of the image file. | |
| 2. EXIF: Inject the flag into the JPEG Comment (COM) segment manually. | |
| 3. ZIP: Append a zip archive containing flag.txt to the image file. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import base64 | |
| import hashlib | |
| import json | |
| import random | |
| import sys | |
| import uuid | |
| import zipfile | |
| import io | |
| from dataclasses import dataclass | |
| from typing import Optional, Literal | |
| import httpx | |
| try: | |
| from app.services.dedup import fetch_existing_titles | |
| except ImportError: | |
| def fetch_existing_titles(table: str, role_filter: Optional[str] = None, limit: int = 50) -> list[str]: | |
| return [] | |
| # Load .env early | |
| try: | |
| from app._env import load_app_env | |
| load_app_env() | |
| except Exception: | |
| pass | |
| from app.generators.crypto import _repair_json | |
| from app.core.config import ( | |
| SUPABASE_URL, SUPABASE_ANON_KEY, | |
| CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_MODEL, | |
| GROQ_API_KEY, GROQ_MODEL, | |
| NVIDIA_API_KEY, NVIDIA_MODEL, | |
| MISTRAL_API_KEY, MISTRAL_MODEL, MISTRAL_API_URL, | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # 0. Constants | |
| # --------------------------------------------------------------------------- # | |
| ALLOWED_DIFFICULTIES = ("مبتدئ", "متوسط", "قوي") | |
| ALLOWED_TEAMS = ("red",) | |
| POOL_TARGET = 5 | |
| POOL_THRESHOLD = 2 | |
| POOL_BATCH = 3 | |
| IDLE_POLL_SECS = 60 | |
| WATCHER_POLL_SECS = 60 | |
| _AI_BACKOFF_UNTIL: dict[str, float] = {} | |
| _POOL_LOCKS: dict[str, asyncio.Lock] = {} | |
| _WATCHER_STARTED: set[tuple[str, str]] = set() | |
| # Curated Unsplash images for cyber themes | |
| UNSPLASH_THEME_URLS = [ | |
| "https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=600&auto=format&fit=crop", # matrix/cyber | |
| "https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=600&auto=format&fit=crop", # cybersecurity/servers | |
| "https://images.unsplash.com/photo-1563986768609-322da13575f3?w=600&auto=format&fit=crop", # technology/abstract | |
| "https://images.unsplash.com/photo-1510511459019-5dda7724fd87?w=600&auto=format&fit=crop", # hacker/code | |
| "https://images.unsplash.com/photo-1601597111158-2fceff292cdc?w=600&auto=format&fit=crop", # network switch | |
| "https://images.unsplash.com/photo-1526374865147-73cf412a95c5?w=600&auto=format&fit=crop", # cyber circuit | |
| "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?w=600&auto=format&fit=crop", # hacking terminal | |
| "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=600&auto=format&fit=crop", # laptop code | |
| ] | |
| XP_BY_DIFFICULTY = {"مبتدئ": 100, "متوسط": 150, "قوي": 200} | |
| # --------------------------------------------------------------------------- # | |
| # 1. Hiding Algorithms | |
| # --------------------------------------------------------------------------- # | |
| def generate_fallback_image_bytes(text: str = "APEX CyberArena") -> bytes: | |
| """Generate a valid dark theme JPEG image using PIL (Pillow) or a tiny binary fallback.""" | |
| try: | |
| from PIL import Image, ImageDraw | |
| # Random dark background color to ensure variation | |
| bg_color = (random.randint(5, 20), random.randint(5, 20), random.randint(15, 30)) | |
| img = Image.new("RGB", (500, 500), color=bg_color) | |
| draw = ImageDraw.Draw(img) | |
| # Draw dynamic cyber grid lines | |
| grid_color = (random.randint(0, 50), random.randint(150, 255), random.randint(150, 255), 40) | |
| grid_spacing = random.choice([30, 40, 50, 60]) | |
| for i in range(0, 500, grid_spacing): | |
| draw.line([(i, 0), (i, 500)], fill=grid_color, width=1) | |
| draw.line([(0, i), (500, i)], fill=grid_color, width=1) | |
| # Draw dynamic abstract geometric patterns | |
| for _ in range(random.randint(3, 8)): | |
| shape_color = (random.randint(50, 255), random.randint(100, 255), random.randint(100, 255)) | |
| x0 = random.randint(50, 350) | |
| y0 = random.randint(50, 350) | |
| x1 = x0 + random.randint(50, 150) | |
| y1 = y0 + random.randint(50, 150) | |
| draw.ellipse([x0, y0, x1, y1], outline=shape_color, width=random.choice([1, 2, 3])) | |
| draw.text((30, 30), text, fill=(255, 255, 255)) | |
| draw.text((30, 450), f"Security Artifact ID: {uuid.uuid4().hex[:8]}", fill=(120, 120, 120)) | |
| buf = io.BytesIO() | |
| img.save(buf, format="JPEG") | |
| return buf.getvalue() | |
| except Exception: | |
| # Minimal 1x1 valid JPEG bytes fallback + random string to guarantee different hash | |
| base_jpeg = ( | |
| b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00" | |
| b"\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08" | |
| b"\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e" | |
| b"\x1d\x1a\x1c\x1c $.' \",#\x1c\x1c(7),01444\x1f'9=82<.342" | |
| b"\xff\xc0\x00\x0b\x08\x00\x01\x00\x01\x01\x01\x11\x00\xff\xc4\x00" | |
| b"\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00" | |
| b"\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\xff\xc4\x00" | |
| b"\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00" | |
| b"\x00\x00\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13" | |
| b"Qa\x07\"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0" | |
| b"$3br\x82\x16\x17\x18\x19\x1a%&'()*456789:CDEFGHIJSTUVWXYZ" | |
| b"cdefghijstuvwxyz\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95" | |
| b"\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3" | |
| b"\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca" | |
| b"\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe2\xe3\xe4\xe5\xe6\xe7\xe8" | |
| b"\xe9\xea\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xda\x00\x0c\x03" | |
| b"\x01\x00\x02\x11\x03\x11\x00?\x00\xed\xfc\xff\xd9" | |
| ) | |
| return base_jpeg + f"\n# ID: {uuid.uuid4().hex}\n".encode("utf-8") | |
| def generate_host_file_bytes(filename: str) -> bytes: | |
| """Generate dynamic dummy bytes for various stego host carriers (.pdf, .zip, .docx, .png).""" | |
| ext = filename.split(".")[-1].lower() | |
| unique_id = uuid.uuid4().hex[:10] | |
| if ext in ("jpg", "jpeg", "jfif"): | |
| return generate_fallback_image_bytes(f"CyberArena Stock Asset: {unique_id}") | |
| elif ext == "png": | |
| # PNG signature + simple dynamic chunk representation | |
| return b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15c4\x00\x00\x00\nIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82" + f"\n# ID: {unique_id}".encode() | |
| elif ext == "pdf": | |
| return ( | |
| f"%PDF-1.4\n" | |
| f"1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n" | |
| f"2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n" | |
| f"3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >> endobj\n" | |
| f"4 0 obj << /Length 55 >> stream\n" | |
| f"BT /F1 24 Tf 100 700 Td (APEX Confidential Artifact ID: {unique_id}) Tj ET\n" | |
| f"endstream\n" | |
| f"endobj\n" | |
| f"xref\n" | |
| f"0 5\n" | |
| f"0000000000 65535 f\n" | |
| f"0000000009 00000 n\n" | |
| f"0000000058 00000 n\n" | |
| f"0000000115 00000 n\n" | |
| f"0000000218 00000 n\n" | |
| f"trailer << /Size 5 /Root 1 0 R >>\n" | |
| f"startxref\n" | |
| f"312\n" | |
| f"%%EOF\n" | |
| ).encode("utf-8") | |
| elif ext == "zip": | |
| zip_buf = io.BytesIO() | |
| with zipfile.ZipFile(zip_buf, 'w', zipfile.ZIP_DEFLATED) as zip_file: | |
| zip_file.writestr("decoy.txt", f"Decoy file ID: {uuid.uuid4().hex}\n") | |
| return zip_buf.getvalue() | |
| elif ext in ("docx", "xlsx"): | |
| zip_buf = io.BytesIO() | |
| with zipfile.ZipFile(zip_buf, 'w', zipfile.ZIP_DEFLATED) as zip_file: | |
| zip_file.writestr("[Content_Types].xml", f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/></Types> <!-- ID: {unique_id} -->') | |
| return zip_buf.getvalue() | |
| else: | |
| # Fallback binary text representation | |
| return f"APEX Analysis Report {uuid.uuid4().hex}\n==================================\nThis is a secure system log.\n".encode("utf-8") | |
| async def fetch_random_unsplash_image() -> bytes: | |
| """Fetch an image from Unsplash. Falls back to generating one if offline.""" | |
| url = random.choice(UNSPLASH_THEME_URLS) | |
| try: | |
| async with httpx.AsyncClient(timeout=10) as client: | |
| r = await client.get(url) | |
| if r.status_code == 200: | |
| return r.content | |
| except Exception as e: | |
| print(f"[steganography] Unsplash fetch failed: {e}") | |
| return generate_fallback_image_bytes() | |
| def hide_flag_eof(image_bytes: bytes, flag: str) -> bytes: | |
| """Hiding method 1: Append flag directly to EOF.""" | |
| return image_bytes + b"\n" + flag.encode("utf-8") + b"\n" | |
| def hide_flag_exif(image_bytes: bytes, flag: str) -> bytes: | |
| """Hiding method 2: Inject flag into standard JPEG comment segment manually.""" | |
| if image_bytes.startswith(b"\xff\xd8"): | |
| comment_bytes = flag.encode("utf-8") | |
| length = len(comment_bytes) + 2 | |
| length_bytes = length.to_bytes(2, byteorder='big') | |
| # FF FE is the JPEG COM marker | |
| com_segment = b"\xff\xfe" + length_bytes + comment_bytes | |
| # Insert segment right after the SOI marker (FF D8) | |
| return image_bytes[0:2] + com_segment + image_bytes[2:] | |
| return hide_flag_eof(image_bytes, flag) | |
| def hide_flag_zip(image_bytes: bytes, flag: str) -> bytes: | |
| """Hiding method 3: Append a zip containing flag.txt to the end of the image.""" | |
| zip_buf = io.BytesIO() | |
| with zipfile.ZipFile(zip_buf, 'w', zipfile.ZIP_DEFLATED) as zip_file: | |
| zip_file.writestr("flag.txt", flag + "\n") | |
| return image_bytes + zip_buf.getvalue() | |
| def hide_flag(image_bytes: bytes, method: str, flag: str) -> bytes: | |
| if method == "EOF": | |
| return hide_flag_eof(image_bytes, flag) | |
| elif method == "EXIF": | |
| return hide_flag_exif(image_bytes, flag) | |
| elif method == "ZIP": | |
| return hide_flag_zip(image_bytes, flag) | |
| raise ValueError(f"Unknown hiding method: {method}") | |
| def generate_random_filename(hiding_method: str) -> str: | |
| """Generates fully randomized, realistic cybersecurity/corporate filenames.""" | |
| prefixes = [ | |
| "suspect", "evidence", "leak", "backup", "invoice", "report", "confidential", | |
| "intercept", "transfer", "log", "archive", "secret", "dump", "agent_report", | |
| "network_capture", "shadow_copy", "credentials", "kernel_dump", "key_log", | |
| "system_config", "traffic_log", "threat_intel", "payload", "exfiltration", | |
| "covert_data", "unauthorized_access", "encrypted_db", "device_firmware", | |
| "chat_history", "user_profile", "auth_logs", "database_dump", "cctv_still", | |
| "blueprint", "memo", "contract", "financials", "audit_trail" | |
| ] | |
| nouns = [ | |
| "data", "file", "record", "payload", "evidence", "intel", "capture", "doc", | |
| "archive", "leak", "image", "packet", "db", "backup", "log", "firmware", | |
| "receipt", "scan", "capture", "export", "attachment", "snapshot", "dump" | |
| ] | |
| if hiding_method == "EXIF": | |
| ext = random.choice(["jpg", "jpeg", "jfif"]) | |
| else: | |
| ext = random.choice([ | |
| "jpg", "jpeg", "png", "pdf", "zip", "docx", "xlsx", "txt", "bin", "dat", | |
| "conf", "db", "tar.gz", "rar", "sys", "bak" | |
| ]) | |
| pattern = random.choice([ | |
| f"{random.choice(prefixes)}_{random.choice(nouns)}", | |
| f"{random.choice(prefixes)}_{random.randint(1000, 9999)}", | |
| f"{random.choice(prefixes)}", | |
| f"{random.choice(nouns)}_{random.choice(prefixes)}_{random.randint(10, 99)}" | |
| ]) | |
| return f"{pattern}.{ext}" | |
| # --------------------------------------------------------------------------- # | |
| # 2. Challenge Dataclasses | |
| # --------------------------------------------------------------------------- # | |
| class ScenarioSpec: | |
| team_role: str | |
| module: str | |
| difficulty: str | |
| title: str | |
| story: str | |
| task_outline: str | |
| hiding_method: str | |
| hints: list | |
| class Challenge: | |
| team_role: str | |
| module: str | |
| title: str | |
| story: str | |
| task_outline: str | |
| files: dict | |
| file_metadata: dict | |
| command_outputs: dict | |
| hints: list | |
| tools_whitelist: list | |
| flag_hash: str | |
| flag_preview: str | |
| difficulty: str | |
| xp_reward: int | |
| hiding_method: str | |
| image_url: str | |
| topic: str | |
| def to_db_row(self) -> dict: | |
| return { | |
| "team_role": self.team_role, | |
| "module": self.module, | |
| "title": self.title, | |
| "story": self.story, | |
| "task_outline": self.task_outline, | |
| "files": self.files, | |
| "file_metadata": self.file_metadata, | |
| "command_outputs": self.command_outputs, | |
| "hints": self.hints, | |
| "tools_whitelist": self.tools_whitelist, | |
| "flag_hash": self.flag_hash, | |
| "flag_preview": self.flag_preview, | |
| "difficulty": self.difficulty, | |
| "xp_reward": self.xp_reward, | |
| "hiding_method": self.hiding_method, | |
| "image_url": self.image_url, | |
| "topic": self.topic, | |
| } | |
| # --------------------------------------------------------------------------- # | |
| # 3. Curated Seeds | |
| # --------------------------------------------------------------------------- # | |
| CURATED_SEEDS = [ | |
| { | |
| "difficulty": "مبتدئ", | |
| "hiding_method": "EOF", | |
| "title": "شفرة الملفات المتروكة", | |
| "story": "اعترض فريق المراقبة الأمنية صورة مشبوهة باسم suspect.jpg تم رفعها إلى خادم غير مصرح به. يُعتقد أن المهاجم استغل ثغرة وقام بلصق العلم في نهاية ملف الصورة دون تعديل واصفاتها.", | |
| "task_outline": "قم بفحص الملف suspect.jpg المرفق. ابحث عن أي نصوص سرية مخفية في نهاية بنية الصورة الثنائية للحصول على العلم (Flag).", | |
| "hints": [ | |
| "الملفات الثنائية مثل الصور قد تحتوي أحياناً على نصوص مخفية في نهايتها. هل جربت البحث عن أدوات تقوم باستخراج النصوص المقروءة من الملفات الثنائية؟", | |
| "هناك أداة شائعة في أنظمة لينكس تستخدم لعرض السلاسل النصية المقروءة داخل أي ملف ثنائي. فحص مخرجات هذه الأداة لملف الصورة قد يكشف لك العلم." | |
| ] | |
| }, | |
| { | |
| "difficulty": "متوسط", | |
| "hiding_method": "ZIP", | |
| "title": "الحقيبة المزدوجة", | |
| "story": "عثر فريق الاستجابة للحوادث على خادم ويب مصاب. المهاجم قام بدمج أرشيف كامل مضغوط يحوي معلومات الاختراق داخل صورة التحدي challenge.jpg كتقنية للتمويه والتخفي.", | |
| "task_outline": "افحص الملف challenge.jpg المرفق لتحديد ما إذا كان يحوي أرشيفاً مضغوطاً مدمجاً في بنيته، ثم استخرج الأرشيف لتجد ملف العلم flag.txt.", | |
| "hints": [ | |
| "الامتداد الظاهري للملف ليس دائماً حقيقياً. قد تقوم بعض البرمجيات بدمج أنواع مختلفة من الملفات معاً (مثل دمج أرشيف مضغوط مع صورة). كيف يمكنك التحقق من البنية الحقيقية للملف؟", | |
| "إذا كانت بنية الملف تحتوي على أرشيف مضغوط ملحق بها، فربما تفيدك أدوات فك الضغط المعتادة لاستخراج الملفات المدمجة بداخلها." | |
| ] | |
| }, | |
| { | |
| "difficulty": "قوي", | |
| "hiding_method": "EXIF", | |
| "title": "واصفات العميل الوصفية", | |
| "story": "أرسل عميل سري صورة طبيعية باسم agent.jpg إلى مركز العمليات. تحليل واصفات الملف أظهر أن الصورة لا تحتوي على أي تعديل في البيانات الثنائية الملحقة، ولكن تم استغلال حقل التعليقات (Comment) في EXIF Metadata لنقل رسالة سرية.", | |
| "task_outline": "افحص بيانات واصفات الملف agent.jpg واستخرج العلم المخفي داخل حقول البيانات الوصفية المدمجة في الصورة (EXIF Metadata).", | |
| "hints": [ | |
| "الصور الرقمية تحمل معها بيانات وصفية (Metadata) تصف الكاميرا والتعليقات والبيانات الجغرافية. هل فكرت في فحص هذه البيانات الوصفية للملف؟", | |
| "يمكنك قراءة التعليقات والبيانات الوصفية باستخدام أدوات عرض النصوص أو أدوات فحص بيانات EXIF. ابحث عن حقل التعليق (Comment) المدمج في رأس الملف." | |
| ] | |
| } | |
| ] | |
| # --------------------------------------------------------------------------- # | |
| # 4. Challenge Builder | |
| # --------------------------------------------------------------------------- # | |
| def _align_filename_in_story(text: str, filename: str) -> str: | |
| import re | |
| # Replace any word resembling a file name with the new filename | |
| text = re.sub( | |
| r"[a-zA-Z0-9_\-]+\.(jpg|jpeg|png|pdf|zip|docx|xlsx|txt|bin|dat|conf|db|tar\.gz|rar|sys|bak|jfif)", | |
| filename, | |
| text | |
| ) | |
| return text | |
| class ChallengeBuilder: | |
| async def build(self, spec: ScenarioSpec, image_bytes: bytes) -> Challenge: | |
| flag_preview = f"CyberArena{{{uuid.uuid4().hex[:16]}}}" | |
| flag_hash = hashlib.sha256(flag_preview.encode()).hexdigest() | |
| # Generate fully randomized filename | |
| filename = generate_random_filename(spec.hiding_method) | |
| spec.story = _align_filename_in_story(spec.story, filename) | |
| spec.task_outline = _align_filename_in_story(spec.task_outline, filename) | |
| # Decide dynamic host bytes | |
| ext = filename.split(".")[-1].lower() | |
| if ext in ("jpg", "jpeg", "png", "jfif") and image_bytes: | |
| host_bytes = image_bytes | |
| else: | |
| host_bytes = generate_host_file_bytes(filename) | |
| file_bytes = hide_flag(host_bytes, spec.hiding_method, flag_preview) | |
| files = { | |
| filename: base64.b64encode(file_bytes).decode("utf-8") | |
| } | |
| # Generate realistic 'file' command outputs | |
| file_desc = "JPEG image data, JFIF standard 1.01" | |
| if ext == "png": | |
| file_desc = "PNG image data, 1 x 1, 8-bit/color RGBA" | |
| elif ext == "jfif": | |
| file_desc = "JPEG image data, JFIF standard 1.01" | |
| elif ext == "pdf": | |
| file_desc = "PDF document, version 1.4" | |
| elif ext == "zip": | |
| file_desc = "Zip archive data, at least v2.0 to extract" | |
| elif ext in ("docx", "xlsx"): | |
| file_desc = "Microsoft Word 2007+ Document (Zip archive)" | |
| elif ext == "txt": | |
| file_desc = "ASCII text" | |
| elif ext in ("bin", "dat", "bak"): | |
| file_desc = "data" | |
| elif ext == "conf": | |
| file_desc = "ASCII text, with CRLF line terminators" | |
| elif ext == "db": | |
| file_desc = "SQLite 3.x database" | |
| elif ext == "tar.gz": | |
| file_desc = "gzip compressed data, from Unix" | |
| elif ext == "rar": | |
| file_desc = "RAR archive data, v5.0" | |
| elif ext == "sys": | |
| file_desc = "PE32 system driver (native) Intel 80386" | |
| if spec.hiding_method == "ZIP" and ext in ("jpg", "jpeg", "png", "pdf", "jfif"): | |
| file_desc += " (with appended Zip archive data)" | |
| # Command outputs metadata for UI cheat sheets / dashboard previews | |
| command_outputs = { | |
| "ls": {"stdout": filename, "stderr": "", "exit_code": 0}, | |
| f"file {filename}": { | |
| "stdout": f"{filename}: {file_desc}", | |
| "stderr": "", | |
| "exit_code": 0 | |
| } | |
| } | |
| return Challenge( | |
| team_role=spec.team_role, | |
| module="steganography", | |
| title=spec.title, | |
| story=spec.story, | |
| task_outline=spec.task_outline, | |
| files=files, | |
| file_metadata={ | |
| filename: {"encoding": "binary", "size": len(file_bytes)} | |
| }, | |
| command_outputs=command_outputs, | |
| hints=spec.hints, | |
| tools_whitelist=["cat", "ls", "file", "strings", "unzip"], | |
| flag_hash=flag_hash, | |
| flag_preview=flag_preview, | |
| difficulty=spec.difficulty, | |
| xp_reward=XP_BY_DIFFICULTY.get(spec.difficulty, 150), | |
| hiding_method=spec.hiding_method, | |
| image_url="", | |
| topic="steganography" | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # 5. AI Generation Prompts | |
| # --------------------------------------------------------------------------- # | |
| STEGANOGRAPHY_PROMPT = """أنت كبير مصممي سيناريوهات الأمن السيبراني في منصة APEX. | |
| مهمتك: تصميم سيناريو تدريبي للتحقيق الجنائي الرقمي وإخفاء المعلومات (Steganography) للفريق الأحمر. | |
| الموضوع: إخفاء المعلومات في الوسائط (Steganography) | |
| مستوى الصعوبة: {difficulty} | |
| الطريقة المحددة للإخفاء: {hiding_method} | |
| تنبيه هام جداً لمنع التكرار (قاعدة صارمة ضد التشابه): | |
| يجب أن تتجنب تماماً تكرار أي من العناوين أو الأفكار أو السياقات المستعملة مؤخراً. | |
| العناوين التي تم توليدها مؤخراً ويُمنع استخدامها أو تكرار أي فكرة منها هي: | |
| {blacklisted_titles} | |
| السيناريو يجب أن يكون بلغة عربية فصحى احترافية ومشوقة. | |
| 1. العنوان: عنوان غامض ومبتكر باللغة العربية (مثل: "تسريب الظلال"، "الرسالة المشفرة للعميل"، "الملف الملتحم"). | |
| 2. القصة: قصة سياقية واقعية (3-4 جمل) عن حادثة أمنية، تسريب، أو عميل يقوم بنقل رسائل سرية مخفية في صورة. | |
| 3. المهمة: اصف للمستخدم ما يجب القيام به باللغة العربية. تجنب ذكر "ساندبوكس التدريب" أو أي أدوات صريحة، وركز على أن الملف يتم تحميله محلياً على جهاز المستخدم. | |
| 4. التلميحات (hints): يجب ألا تحتوي على حل مباشر أو أوامر طرفية صريحة (مثل run unzip or strings). بدلاً من ذلك، يجب أن تقدم توجيهات عامة ومساعدة غير مباشرة (Socratic hints) توجه المستخدم نحو التفكير في كيفية فحص الملف (مثل الإشارة لوجود بيانات وصفية أو إمكانية دمج الأرشيفات في الملفات وكيفية استخراجها). | |
| أرجع JSON فقط بدون أي نص قبله أو بعده وبدون كود التنسيق ```json: | |
| {{ | |
| "title": "عنوان التحدي باللغة العربية", | |
| "story": "قصة السياق الكاملة باللغة العربية الفصحى", | |
| "task_outline": "تفاصيل المهمة باللغة العربية الفصحى", | |
| "hints": [ | |
| "تلميح مساعد ذكي أول (غير مباشر)", | |
| "تلميح مساعد ذكي ثان (غير مباشر)" | |
| ] | |
| }}""" | |
| async def ai_generate_scenario_via_mistral(team_role: str, difficulty: str, hiding_method: str, blacklisted_titles: str = "") -> ScenarioSpec: | |
| if not MISTRAL_API_KEY: | |
| raise RuntimeError("Mistral API Key missing") | |
| prompt = STEGANOGRAPHY_PROMPT.format(difficulty=difficulty, hiding_method=hiding_method, blacklisted_titles=blacklisted_titles) | |
| body = { | |
| "model": MISTRAL_MODEL, | |
| "messages": [ | |
| {"role": "system", "content": "You output valid JSON only. No prose, no markdown fences."}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| "temperature": 0.7, | |
| "response_format": {"type": "json_object"} | |
| } | |
| async with httpx.AsyncClient(timeout=30) as client: | |
| r = await client.post(MISTRAL_API_URL, headers={"Authorization": f"Bearer {MISTRAL_API_KEY}"}, json=body) | |
| if r.status_code != 200: | |
| raise RuntimeError(f"Mistral HTTP {r.status_code}") | |
| data = _repair_json(r.json()["choices"][0]["message"]["content"]) | |
| return ScenarioSpec( | |
| team_role=team_role, | |
| module="steganography", | |
| difficulty=difficulty, | |
| title=data["title"], | |
| story=data["story"], | |
| task_outline=data["task_outline"], | |
| hiding_method=hiding_method, | |
| hints=data.get("hints") or [] | |
| ) | |
| async def ai_generate_scenario_via_cloudflare(team_role: str, difficulty: str, hiding_method: str, blacklisted_titles: str = "") -> ScenarioSpec: | |
| if not CLOUDFLARE_API_TOKEN or not CLOUDFLARE_ACCOUNT_ID: | |
| raise RuntimeError("Cloudflare settings missing") | |
| url = f"https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/run/{CLOUDFLARE_MODEL}" | |
| prompt = STEGANOGRAPHY_PROMPT.format(difficulty=difficulty, hiding_method=hiding_method, blacklisted_titles=blacklisted_titles) | |
| body = { | |
| "messages": [ | |
| {"role": "system", "content": "You are a cybersecurity training scenario generator. Output JSON only."}, | |
| {"role": "user", "content": prompt} | |
| ] | |
| } | |
| async with httpx.AsyncClient(timeout=30) as client: | |
| r = await client.post(url, headers={"Authorization": f"Bearer {CLOUDFLARE_API_TOKEN}"}, json=body) | |
| if r.status_code != 200: | |
| raise RuntimeError(f"Cloudflare HTTP {r.status_code}") | |
| data = _repair_json(r.json()["result"]["response"]) | |
| return ScenarioSpec( | |
| team_role=team_role, | |
| module="steganography", | |
| difficulty=difficulty, | |
| title=data["title"], | |
| story=data["story"], | |
| task_outline=data["task_outline"], | |
| hiding_method=hiding_method, | |
| hints=data.get("hints") or [] | |
| ) | |
| async def ai_generate_scenario_via_groq(team_role: str, difficulty: str, hiding_method: str, blacklisted_titles: str = "") -> ScenarioSpec: | |
| if not GROQ_API_KEY: | |
| raise RuntimeError("Groq key missing") | |
| prompt = STEGANOGRAPHY_PROMPT.format(difficulty=difficulty, hiding_method=hiding_method, blacklisted_titles=blacklisted_titles) | |
| body = { | |
| "model": GROQ_MODEL, | |
| "messages": [ | |
| {"role": "system", "content": "You output valid JSON only."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| "temperature": 0.7, | |
| "response_format": {"type": "json_object"} | |
| } | |
| async with httpx.AsyncClient(timeout=30) as client: | |
| r = await client.post(GROQ_API_URL, headers={"Authorization": f"Bearer {GROQ_API_KEY}"}, json=body) | |
| if r.status_code != 200: | |
| raise RuntimeError(f"Groq HTTP {r.status_code}") | |
| data = _repair_json(r.json()["choices"][0]["message"]["content"]) | |
| return ScenarioSpec( | |
| team_role=team_role, | |
| module="steganography", | |
| difficulty=difficulty, | |
| title=data["title"], | |
| story=data["story"], | |
| task_outline=data["task_outline"], | |
| hiding_method=hiding_method, | |
| hints=data.get("hints") or [] | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # 6. DB operations | |
| # --------------------------------------------------------------------------- # | |
| def get_pool_count(team_role: str, module: Optional[str] = None) -> int: | |
| if not SUPABASE_ANON_KEY or not SUPABASE_URL: | |
| return 0 | |
| url = f"{SUPABASE_URL}/rest/v1/steganography_challenges?select=id&team_role=eq.{team_role}" | |
| try: | |
| r = httpx.get(url, headers={ | |
| "apikey": SUPABASE_ANON_KEY, | |
| "Authorization": f"Bearer {SUPABASE_ANON_KEY}" | |
| }, timeout=10) | |
| if r.status_code == 200: | |
| return len(r.json()) | |
| except Exception as e: | |
| print(f"[steganography] get_pool_count error: {e}") | |
| return 0 | |
| async def insert_to_db(challenge: Challenge) -> bool: | |
| if not SUPABASE_ANON_KEY or not SUPABASE_URL: | |
| return False | |
| from app.core.config import normalize_row_module | |
| payload = challenge.to_db_row() | |
| payload = normalize_row_module("steganography_challenges", payload) | |
| team_role = payload.get("team_role", "red") | |
| from app.services.insert_guard import atomic_insert | |
| from app.services.dedup import is_duplicate_steganography | |
| return await atomic_insert( | |
| table="steganography_challenges", | |
| team_role=team_role, | |
| row=payload, | |
| dedup_func=is_duplicate_steganography, | |
| dedup_args=[ | |
| payload.get("title", ""), | |
| payload.get("story", ""), | |
| payload.get("task_outline", ""), | |
| ], | |
| dedup_kwargs={"role_filter": team_role}, | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # 7. Refill and Watcher API | |
| # --------------------------------------------------------------------------- # | |
| def _get_pool_lock(team_role: str) -> asyncio.Lock: | |
| if team_role not in _POOL_LOCKS: | |
| _POOL_LOCKS[team_role] = asyncio.Lock() | |
| return _POOL_LOCKS[team_role] | |
| async def _refill_pool_inner(team_role: str, count: int) -> int: | |
| """Unlocked inner — caller must hold _get_pool_lock(team_role).""" | |
| current = get_pool_count(team_role) | |
| gap = max(0, POOL_TARGET - current) | |
| if gap == 0: | |
| return 0 | |
| if count > gap: | |
| count = gap | |
| inserted = 0 | |
| methods = ["EOF", "ZIP", "EXIF"] | |
| difficulties = ["مبتدئ", "متوسط", "قوي"] | |
| recent_titles = [] | |
| try: | |
| recent_titles = fetch_existing_titles("steganography_challenges", team_role, limit=30) | |
| except Exception as e: | |
| print(f"[steganography] Failed to fetch recent titles: {e}") | |
| blacklisted_titles_str = "\n".join([f"- {t}" for t in recent_titles]) if recent_titles else "لا يوجد عناوين سابقة حالياً." | |
| for i in range(count): | |
| method = methods[i % len(methods)] | |
| difficulty = difficulties[i % len(difficulties)] | |
| spec = None | |
| source = "ai" | |
| providers = [ | |
| ai_generate_scenario_via_mistral, | |
| ai_generate_scenario_via_cloudflare, | |
| ai_generate_scenario_via_groq | |
| ] | |
| for p in providers: | |
| try: | |
| spec = await p(team_role, difficulty, method, blacklisted_titles=blacklisted_titles_str) | |
| print(f"[steganography] AI {p.__name__} OK: title={spec.title[:40]}") | |
| break | |
| except Exception as e: | |
| print(f"[steganography] AI {p.__name__} failed: {e}") | |
| if spec is None: | |
| source = "seed" | |
| seed = random.choice([s for s in CURATED_SEEDS if s["hiding_method"] == method] or CURATED_SEEDS) | |
| rand_tag = uuid.uuid4().hex[:6] | |
| spec = ScenarioSpec( | |
| team_role=team_role, | |
| module="steganography", | |
| difficulty=seed["difficulty"], | |
| title=f"{seed['title']} - رمز {rand_tag}", | |
| story=seed["story"] + f" [معرف التدقيق: {rand_tag}]", | |
| task_outline=seed["task_outline"], | |
| hiding_method=seed["hiding_method"], | |
| hints=seed["hints"] | |
| ) | |
| try: | |
| img_bytes = await fetch_random_unsplash_image() | |
| ch = await ChallengeBuilder().build(spec, img_bytes) | |
| if await insert_to_db(ch): | |
| inserted += 1 | |
| except Exception as e: | |
| print(f"[steganography] Build/insert failed: {e}") | |
| await asyncio.sleep(0.3) | |
| return inserted | |
| async def refill_pool(team_role: str, count: int = POOL_BATCH) -> int: | |
| async with _get_pool_lock(team_role): | |
| return await _refill_pool_inner(team_role, count) | |
| async def start_pool_watcher(team_role: str) -> None: | |
| key = ("steganography", team_role) | |
| if key in _WATCHER_STARTED: | |
| print(f"[steganography:{team_role}] watcher already running — skipping duplicate start.") | |
| return | |
| _WATCHER_STARTED.add(key) | |
| try: | |
| label = f"[steganography:{team_role}]" | |
| print(f"{label} watcher started (target={POOL_TARGET}).") | |
| while True: | |
| try: | |
| sleep_secs = IDLE_POLL_SECS | |
| async with _get_pool_lock(team_role): | |
| count = get_pool_count(team_role) | |
| if count < POOL_TARGET: | |
| needed = POOL_TARGET - count | |
| print(f"{label} pool below target ({count}/{POOL_TARGET}) — refilling {needed}…") | |
| added = await _refill_pool_inner(team_role, needed) | |
| new_count = count + added | |
| print(f"{label} refilled: {count} → {new_count} (target {POOL_TARGET}).") | |
| sleep_secs = 5 | |
| await asyncio.sleep(sleep_secs) | |
| except Exception as e: | |
| import traceback | |
| print(f"{label} watcher error: {e}") | |
| traceback.print_exc() | |
| await asyncio.sleep(10) | |
| finally: | |
| _WATCHER_STARTED.discard(key) | |
| # --------------------------------------------------------------------------- # | |
| # 8. CLI Parser | |
| # --------------------------------------------------------------------------- # | |
| def main(): | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| sys.stderr.reconfigure(encoding="utf-8", errors="replace") | |
| except Exception: | |
| pass | |
| p = argparse.ArgumentParser(description="Steganography challenge generator (RED TEAM ONLY)") | |
| p.add_argument("--team", choices=("red",), default="red") | |
| p.add_argument("--count", type=int, default=5) | |
| p.add_argument("--refill", choices=("red",), help="Refill the team-wide pool") | |
| p.add_argument("--dry-run", action="store_true", help="Build but don't insert") | |
| args = p.parse_args() | |
| if args.refill: | |
| added = asyncio.run(refill_pool(args.refill, args.count)) | |
| print(f"Refilled {args.refill}: +{added}") | |
| return | |
| print(f"Building {args.count} challenges for RED team...") | |
| async def test_generation(): | |
| methods = ["EOF", "ZIP", "EXIF"] | |
| for i in range(args.count): | |
| method = methods[i % len(methods)] | |
| seed = CURATED_SEEDS[i % len(CURATED_SEEDS)] | |
| spec = ScenarioSpec( | |
| team_role="red", | |
| module="steganography", | |
| difficulty=seed["difficulty"], | |
| title=seed["title"] + " (Test)", | |
| story=seed["story"], | |
| task_outline=seed["task_outline"], | |
| hiding_method=method, | |
| hints=seed["hints"] | |
| ) | |
| img = generate_fallback_image_bytes() | |
| ch = await ChallengeBuilder().build(spec, img) | |
| print(f" • {ch.title} | {ch.difficulty} | hiding={ch.hiding_method} | flag={ch.flag_preview[:40]}...") | |
| if not args.dry_run: | |
| await insert_to_db(ch) | |
| asyncio.run(test_generation()) | |
| if __name__ == "__main__": | |
| import time | |
| main() | |