| import os |
| import time |
| import html |
| import re |
| import threading |
| import gradio as gr |
| import modal |
|
|
| |
| css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:ital,wght@0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap'); |
| |
| /* ============================================================ |
| GLOBAL PALETTE LOCK — prevents ALL Gradio dark-mode bleed |
| Applied to every selector Gradio uses for theming |
| ============================================================ */ |
| :root, |
| html, |
| body, |
| .gradio-container, |
| .dark, |
| html.dark, |
| .dark body, |
| .dark .gradio-container, |
| .dark .block, |
| .dark .form, |
| .dark .wrap, |
| [data-testid="block"], |
| .dark [data-testid="block"] { |
| --body-background-fill: #faf8f5 !important; |
| --body-text-color: #231f1d !important; |
| --background-fill-primary: #faf8f5 !important; |
| --background-fill-secondary: #f2ece4 !important; |
| --border-color-primary: #e6dfd5 !important; |
| --border-color-secondary: #e6dfd5 !important; |
| --input-background-fill: #ffffff !important; |
| --input-border-color: #e6dfd5 !important; |
| --input-text-color: #231f1d !important; |
| --block-background-fill: #ffffff !important; |
| --block-border-color: #e6dfd5 !important; |
| --block-title-text-color: #231f1d !important; |
| --block-label-text-color: #594c43 !important; |
| --button-primary-background-fill: #802f1a !important; |
| --button-primary-background-fill-hover: #692412 !important; |
| --button-primary-text-color: #ffffff !important; |
| --button-primary-border-color: #802f1a !important; |
| --button-secondary-background-fill: #ffffff !important; |
| --button-secondary-text-color: #231f1d !important; |
| --button-secondary-border-color: #e6dfd5 !important; |
| --color-accent: #802f1a !important; |
| --link-text-color: #802f1a !important; |
| --link-text-color-hover: #692412 !important; |
| --shadow-drop: none !important; |
| --shadow-drop-lg: none !important; |
| --shadow-inset: none !important; |
| background-color: #faf8f5 !important; |
| color: #231f1d !important; |
| } |
| |
| /* ============================================================ |
| BASE ELEMENTS — no text shadows, no dark bleed |
| ============================================================ */ |
| *, *::before, *::after { |
| text-shadow: none !important; |
| box-shadow: none !important; |
| } |
| |
| html, body { |
| background-color: #faf8f5 !important; |
| color: #231f1d !important; |
| -webkit-text-fill-color: #231f1d !important; |
| font-family: 'Inter', -apple-system, sans-serif !important; |
| -webkit-font-smoothing: antialiased; |
| } |
| |
| .gradio-container { |
| background-color: #faf8f5 !important; |
| border: none !important; |
| max-width: 95% !important; |
| width: 95% !important; |
| margin: 20px auto !important; |
| padding: 24px !important; |
| box-shadow: none !important; |
| } |
| |
| /* Ensure dropdowns, select options, and textboxes remain clean white with dark text */ |
| .gr-dropdown, .gr-input, .gr-box, select, input, textarea { |
| background-color: #ffffff !important; |
| border: 1px solid #e6dfd5 !important; |
| border-radius: 4px !important; |
| color: #231f1d !important; |
| } |
| |
| .dark select, .dark input, .dark .gr-dropdown { |
| background-color: #ffffff !important; |
| color: #231f1d !important; |
| } |
| |
| /* ── RED TEAM TERMINAL — warm dark crimson tint ── */ |
| .red-terminal textarea, |
| .red-terminal textarea:focus, |
| .red-terminal textarea:disabled, |
| .red-terminal textarea[readonly], |
| .dark .red-terminal textarea, |
| .dark .red-terminal textarea:focus, |
| .dark .red-terminal textarea:disabled, |
| .dark .red-terminal textarea[readonly] { |
| background-color: #1c1110 !important; |
| background: #1c1110 !important; |
| border: 1px solid #3d1c18 !important; |
| border-left: 3px solid #802f1a !important; |
| border-radius: 4px !important; |
| color: #f5e6e0 !important; |
| -webkit-text-fill-color: #f5e6e0 !important; |
| opacity: 1 !important; |
| font-family: 'JetBrains Mono', 'Fira Code', monospace !important; |
| font-size: 0.875rem !important; |
| line-height: 1.7 !important; |
| padding: 20px 20px 20px 18px !important; |
| resize: none !important; |
| text-shadow: none !important; |
| box-shadow: none !important; |
| caret-color: #f5e6e0 !important; |
| } |
| .red-terminal .wrap, |
| .dark .red-terminal .wrap { |
| background-color: #1c1110 !important; |
| border-color: #3d1c18 !important; |
| } |
| .red-terminal label, .red-terminal .block-label, |
| .dark .red-terminal label, .dark .red-terminal .block-label { |
| color: #a06050 !important; |
| -webkit-text-fill-color: #a06050 !important; |
| background: transparent !important; |
| } |
| .red-terminal textarea::-webkit-scrollbar { width: 5px; } |
| .red-terminal textarea::-webkit-scrollbar-track { background: #1c1110; } |
| .red-terminal textarea::-webkit-scrollbar-thumb { background: #5a2018; border-radius: 3px; } |
| |
| .operation-terminal { |
| background: transparent !important; |
| border: none !important; |
| scroll-margin-top: 18px !important; |
| } |
| |
| .operation-shell { |
| background-color: #111312 !important; |
| border: 1px solid #25221f !important; |
| border-left: 3px solid #802f1a !important; |
| border-radius: 4px !important; |
| box-shadow: none !important; |
| min-height: 520px !important; |
| max-height: 68vh !important; |
| overflow: auto !important; |
| padding: 24px 28px !important; |
| animation: none !important; |
| transition: none !important; |
| scroll-behavior: auto !important; |
| } |
| |
| .operation-shell pre { |
| margin: 0 !important; |
| white-space: pre-wrap !important; |
| overflow-wrap: anywhere !important; |
| color: #f0e7d8 !important; |
| -webkit-text-fill-color: #f0e7d8 !important; |
| font-family: 'JetBrains Mono', 'Fira Code', monospace !important; |
| font-size: 0.88rem !important; |
| font-weight: 400 !important; |
| line-height: 1.62 !important; |
| text-shadow: none !important; |
| background: transparent !important; |
| } |
| |
| .operation-terminal pre, |
| .operation-terminal code, |
| .operation-terminal div, |
| .operation-shell pre, |
| .dark .operation-terminal pre, |
| .dark .operation-shell pre { |
| color: #f0e7d8 !important; |
| -webkit-text-fill-color: #f0e7d8 !important; |
| } |
| |
| .operation-shell, |
| .operation-shell *, |
| .operation-terminal, |
| .operation-terminal *, |
| .operation-terminal::before, |
| .operation-terminal::after, |
| .operation-terminal *::before, |
| .operation-terminal *::after { |
| animation: none !important; |
| transition: none !important; |
| opacity: 1 !important; |
| filter: none !important; |
| box-shadow: none !important; |
| } |
| |
| .operation-shell::-webkit-scrollbar { width: 6px; } |
| .operation-shell::-webkit-scrollbar-track { background: #111312; } |
| .operation-shell::-webkit-scrollbar-thumb { background: #3b3029; border-radius: 3px; } |
| |
| /* ── BLUE TEAM TERMINAL — cool dark navy tint ── */ |
| .blue-terminal textarea, |
| .blue-terminal textarea:focus, |
| .blue-terminal textarea:disabled, |
| .blue-terminal textarea[readonly], |
| .dark .blue-terminal textarea, |
| .dark .blue-terminal textarea:focus, |
| .dark .blue-terminal textarea:disabled, |
| .dark .blue-terminal textarea[readonly] { |
| background-color: #0f1318 !important; |
| background: #0f1318 !important; |
| border: 1px solid #1a2a40 !important; |
| border-left: 3px solid #2a6496 !important; |
| border-radius: 4px !important; |
| color: #d8eaf8 !important; |
| -webkit-text-fill-color: #d8eaf8 !important; |
| opacity: 1 !important; |
| font-family: 'JetBrains Mono', 'Fira Code', monospace !important; |
| font-size: 0.875rem !important; |
| line-height: 1.7 !important; |
| padding: 20px 20px 20px 18px !important; |
| resize: none !important; |
| text-shadow: none !important; |
| box-shadow: none !important; |
| caret-color: #d8eaf8 !important; |
| } |
| .blue-terminal .wrap, |
| .dark .blue-terminal .wrap { |
| background-color: #0f1318 !important; |
| border-color: #1a2a40 !important; |
| } |
| .blue-terminal label, .blue-terminal .block-label, |
| .dark .blue-terminal label, .dark .blue-terminal .block-label { |
| color: #5a8aaa !important; |
| -webkit-text-fill-color: #5a8aaa !important; |
| background: transparent !important; |
| } |
| .blue-terminal textarea::-webkit-scrollbar { width: 5px; } |
| .blue-terminal textarea::-webkit-scrollbar-track { background: #0f1318; } |
| .blue-terminal textarea::-webkit-scrollbar-thumb { background: #1e4a6e; border-radius: 3px; } |
| |
| /* Premium Minimalist Button - Rust background */ |
| .launch-button, |
| .launch-button button, |
| button.primary, |
| button.primary span, |
| .launch-button span, |
| #launch-duel-button, |
| #launch-duel-button button, |
| #launch-duel-button span { |
| background-color: #802f1a !important; |
| color: #ffffff !important; |
| -webkit-text-fill-color: #ffffff !important; |
| border: 1px solid #802f1a !important; |
| border-radius: 4px !important; |
| font-weight: 500 !important; |
| font-family: 'Inter', sans-serif !important; |
| padding: 12px 24px !important; |
| transition: all 0.15s ease-in-out !important; |
| cursor: pointer !important; |
| } |
| |
| #launch-duel-button *, |
| .launch-button * { |
| color: #ffffff !important; |
| -webkit-text-fill-color: #ffffff !important; |
| } |
| |
| .launch-button:hover, |
| .launch-button:hover button, |
| button.primary:hover { |
| background-color: #692412 !important; |
| border-color: #692412 !important; |
| color: #ffffff !important; |
| -webkit-text-fill-color: #ffffff !important; |
| } |
| |
| .launch-button:focus, |
| .launch-button button:focus, |
| button.primary:focus { |
| outline: 2px solid #c99584 !important; |
| outline-offset: 2px !important; |
| } |
| |
| .launch-button:disabled, |
| .launch-button button:disabled, |
| button.primary:disabled { |
| background-color: #a88478 !important; |
| border-color: #a88478 !important; |
| color: #fff8f3 !important; |
| -webkit-text-fill-color: #fff8f3 !important; |
| } |
| |
| .live-button, |
| .live-button button, |
| .live-button span { |
| background-color: #ffffff !important; |
| color: #802f1a !important; |
| -webkit-text-fill-color: #802f1a !important; |
| border: 1px solid #c99584 !important; |
| border-radius: 4px !important; |
| font-weight: 500 !important; |
| font-family: 'Inter', sans-serif !important; |
| padding: 12px 24px !important; |
| } |
| |
| .live-button *, |
| .live-button button *, |
| .live-button span { |
| color: #802f1a !important; |
| -webkit-text-fill-color: #802f1a !important; |
| } |
| |
| .live-button:hover, |
| .live-button:hover button { |
| background-color: #f7f2eb !important; |
| border-color: #802f1a !important; |
| } |
| |
| .live-button:focus, |
| .live-button button:focus { |
| outline: 2px solid #c99584 !important; |
| outline-offset: 2px !important; |
| } |
| |
| .live-button:disabled, |
| .live-button button:disabled, |
| .live-button[aria-disabled="true"], |
| .live-button button[aria-disabled="true"], |
| .live-button.disabled, |
| .live-button .disabled { |
| background-color: #f2ece4 !important; |
| color: #594c43 !important; |
| -webkit-text-fill-color: #594c43 !important; |
| border-color: #d7cab9 !important; |
| opacity: 1 !important; |
| } |
| |
| .live-button:disabled *, |
| .live-button button:disabled *, |
| .live-button[aria-disabled="true"] *, |
| .live-button button[aria-disabled="true"] *, |
| .live-button.disabled *, |
| .live-button .disabled * { |
| color: #594c43 !important; |
| -webkit-text-fill-color: #594c43 !important; |
| } |
| |
| .action-row .live-button, |
| .action-row .live-button button, |
| .action-row .live-button button.secondary, |
| .action-row .live-button .secondary, |
| #live-gpu-button, |
| #live-gpu-button button, |
| #live-gpu-button button.secondary, |
| #live-gpu-button .secondary { |
| background: #ffffff !important; |
| background-color: #ffffff !important; |
| border-color: #c99584 !important; |
| color: #802f1a !important; |
| -webkit-text-fill-color: #802f1a !important; |
| opacity: 1 !important; |
| } |
| |
| .action-row .live-button:hover, |
| .action-row .live-button:hover button, |
| .action-row .live-button button:hover, |
| .action-row .live-button button.secondary:hover, |
| .action-row .live-button .secondary:hover, |
| #live-gpu-button:hover, |
| #live-gpu-button:hover button, |
| #live-gpu-button button:hover, |
| #live-gpu-button button.secondary:hover, |
| #live-gpu-button .secondary:hover { |
| background: #f7f2eb !important; |
| background-color: #f7f2eb !important; |
| border-color: #802f1a !important; |
| color: #802f1a !important; |
| -webkit-text-fill-color: #802f1a !important; |
| opacity: 1 !important; |
| } |
| |
| .action-row .live-button:active, |
| .action-row .live-button button:active, |
| .action-row .live-button button.secondary:active, |
| .action-row .live-button:focus, |
| .action-row .live-button button:focus, |
| .action-row .live-button button.secondary:focus, |
| #live-gpu-button:active, |
| #live-gpu-button button:active, |
| #live-gpu-button button.secondary:active, |
| #live-gpu-button:focus, |
| #live-gpu-button button:focus, |
| #live-gpu-button button.secondary:focus { |
| background: #efe4d8 !important; |
| background-color: #efe4d8 !important; |
| border-color: #802f1a !important; |
| color: #802f1a !important; |
| -webkit-text-fill-color: #802f1a !important; |
| } |
| |
| .action-row .live-button button:disabled, |
| .action-row .live-button button.secondary:disabled, |
| #live-gpu-button button:disabled, |
| #live-gpu-button button.secondary:disabled { |
| background: #f2ece4 !important; |
| background-color: #f2ece4 !important; |
| border-color: #d7cab9 !important; |
| color: #594c43 !important; |
| -webkit-text-fill-color: #594c43 !important; |
| opacity: 1 !important; |
| } |
| |
| .action-row { |
| align-items: end !important; |
| gap: 20px !important; |
| } |
| |
| .action-row .scenario-selector { |
| flex: 1 1 auto !important; |
| } |
| |
| .action-row .launch-button, |
| .action-row .live-button { |
| flex: 0 0 auto !important; |
| min-width: 190px !important; |
| max-width: 260px !important; |
| } |
| |
| .action-row .launch-button button, |
| .action-row .live-button button { |
| min-height: 48px !important; |
| width: 100% !important; |
| padding: 10px 18px !important; |
| } |
| |
| @media (max-width: 900px) { |
| .action-row .launch-button, |
| .action-row .live-button { |
| min-width: 100% !important; |
| max-width: 100% !important; |
| } |
| } |
| |
| .hero { |
| text-align: center !important; |
| max-width: 860px !important; |
| margin: 2px auto 30px auto !important; |
| padding: 4px 0 0 0 !important; |
| } |
| |
| .hero-eyebrow { |
| color: #802f1a !important; |
| font-family: 'Inter', sans-serif !important; |
| font-size: 0.78rem !important; |
| font-weight: 700 !important; |
| letter-spacing: 0 !important; |
| margin: 0 0 8px 0 !important; |
| text-transform: uppercase !important; |
| } |
| |
| .hero h1 { |
| color: #231f1d !important; |
| font-family: 'Playfair Display', 'Georgia', serif !important; |
| font-size: 3rem !important; |
| line-height: 1.05 !important; |
| letter-spacing: 0 !important; |
| margin: 0 0 14px 0 !important; |
| font-weight: 700 !important; |
| } |
| |
| .hero-lead { |
| color: #3a332f !important; |
| font-family: 'Inter', sans-serif !important; |
| font-size: 1.04rem !important; |
| line-height: 1.62 !important; |
| max-width: 720px !important; |
| margin: 0 auto 18px auto !important; |
| } |
| |
| .hero-meta { |
| display: flex !important; |
| justify-content: center !important; |
| align-items: center !important; |
| gap: 8px !important; |
| flex-wrap: wrap !important; |
| } |
| |
| .hero-meta span { |
| background: #f2ece4 !important; |
| border: 1px solid #e6dfd5 !important; |
| border-radius: 999px !important; |
| color: #594c43 !important; |
| font-family: 'Inter', sans-serif !important; |
| font-size: 0.82rem !important; |
| font-weight: 600 !important; |
| line-height: 1 !important; |
| padding: 8px 11px !important; |
| } |
| |
| /* Status Banner - Warm gray/beige banner with high contrast text */ |
| #status-banner { |
| background-color: #f2ece4 !important; |
| border: 1px solid #e6dfd5 !important; |
| border-radius: 4px !important; |
| padding: 14px 20px !important; |
| font-family: 'Inter', sans-serif !important; |
| font-size: 0.95rem !important; |
| font-weight: 600 !important; |
| } |
| |
| #status-banner, #status-banner p, #status-banner span { |
| color: #802f1a !important; |
| } |
| |
| .lab-note { |
| background-color: #f7f2eb !important; |
| border: 1px solid #e6dfd5 !important; |
| border-radius: 4px !important; |
| padding: 12px 16px !important; |
| margin: 4px 0 16px 0 !important; |
| color: #594c43 !important; |
| font-family: 'Inter', sans-serif !important; |
| font-size: 0.88rem !important; |
| line-height: 1.55 !important; |
| } |
| |
| .lab-note strong { |
| color: #802f1a !important; |
| font-weight: 600 !important; |
| } |
| |
| /* Labels and Markdown titles */ |
| h3 { |
| font-family: 'Playfair Display', serif !important; |
| color: #231f1d !important; |
| font-weight: 700 !important; |
| margin-top: 10px !important; |
| } |
| |
| .block-label, .gr-label { |
| color: #594c43 !important; |
| font-family: 'Inter', sans-serif !important; |
| font-size: 0.75rem !important; |
| text-transform: uppercase !important; |
| letter-spacing: 0 !important; |
| } |
| |
| /* Session logs and text styling */ |
| #download-logs, #download-logs * { |
| background-color: #ffffff !important; |
| color: #231f1d !important; |
| } |
| #log-textbox textarea { |
| background-color: #ffffff !important; |
| color: #231f1d !important; |
| font-family: 'JetBrains Mono', monospace !important; |
| font-size: 0.85rem !important; |
| } |
| .terminal-header { |
| display: flex !important; |
| justify-content: space-between !important; |
| align-items: center !important; |
| margin-top: 15px !important; |
| margin-bottom: 5px !important; |
| } |
| .terminal-title h3 { |
| margin: 0 !important; |
| } |
| #copy-logs-btn { |
| margin: 0 !important; |
| padding: 6px 14px !important; |
| font-size: 0.85rem !important; |
| font-family: 'Inter', sans-serif !important; |
| background-color: #ffffff !important; |
| border: 1px solid #e6dfd5 !important; |
| color: #231f1d !important; |
| border-radius: 4px !important; |
| cursor: pointer !important; |
| height: auto !important; |
| min-height: unset !important; |
| } |
| #copy-logs-btn:hover { |
| background-color: #f2ece4 !important; |
| border-color: #802f1a !important; |
| color: #802f1a !important; |
| } |
| """ |
|
|
| SCENARIO_CHOICES = [ |
| "Scenario 1: Insecure Configuration File (Secret Leak)", |
| "Scenario 2: Exposed Database Port (Global Binding)", |
| "Scenario 3: Unencrypted Communication Pipeline (MITM)", |
| "Lab Pack 4: DVWA-style SQL Injection & Weak Sessions", |
| "Lab Pack 5: Juice Shop-style Broken Auth & API Policy", |
| "Lab Pack 6: WebGoat-style Deserialization & Upload Risk" |
| ] |
|
|
| SCENARIO_MAP = { |
| "Scenario 1: Insecure Configuration File (Secret Leak)": 1, |
| "Scenario 2: Exposed Database Port (Global Binding)": 2, |
| "Scenario 3: Unencrypted Communication Pipeline (MITM)": 3, |
| "Lab Pack 4: DVWA-style SQL Injection & Weak Sessions": 4, |
| "Lab Pack 5: Juice Shop-style Broken Auth & API Policy": 5, |
| "Lab Pack 6: WebGoat-style Deserialization & Upload Risk": 6 |
| } |
|
|
| LIVE_GPU_LOCK = threading.Lock() |
|
|
| DEMO_SCENARIOS = { |
| 1: { |
| "files": ["app_config.json", "server.env", "deploy.sh"], |
| "cwes": ["CWE-312", "CWE-312", "CWE-732"], |
| "issues": [ |
| "Hardcoded API key and database password", |
| "Cloud and payment credentials committed to env file", |
| "World-readable deploy script with embedded DB password", |
| ], |
| "fixes": [ |
| "Moved secrets to environment references and disabled debug", |
| "Removed live keys and replaced them with secret-manager placeholders", |
| "Removed embedded password and hardened deploy.sh to owner-only execution", |
| ], |
| }, |
| 2: { |
| "files": ["db_settings.yaml", "nginx.conf", "firewall_rules.json"], |
| "cwes": ["CWE-306", "CWE-327", "CWE-284"], |
| "issues": [ |
| "Database binds publicly without authentication", |
| "Weak SSL protocols and ciphers enabled", |
| "Inbound firewall allows all TCP/UDP ports from the internet", |
| ], |
| "fixes": [ |
| "Bound database to localhost and required authentication", |
| "Restricted TLS to modern protocols and strong ciphers", |
| "Replaced broad inbound rules with least-privilege allowlist", |
| ], |
| }, |
| 3: { |
| "files": ["pipeline_config.json", "traffic_stream.log", "api_gateway.json"], |
| "cwes": ["CWE-319", "CWE-532", "CWE-306"], |
| "issues": [ |
| "Billing stream sends data over HTTP", |
| "Traffic log contains card data and CVV", |
| "Admin API is public with no auth or rate limit", |
| ], |
| "fixes": [ |
| "Upgraded endpoint to HTTPS with certificate verification", |
| "Scrubbed card data and CVV from logs", |
| "Enabled auth, rate limiting, and private admin routes", |
| ], |
| }, |
| 4: { |
| "files": ["login_handler.py", "session_config.json", "access_audit.log"], |
| "cwes": ["CWE-89", "CWE-352", "CWE-532"], |
| "issues": [ |
| "Login flow builds SQL with string interpolation", |
| "Session cookies are weak and CSRF is disabled", |
| "Access logs expose password payloads and reset tokens", |
| ], |
| "fixes": [ |
| "Rewrote login query with parameterized inputs", |
| "Rotated session secret and enabled secure cookies plus CSRF", |
| "Removed credential payloads and reset tokens from audit logs", |
| ], |
| }, |
| 5: { |
| "files": ["auth_routes.js", "api_policy.json", "payment_debug.log"], |
| "cwes": ["CWE-347", "CWE-942", "CWE-532"], |
| "issues": [ |
| "JWT admin check trusts decoded unsigned tokens", |
| "API policy allows wildcard CORS and public admin access", |
| "Payment debug log leaks card data and JWT-like tokens", |
| ], |
| "fixes": [ |
| "Switched to verified JWTs with env-backed signing secret", |
| "Scoped CORS, required auth, added rate limit, and privatized admin routes", |
| "Scrubbed card data, CVV, and token values from debug logs", |
| ], |
| }, |
| 6: { |
| "files": ["profile_importer.py", "upload_policy.yaml", "worker_permissions.json"], |
| "cwes": ["CWE-502", "CWE-434", "CWE-250"], |
| "issues": [ |
| "Profile importer deserializes untrusted pickle data", |
| "Upload policy accepts wildcard executable content", |
| "Worker runs as root with shell and unrestricted egress", |
| ], |
| "fixes": [ |
| "Replaced pickle with JSON parsing and schema validation", |
| "Restricted upload types, enabled scanning, and moved storage outside webroot", |
| "Dropped root privileges, disabled shell spawning, and restricted egress", |
| ], |
| }, |
| } |
|
|
| def clean_console(text): |
| text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text) |
| replacements = { |
| "✓": "PASS", |
| "✗": "FAIL", |
| "▶": "...", |
| "—": "-", |
| "–": "-", |
| "→": "->", |
| "─": "-", |
| "━": "-", |
| "╔": "+", |
| "╗": "+", |
| "╚": "+", |
| "╝": "+", |
| "║": "|", |
| "├": "|", |
| "│": "|", |
| "└": "|", |
| "\\n": "\n", |
| } |
| for old, new in replacements.items(): |
| text = text.replace(old, new) |
| return text |
|
|
| def format_operation_trace(red_out, blue_out, backend_label="Hugging Face Space UI -> Modal GPU worker"): |
| red = clean_console(red_out or "").strip() |
| blue = clean_console(blue_out or "").strip() |
|
|
| lines = [ |
| "VYBER OPERATION TRACE", |
| "mode : autonomous cyber-range", |
| "model : vxkyyy/vyber-security-7b-gguf", |
| f"backend : {backend_label}", |
| "workspace : /tmp/sandbox", |
| "", |
| "PHASE 01 / RED TEAM DISCOVERY", |
| "------------------------------------------------------------", |
| ] |
|
|
| if red: |
| lines.append(red) |
| else: |
| lines.append("waiting for target initialization") |
|
|
| blue_ready = any(marker in (red_out or "") for marker in [ |
| "EXPLOIT REPORT COMMITTED", |
| "SCOREBOARD", |
| "FINAL VERDICT" |
| ]) or any(marker in (blue_out or "") for marker in [ |
| "BLUE AGENT", |
| "PATCH_COMPLETE", |
| "ACTION", |
| "FINAL VERDICT" |
| ]) |
|
|
| if blue_ready and blue: |
| lines.extend([ |
| "", |
| "PHASE 02 / BLUE TEAM REMEDIATION", |
| "------------------------------------------------------------", |
| blue, |
| ]) |
|
|
| text = "\n".join(lines) |
| return ( |
| "<div class='operation-shell'>" |
| "<pre style='color:#f0e7d8 !important;-webkit-text-fill-color:#f0e7d8 !important;'>" |
| + html.escape(text) + |
| "</pre></div>" |
| ) |
|
|
| def save_session_log(mode: str, scenario_id: int, red_out: str, blue_out: str) -> tuple[str, str]: |
| import datetime |
| |
| os.makedirs("logs", exist_ok=True) |
| |
| timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") |
| filename = f"logs/session_{timestamp}_{mode}_scenario_{scenario_id}.log" |
| |
| red_clean = clean_console(red_out or "").strip() |
| blue_clean = clean_console(blue_out or "").strip() |
| |
| log_content = ( |
| f"VYBER OPERATION LOG\n" |
| f"===================\n" |
| f"Timestamp: {datetime.datetime.now().isoformat()}\n" |
| f"Mode: {mode}\n" |
| f"Scenario: {scenario_id}\n\n" |
| f"PHASE 01 / RED TEAM DISCOVERY\n" |
| f"------------------------------------------------------------\n" |
| f"{red_clean}\n\n" |
| ) |
| if blue_clean: |
| log_content += ( |
| f"PHASE 02 / BLUE TEAM REMEDIATION\n" |
| f"------------------------------------------------------------\n" |
| f"{blue_clean}\n" |
| ) |
| |
| try: |
| with open(filename, "w", encoding="utf-8") as f: |
| f.write(log_content) |
| return filename, log_content |
| except Exception as e: |
| print(f"Failed to save session log: {e}") |
| return "", log_content |
|
|
| def run_demo_replay(scenario_name): |
| scenario_id = SCENARIO_MAP.get(scenario_name, 1) |
| demo = DEMO_SCENARIOS[scenario_id] |
| ids = [f"S{scenario_id}-V{i}" for i in range(1, 4)] |
|
|
| red = ( |
| "vyber-range v1.0 // red-team agent shell\n" |
| "----------------------------------------------\n" |
| f" [demo] scenario : {scenario_id}\n" |
| " [demo] status : target files deployed\n" |
| ) |
| yield format_operation_trace(red, "", "Demo replay (no GPU)"), "Status: Demo replay started - no GPU credits used", gr.update(visible=False), gr.update(visible=False) |
| time.sleep(0.4) |
|
|
| red += "\n+--------------------------------------------+\n" |
| red += "| THREAT SURFACE 3 VULNERABILITIES PLANTED |\n" |
| red += "+--------------------------------------------+\n" |
| for vuln_id, cwe, filename, issue in zip(ids, demo["cwes"], demo["files"], demo["issues"]): |
| red += f" [OPEN] {vuln_id} {cwe} {filename}\n" |
| red += f" {issue}\n" |
| yield format_operation_trace(red, "", "Demo replay (no GPU)"), "Status: Demo Red Agent mapped the threat surface", gr.update(visible=False), gr.update(visible=False) |
| time.sleep(0.5) |
|
|
| red += "\n+--------------------------------------------+\n" |
| red += "| ROUND 1 EXPLOIT REPORT COMMITTED |\n" |
| red += "+--------------------------------------------+\n" |
| for vuln_id, cwe, filename, issue in zip(ids, demo["cwes"], demo["files"], demo["issues"]): |
| red += f" FINDING {vuln_id}\n" |
| red += f" FILE: {filename}\n" |
| red += f" CWE: {cwe}\n" |
| red += " SEVERITY: High\n" |
| red += f" EVIDENCE: {issue}\n" |
| red += f" ATTACK_PATH: Red replays the lab exploit against {filename}.\n" |
| red += f" IMPACT: {issue}\n" |
| yield format_operation_trace(red, "", "Demo replay (no GPU)"), "Status: Demo Red Agent committed exploit report", gr.update(visible=False), gr.update(visible=False) |
| time.sleep(0.5) |
|
|
| blue = ( |
| "vyber-range v1.0 // blue-team SOC shell\n" |
| "----------------------------------------------\n" |
| " [demo] telemetry : active\n" |
| " [demo] watchdog : monitoring /tmp/sandbox/\n" |
| " [demo] status : exploit intel received\n" |
| "\n+--------------------------------------------+\n" |
| "| ROUND 1 BLUE AGENT VYBER HARNESS |\n" |
| "+--------------------------------------------+\n" |
| " [demo] launching Vyber patch harness\n" |
| ) |
| for vuln_id, filename, fix in zip(ids, demo["files"], demo["fixes"]): |
| blue += f" [PATCHED] {vuln_id} {filename}\n" |
| blue += f" {fix}\n" |
| yield format_operation_trace(red, blue, "Demo replay (no GPU)"), "Status: Demo Blue Agent patched all findings", gr.update(visible=False), gr.update(visible=False) |
| time.sleep(0.5) |
|
|
| verdict = "\n+--------------------------------------------+\n" |
| verdict += "| ROUND 1 RED RE-ATTACK VERIFICATION |\n" |
| verdict += "+--------------------------------------------+\n" |
| for vuln_id, cwe, filename in zip(ids, demo["cwes"], demo["files"]): |
| verdict += f" [BLOCKED] {vuln_id} {cwe} {filename}\n" |
| verdict += "\n [demo] verdict : PASS ALL 3 EXPLOITS BLOCKED BY PATCHES\n" |
| verdict += " [demo] result : SYSTEM SECURE\n" |
| blue += verdict |
| saved_path, log_content = save_session_log("demo", scenario_id, red, blue) |
| status_msg = "Status: Demo complete" |
| if saved_path: |
| status_msg += f" - Log saved to {saved_path}" |
| else: |
| status_msg += " - live GPU was not used" |
| yield format_operation_trace(red, blue, "Demo replay (no GPU)"), status_msg, (gr.update(value=saved_path, visible=True) if saved_path else gr.update(visible=False)), gr.update(value=log_content, visible=False) |
|
|
| def launch_duel(scenario_name): |
| scenario_id = SCENARIO_MAP.get(scenario_name, 1) |
|
|
| if not LIVE_GPU_LOCK.acquire(blocking=False): |
| busy = ( |
| "vyber-range v1.0 // live-gpu admission control\n" |
| "----------------------------------------------\n" |
| " status : live Modal GPU duel already running\n" |
| " action : run the demo replay now, or retry live mode shortly\n" |
| ) |
| yield format_operation_trace(busy, "", "Live Modal GPU worker"), "Status: Live GPU busy - demo replay is available", gr.update(visible=False), gr.update(visible=False) |
| return |
|
|
| last_red_out = "" |
| last_blue_out = "" |
| saved_successfully = False |
|
|
| try: |
| |
| yield format_operation_trace("", ""), "Status: Connecting to serverless GPU backend (provisioning node and loading weights)...", gr.update(visible=False), gr.update(visible=False) |
|
|
| |
| openai_api_key = os.environ.get("OPENAI_API_KEY", "") |
| try: |
| |
| f = modal.Function.from_name("cyber-defense-range", "run_duel_stream") |
| |
| for red_out, blue_out, banner_txt in f.remote_gen(scenario_id, openai_api_key=openai_api_key): |
| last_red_out = red_out |
| last_blue_out = blue_out |
| yield format_operation_trace(red_out, blue_out), f"Status: {clean_console(banner_txt)}", gr.update(visible=False), gr.update(visible=False) |
| saved_successfully = True |
| except Exception as e: |
| print(f"Modal Remote Gen lookup failed: {e}. Falling back to local execution.") |
| |
| try: |
| |
| from backend import run_duel_stream |
| |
| for red_out, blue_out, banner_txt in run_duel_stream.local(scenario_id, openai_api_key=openai_api_key): |
| last_red_out = red_out |
| last_blue_out = blue_out |
| yield format_operation_trace(red_out, blue_out), f"Status: {clean_console(banner_txt)}", gr.update(visible=False), gr.update(visible=False) |
| saved_successfully = True |
| except Exception as local_err: |
| print(f"Live GPU backend unavailable. Modal detail: {e}. Local fallback detail: {local_err}") |
| error_msg = ( |
| "Live GPU backend is currently paused to save compute cost.\n" |
| "Run Demo Replay now, or retry Live GPU Duel during the judging window." |
| ) |
| yield format_operation_trace(clean_console(error_msg), ""), "Status: Live GPU paused - demo replay is available", gr.update(visible=False), gr.update(visible=False) |
|
|
| if saved_successfully and (last_red_out or last_blue_out): |
| saved_path, log_content = save_session_log("live", scenario_id, last_red_out, last_blue_out) |
| if saved_path: |
| yield format_operation_trace(last_red_out, last_blue_out), f"Status: Live GPU Duel complete - Log saved to {saved_path}", gr.update(value=saved_path, visible=True), gr.update(value=log_content, visible=False) |
| else: |
| yield format_operation_trace(last_red_out, last_blue_out), "Status: Live GPU Duel complete", gr.update(visible=False), gr.update(value=log_content, visible=False) |
| finally: |
| LIVE_GPU_LOCK.release() |
|
|
| |
| with gr.Blocks(theme=gr.themes.Default(primary_hue="zinc", secondary_hue="zinc"), css=css) as demo: |
| gr.HTML( |
| "<section class='hero'>" |
| "<p class='hero-eyebrow'>Autonomous Cyber-Range</p>" |
| "<h1>Vyber Duel</h1>" |
| "<p class='hero-lead'>" |
| "Red finds the exploit, Blue patches the code, and Red replays the same attack to verify the fix inside an isolated sandbox." |
| "</p>" |
| "<div class='hero-meta'>" |
| "<span>Fine-tuned 7B security model</span>" |
| "<span>llama.cpp on Modal GPU</span>" |
| "<span>First run warms hardware and loads GGUF weights</span>" |
| "</div>" |
| "</section>" |
| ) |
| |
| with gr.Row(elem_classes=["action-row"]): |
| scenario_dropdown = gr.Dropdown( |
| choices=SCENARIO_CHOICES, |
| value="Scenario 1: Insecure Configuration File (Secret Leak)", |
| label="Target Cyber-Range Scenario", |
| elem_classes=["scenario-selector"], |
| scale=1 |
| ) |
| launch_btn = gr.Button("Launch Live GPU Duel", variant="primary", elem_id="launch-duel-button", elem_classes=["launch-button"], scale=0, min_width=220) |
| demo_btn = gr.Button("Run Demo Replay", variant="secondary", elem_id="live-gpu-button", elem_classes=["live-button"], scale=0, min_width=190) |
|
|
| gr.HTML( |
| "<div class='lab-note'>" |
| "<strong>Live GPU Duel:</strong> Automatically launches the autonomous Red Agent and Blue Agent patch loop. " |
| "Demo Replay runs the no-GPU simulated trace." |
| "</div>" |
| ) |
| |
| status_banner = gr.Markdown("Status: Active sandbox waiting for execution command", elem_id="status-banner") |
| |
| download_logs = gr.File(label="Download Session Log File", visible=False, elem_id="download-logs") |
| log_textbox = gr.Textbox(label="Session Log (Copy to Clipboard)", visible=False, elem_id="log-textbox", show_copy_button=True, lines=10) |
| |
| with gr.Row(elem_classes=["terminal-header"]): |
| gr.Markdown("### Operation Terminal", elem_classes=["terminal-title"]) |
| copy_btn = gr.Button("Copy Session Log", variant="secondary", elem_id="copy-logs-btn", scale=0, min_width=170) |
|
|
| operation_terminal = gr.HTML( |
| value=format_operation_trace("", ""), |
| elem_id="operation-terminal", |
| elem_classes=["operation-terminal"] |
| ) |
| |
| demo_btn.click( |
| fn=run_demo_replay, |
| inputs=scenario_dropdown, |
| outputs=[operation_terminal, status_banner, download_logs, log_textbox], |
| scroll_to_output=True, |
| concurrency_limit=20, |
| concurrency_id="demo_replay" |
| ) |
|
|
| launch_btn.click( |
| fn=launch_duel, |
| inputs=scenario_dropdown, |
| outputs=[operation_terminal, status_banner, download_logs, log_textbox], |
| scroll_to_output=True, |
| concurrency_limit=1, |
| concurrency_id="live_gpu_duel" |
| ) |
|
|
| copy_btn.click( |
| fn=None, |
| inputs=[log_textbox], |
| js="""(text) => { |
| let finalText = text; |
| if (!finalText) { |
| const pre = document.querySelector('.operation-shell pre'); |
| if (pre) { |
| finalText = pre.innerText; |
| } |
| } |
| if (finalText) { |
| try { |
| if (navigator.clipboard && navigator.clipboard.writeText) { |
| navigator.clipboard.writeText(finalText).then(() => { |
| updateButtonText(); |
| }).catch(err => { |
| fallbackCopy(finalText); |
| }); |
| } else { |
| fallbackCopy(finalText); |
| } |
| } catch (e) { |
| fallbackCopy(finalText); |
| } |
| } else { |
| alert('No logs to copy yet. Run a duel first.'); |
| } |
| |
| function fallbackCopy(val) { |
| try { |
| const textArea = document.createElement("textarea"); |
| textArea.value = val; |
| textArea.style.top = "0"; |
| textArea.style.left = "0"; |
| textArea.style.position = "fixed"; |
| document.body.appendChild(textArea); |
| textArea.focus(); |
| textArea.select(); |
| const successful = document.execCommand('copy'); |
| document.body.removeChild(textArea); |
| if (successful) { |
| updateButtonText(); |
| } else { |
| alert('Copying is blocked by your browser settings in this sandbox.'); |
| } |
| } catch (err) { |
| alert('Failed to copy text: ' + err); |
| } |
| } |
| |
| function updateButtonText() { |
| const btn = document.getElementById('copy-logs-btn'); |
| if (btn) { |
| const span = btn.querySelector('span') || btn; |
| const oldText = span.innerText; |
| span.innerText = 'Copied!'; |
| setTimeout(() => { span.innerText = oldText; }, 2000); |
| } |
| } |
| }""" |
| ) |
|
|
| demo.queue(max_size=50, default_concurrency_limit=2) |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |
|
|