Spaces:
Sleeping
Sleeping
| """ | |
| Modern Gradio Web Interface for Security Auditor | |
| Based on design mockups in assets/ folder | |
| """ | |
| import gradio as gr | |
| import asyncio | |
| import tempfile | |
| import shutil | |
| from pathlib import Path | |
| from datetime import datetime | |
| import uuid | |
| import os | |
| import sys | |
| import time | |
| import threading | |
| import json | |
| import base64 | |
| import re | |
| # Import existing security checker | |
| from security_checker import SecurityChecker, RiskLevel | |
| # Import custom theme and UI components | |
| from theme import create_security_auditor_theme | |
| from ui_components import ( | |
| create_severity_badge, | |
| create_finding_card, | |
| create_summary_section, | |
| create_empty_state, | |
| create_loading_state | |
| ) | |
| class ModernSecurityAuditorApp: | |
| """ | |
| Modern Gradio web interface for Security Auditor. | |
| Based on design mockups in assets/ folder. | |
| """ | |
| def __init__(self): | |
| self.checker = SecurityChecker() | |
| self.active_sessions = {} | |
| self.cleanup_interval = 3600 # 1 hour | |
| self.max_upload_size_mb = 100 | |
| self._help_html = self._prepare_help_content() | |
| def _prepare_help_content(self): | |
| """Read help.html and embed images as base64 data URIs for self-contained display.""" | |
| help_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "help.html") | |
| img_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "docimages") | |
| try: | |
| with open(help_path, "r", encoding="utf-8") as f: | |
| html = f.read() | |
| except FileNotFoundError: | |
| return "<html><body><h1>Help file not found</h1></body></html>" | |
| def replace_img_src(match): | |
| filename = match.group(1) | |
| img_path = os.path.join(img_dir, filename) | |
| try: | |
| with open(img_path, "rb") as img_f: | |
| b64 = base64.b64encode(img_f.read()).decode("ascii") | |
| return f'src="data:image/png;base64,{b64}"' | |
| except FileNotFoundError: | |
| return match.group(0) | |
| html = re.sub(r'src="/helpimg/([^"]+)"', replace_img_src, html) | |
| return html | |
| def create_interface(self) -> gr.Blocks: | |
| """Create modern Gradio interface matching design mockups.""" | |
| self.theme = create_security_auditor_theme() | |
| # Prepare help HTML as JSON-safe string for client-side Blob URL | |
| self._help_js = json.dumps(self._help_html) | |
| # Custom CSS for additional styling | |
| self.custom_css = """ | |
| /* Tabler Icons CDN */ | |
| @import url('https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css'); | |
| /* Anthropic-inspired Theme Overrides */ | |
| :root { | |
| --anthropic-cream: #faf9f6; | |
| --anthropic-slate: #131314; | |
| --anthropic-terracotta: #d97757; | |
| --anthropic-terracotta-hover: #cc6944; | |
| --anthropic-gray: #6b7280; | |
| --anthropic-border: #e5e7eb; | |
| } | |
| /* Logo and branding - Anthropic style */ | |
| .logo-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 20px 24px; | |
| border-bottom: 1px solid var(--anthropic-border); | |
| background: var(--anthropic-cream); | |
| } | |
| .logo-icon { | |
| width: 40px; | |
| height: 40px; | |
| background: var(--anthropic-slate); | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| } | |
| .logo-icon svg { | |
| width: 24px; | |
| height: 24px; | |
| stroke: white; | |
| } | |
| /* Section headers - cleaner, less shouty */ | |
| .section-header { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--anthropic-gray); | |
| text-transform: none; | |
| letter-spacing: normal; | |
| margin: 20px 0 12px 0; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .section-header svg { | |
| width: 18px; | |
| height: 18px; | |
| stroke: var(--anthropic-gray); | |
| } | |
| /* Mode selector header */ | |
| .mode-selector-header { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--anthropic-gray); | |
| text-transform: none; | |
| letter-spacing: normal; | |
| margin-bottom: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .mode-selector-header svg { | |
| width: 18px; | |
| height: 18px; | |
| stroke: var(--anthropic-gray); | |
| } | |
| /* Gradio overrides */ | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| } | |
| /* Card styling */ | |
| .card-title { | |
| margin: 0 0 8px 0; | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: var(--anthropic-slate); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .card-title svg { | |
| width: 20px; | |
| height: 20px; | |
| stroke: var(--anthropic-terracotta); | |
| } | |
| .card-description { | |
| margin: 0 0 16px 0; | |
| color: var(--anthropic-gray); | |
| font-size: 14px; | |
| } | |
| /* NVD Toggle Switch Styling */ | |
| .nvd-toggle { | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| } | |
| .nvd-toggle input[type="checkbox"] { | |
| appearance: none; | |
| -webkit-appearance: none; | |
| width: 48px; | |
| height: 26px; | |
| background: #e5e7eb; | |
| border-radius: 13px; | |
| position: relative; | |
| cursor: pointer; | |
| transition: background 0.3s ease; | |
| margin: 0; | |
| } | |
| .nvd-toggle input[type="checkbox"]:checked { | |
| background: var(--anthropic-terracotta); | |
| } | |
| .nvd-toggle input[type="checkbox"]::before { | |
| content: ""; | |
| position: absolute; | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 50%; | |
| background: white; | |
| top: 2px; | |
| left: 2px; | |
| transition: left 0.3s ease; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| .nvd-toggle input[type="checkbox"]:checked::before { | |
| left: 24px; | |
| } | |
| .nvd-toggle label { | |
| display: flex; | |
| align-items: center; | |
| cursor: pointer; | |
| } | |
| /* Tooltip positioning */ | |
| .info-icon-tooltip { | |
| position: relative; | |
| } | |
| /* Show tooltip on hover over the NVD label row */ | |
| .nvd-label-row:hover .info-tooltip-text { | |
| visibility: visible !important; | |
| opacity: 1 !important; | |
| } | |
| /* Info badge - subtle, minimal */ | |
| .info-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 16px; | |
| background: #ffffff; | |
| border: 1px solid var(--anthropic-border); | |
| border-radius: 6px; | |
| margin-top: 20px; | |
| } | |
| .info-badge-icon svg { | |
| width: 24px; | |
| height: 24px; | |
| stroke: var(--anthropic-terracotta); | |
| } | |
| .info-badge-content { | |
| flex: 1; | |
| } | |
| .info-badge-title { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--anthropic-slate); | |
| margin-bottom: 2px; | |
| } | |
| .info-badge-subtitle { | |
| font-size: 12px; | |
| color: var(--anthropic-gray); | |
| } | |
| /* Analyze button - Anthropic terracotta */ | |
| .analyze-button { | |
| height: 100% !important; | |
| min-height: 56px !important; | |
| font-size: 15px !important; | |
| font-weight: 600 !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| gap: 8px !important; | |
| background: var(--anthropic-terracotta) !important; | |
| border: none !important; | |
| border-radius: 6px !important; | |
| transition: background 0.2s ease !important; | |
| } | |
| .analyze-button:hover { | |
| background: var(--anthropic-terracotta-hover) !important; | |
| } | |
| .analyze-button svg { | |
| width: 20px; | |
| height: 20px; | |
| stroke: white; | |
| } | |
| /* Reset button - prominent */ | |
| .reset-button { | |
| min-height: 44px !important; | |
| font-size: 15px !important; | |
| font-weight: 600 !important; | |
| background: var(--anthropic-terracotta) !important; | |
| border: none !important; | |
| border-radius: 6px !important; | |
| color: white !important; | |
| transition: background 0.2s ease !important; | |
| } | |
| .reset-button:hover { | |
| background: var(--anthropic-terracotta-hover) !important; | |
| } | |
| /* Help button - outlined, distinct from primary actions */ | |
| .help-button { | |
| min-height: 44px !important; | |
| font-size: 15px !important; | |
| font-weight: 600 !important; | |
| background: transparent !important; | |
| border: 2px solid var(--anthropic-border) !important; | |
| border-radius: 6px !important; | |
| color: var(--anthropic-gray) !important; | |
| transition: all 0.2s ease !important; | |
| margin-top: 8px !important; | |
| } | |
| .help-button:hover { | |
| border-color: var(--anthropic-terracotta) !important; | |
| color: var(--anthropic-terracotta) !important; | |
| background: rgba(217, 119, 87, 0.05) !important; | |
| } | |
| /* Export button styling */ | |
| .export-button { | |
| display: inline-flex !important; | |
| align-items: center !important; | |
| gap: 6px !important; | |
| } | |
| /* Button icons via pseudo-elements */ | |
| .analyze-button::before { | |
| content: ""; | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cpath d='m21 21-4.35-4.35'%3E%3C/path%3E%3C/svg%3E"); | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| } | |
| .export-button::before { | |
| content: ""; | |
| display: inline-block; | |
| width: 18px; | |
| height: 18px; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'%3E%3C/path%3E%3Cpolyline points='7 10 12 15 17 10'%3E%3C/polyline%3E%3Cline x1='12' y1='15' x2='12' y2='3'%3E%3C/line%3E%3C/svg%3E"); | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| } | |
| /* Severity badges - static visual indicators */ | |
| .severity-badge { | |
| position: relative; | |
| } | |
| /* JavaScript for moving NVD toggle */ | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Move NVD toggle into the container | |
| setTimeout(function() { | |
| const container = document.getElementById('nvd-toggle-container'); | |
| const toggle = document.querySelector('.nvd-toggle'); | |
| if (container && toggle) { | |
| container.appendChild(toggle); | |
| } | |
| }, 100); | |
| }); | |
| </script> | |
| """ | |
| # Google Analytics 4 tracking | |
| ga_head = """ | |
| <script async src="https://www.googletagmanager.com/gtag/js?id=G-BLV0DJP20J"></script> | |
| <script> | |
| window.dataLayer = window.dataLayer || []; | |
| function gtag(){dataLayer.push(arguments);} | |
| gtag('js', new Date()); | |
| gtag('config', 'G-BLV0DJP20J'); | |
| </script> | |
| """ | |
| with gr.Blocks(title="Security Auditor", head=ga_head) as app: | |
| # Header with logo | |
| gr.HTML(""" | |
| <div class="logo-container"> | |
| <div class="logo-icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" | |
| fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> | |
| </svg> | |
| </div> | |
| <div> | |
| <h1 style="margin: 0; font-size: 18px; font-weight: 700; color: #131314;">Security Auditor</h1> | |
| </div> | |
| </div> | |
| """) | |
| gr.Markdown("## Scan your application code for security vulnerabilities and get remediation guidance") | |
| # Main layout | |
| with gr.Row(): | |
| # Sidebar | |
| with gr.Column(scale=1, min_width=250): | |
| # Analysis Mode with integrated settings | |
| with gr.Group(): | |
| gr.HTML('''<div class="mode-selector-header"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" | |
| fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="12" y1="20" x2="12" y2="10"></line> | |
| <line x1="18" y1="20" x2="18" y2="4"></line> | |
| <line x1="6" y1="20" x2="6" y2="16"></line> | |
| </svg> | |
| Analysis Mode | |
| </div>''') | |
| scan_mode = gr.Radio( | |
| choices=["Local Directory", "Remote URL"], | |
| value="Local Directory", | |
| label="", | |
| container=False, | |
| interactive=True | |
| ) | |
| # NVD Enrichment toggle with info icon - side by side layout | |
| gr.HTML(''' | |
| <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 16px; gap: 12px;"> | |
| <div class="nvd-label-row" style="display: flex; align-items: center; gap: 8px; flex-shrink: 1; min-width: 0; cursor: default;"> | |
| <span style=" | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #131314; | |
| white-space: nowrap; | |
| ">NVD Enriched Scan Results</span> | |
| <div class="info-icon-tooltip" style="position: relative; display: inline-flex; align-items: center; flex-shrink: 0;"> | |
| <svg class="info-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" | |
| fill="none" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="cursor: pointer; display: block;" | |
| onclick="toggleTooltip(this)"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="12" y1="16" x2="12" y2="12"></line> | |
| <line x1="12" y1="8" x2="12.01" y2="8"></line> | |
| </svg> | |
| <div class="info-tooltip-text" style=" | |
| visibility: hidden; | |
| opacity: 0; | |
| position: absolute; | |
| bottom: calc(100% + 8px); | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #131314; | |
| color: white; | |
| padding: 12px 16px; | |
| border-radius: 6px; | |
| font-size: 13px; | |
| font-weight: 400; | |
| white-space: normal; | |
| width: 280px; | |
| line-height: 1.5; | |
| z-index: 9999; | |
| transition: opacity 0.2s, visibility 0.2s; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); | |
| "> | |
| Enriches scan results with related Common Vulnerabilities and Exposures (CVE) references from the National Vulnerability Database (NVD). | |
| </div> | |
| </div> | |
| </div> | |
| <div id="nvd-toggle-container" style="flex-shrink: 0;"></div> | |
| </div> | |
| <script> | |
| (function() { | |
| var outsideClickHandler = null; | |
| window.toggleTooltip = function(icon) { | |
| var tooltip = icon.nextElementSibling; | |
| var isVisible = tooltip.style.visibility === 'visible'; | |
| // Hide all tooltips first | |
| document.querySelectorAll('.info-tooltip-text').forEach(function(t) { | |
| t.style.visibility = 'hidden'; | |
| t.style.opacity = '0'; | |
| }); | |
| // Remove any existing outside click handler | |
| if (outsideClickHandler) { | |
| document.removeEventListener('click', outsideClickHandler); | |
| outsideClickHandler = null; | |
| } | |
| if (!isVisible) { | |
| tooltip.style.visibility = 'visible'; | |
| tooltip.style.opacity = '1'; | |
| setTimeout(function() { | |
| outsideClickHandler = function(e) { | |
| if (!icon.contains(e.target) && !tooltip.contains(e.target)) { | |
| tooltip.style.visibility = 'hidden'; | |
| tooltip.style.opacity = '0'; | |
| document.removeEventListener('click', outsideClickHandler); | |
| outsideClickHandler = null; | |
| } | |
| }; | |
| document.addEventListener('click', outsideClickHandler); | |
| }, 100); | |
| } | |
| }; | |
| })(); | |
| </script> | |
| ''') | |
| nvd_enrichment = gr.Checkbox( | |
| label=None, | |
| value=True, | |
| elem_classes=["nvd-toggle"], | |
| container=False, | |
| show_label=False | |
| ) | |
| # Not necessary to display Actions label with custom lightbolt icon as there is only one action. | |
| # gr.HTML('''<div class="section-header"> | |
| # <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" | |
| # fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| # <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon> | |
| # </svg> | |
| # Actions | |
| # </div>''') | |
| reset_btn = gr.Button("Reset", variant="primary", size="lg", elem_classes=["reset-button"]) | |
| help_btn = gr.Button("Help", variant="secondary", size="lg", elem_classes=["help-button"]) | |
| # Info badge - Engine information | |
| gr.HTML(''' | |
| <div class="info-badge"> | |
| <div class="info-badge-icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" | |
| fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path> | |
| </svg> | |
| </div> | |
| <div class="info-badge-content"> | |
| <div class="info-badge-title">SAST + DAST + NVD</div> | |
| <div class="info-badge-subtitle">40+ Vulnerability Checks</div> | |
| </div> | |
| </div> | |
| ''') | |
| # Main content area | |
| with gr.Column(scale=3): | |
| # Input section (changes based on mode) | |
| with gr.Group(): | |
| # Local Directory mode | |
| with gr.Column(visible=True) as local_mode: | |
| gr.HTML(""" | |
| <div style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; margin-bottom: 16px;"> | |
| <h3 class="card-title"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path> | |
| </svg> | |
| Application Directory | |
| </h3> | |
| <p class="card-description">Scan local directory containing the application code.</p> | |
| </div> | |
| """) | |
| file_upload = gr.File( | |
| label="Upload Files - Total Size Maximum 25 MB", | |
| file_count="multiple", | |
| file_types=[".py", ".js", ".ts", ".java", ".php", ".go", ".rb", ".c", ".cpp", ".cs", ".swift", ".kt", ".scala", ".rs", ".jsx", ".tsx"], | |
| height=120 | |
| ) | |
| gr.HTML(""" | |
| <p style="color: #9ca3af; font-size: 12px; margin: -8px 0 16px 0; line-height: 1.5;"> | |
| Accepted: .py, .js, .ts, .java, .php, .go, .rb, .c, .cpp, .cs, .swift, .kt, .scala, .rs, .jsx, .tsx | |
| </p> | |
| """) | |
| directory_path = gr.Textbox( | |
| label="Or Enter Directory Path", | |
| placeholder="C:/Projects/my-application", | |
| lines=2, | |
| max_lines=3 | |
| ) | |
| analyze_btn_local = gr.Button( | |
| "Analyze", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["analyze-button"] | |
| ) | |
| # Remote URL mode | |
| with gr.Column(visible=False) as url_mode: | |
| gr.HTML(""" | |
| <div style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; margin-bottom: 16px;"> | |
| <h3 class="card-title"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="2" y1="12" x2="22" y2="12"></line> | |
| <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> | |
| </svg> | |
| Application URL | |
| </h3> | |
| <p class="card-description">Scan remote web application or deployment.</p> | |
| </div> | |
| """) | |
| web_url = gr.Textbox( | |
| label="Web Application URL", | |
| placeholder="https://your-app.example.com", | |
| lines=2, | |
| max_lines=3 | |
| ) | |
| analyze_btn_url = gr.Button( | |
| "Analyze", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["analyze-button"] | |
| ) | |
| # Progress indicator | |
| progress_box = gr.HTML(value=create_empty_state()) | |
| # Results section | |
| results_section = gr.Column(visible=False) | |
| with results_section: | |
| # Analysis Summary | |
| summary_html = gr.HTML() | |
| # Security Findings | |
| gr.HTML(""" | |
| <h2 style=" | |
| margin: 16px 0 13px 0; | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: #131314; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| "> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="stroke: #d97757;"> | |
| <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path> | |
| <line x1="12" y1="9" x2="12" y2="13"></line> | |
| <line x1="12" y1="17" x2="12.01" y2="17"></line> | |
| </svg> | |
| Security Findings | |
| </h2> | |
| """) | |
| findings_count = gr.HTML() | |
| findings_html = gr.HTML() | |
| # Export button | |
| with gr.Row(): | |
| export_json_btn = gr.Button( | |
| "Export JSON Report", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["export-button"] | |
| ) | |
| export_md_btn = gr.Button( | |
| "Export Markdown Report", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["export-button"] | |
| ) | |
| download_file = gr.File(label="Download Report", visible=False) | |
| download_file_md = gr.File(label="Download Markdown Report", visible=False) | |
| # Event handlers | |
| def toggle_mode(mode): | |
| """Toggle visibility based on selected mode.""" | |
| return { | |
| local_mode: gr.update(visible=(mode == "Local Directory")), | |
| url_mode: gr.update(visible=(mode == "Remote URL")) | |
| } | |
| scan_mode.change( | |
| fn=toggle_mode, | |
| inputs=[scan_mode], | |
| outputs=[local_mode, url_mode] | |
| ) | |
| def reset_interface(): | |
| """Reset the interface to initial state.""" | |
| return ( | |
| create_empty_state(), # progress_box | |
| gr.update(visible=False), # results_section | |
| "", # summary_html | |
| "", # findings_count | |
| "", # findings_html | |
| None, # file_upload | |
| "", # directory_path | |
| "", # web_url | |
| ) | |
| reset_btn.click( | |
| fn=reset_interface, | |
| outputs=[ | |
| progress_box, | |
| results_section, | |
| summary_html, | |
| findings_count, | |
| findings_html, | |
| file_upload, | |
| directory_path, | |
| web_url | |
| ] | |
| ) | |
| help_btn.click( | |
| fn=None, | |
| inputs=[], | |
| outputs=[], | |
| js=f"() => {{ const html = {self._help_js}; const blob = new Blob([html], {{type:'text/html;charset=utf-8'}}); window.open(URL.createObjectURL(blob), '_blank'); }}" | |
| ) | |
| def scan_local_files(files, dir_path, nvd_check): | |
| """Handle local file/directory scanning.""" | |
| # Show loading state | |
| yield ( | |
| create_loading_state("Initializing scan..."), | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| # Check if we have files or directory path | |
| if not files and not dir_path: | |
| yield ( | |
| """<div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="display: inline-block; vertical-align: middle; margin-right: 8px;"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="15" y1="9" x2="9" y2="15"></line> | |
| <line x1="9" y1="9" x2="15" y2="15"></line> | |
| </svg> | |
| <strong>No input provided</strong><br/> | |
| Please upload files or enter a directory path. | |
| </div>""", | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| return | |
| # Create session | |
| session_id = str(uuid.uuid4()) | |
| session_dir = Path(tempfile.mkdtemp(prefix=f"audit_{session_id}_")) | |
| try: | |
| target_path = session_dir | |
| # If files uploaded, save them | |
| if files: | |
| yield ( | |
| create_loading_state(f"Uploading {len(files)} files..."), | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| for file in files: | |
| dest = session_dir / Path(file.name).name | |
| shutil.copy(file.name, dest) | |
| # If directory path provided, use it directly (if exists) | |
| elif dir_path and os.path.exists(dir_path): | |
| target_path = Path(dir_path) | |
| yield ( | |
| create_loading_state("Scanning for vulnerabilities..."), | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| # Run scan | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| result = loop.run_until_complete( | |
| self.checker.scan_local(str(target_path), include_nvd=nvd_check) | |
| ) | |
| loop.close() | |
| # Generate HTML components | |
| summary = result.summary() | |
| summary_section = create_summary_section({ | |
| 'target': str(target_path), | |
| 'files_scanned': result.files_scanned, | |
| 'scan_type': 'local', | |
| 'summary': summary | |
| }) | |
| # Create filter script for severity badges | |
| filter_script = """ | |
| <script> | |
| // Filter function called by severity badges | |
| window.filterBySeverityBadge = function(severity) { | |
| const allBadges = document.querySelectorAll('.severity-badge'); | |
| const allCards = document.querySelectorAll('.finding-card'); | |
| // Check if clicking the active badge (toggle off) | |
| const clickedBadge = document.querySelector('.severity-badge[data-severity="' + severity + '"]'); | |
| const isActive = clickedBadge && clickedBadge.classList.contains('active'); | |
| if (isActive) { | |
| // Show all findings | |
| allBadges.forEach(function(badge) { | |
| badge.classList.remove('active'); | |
| badge.classList.remove('inactive'); | |
| }); | |
| allCards.forEach(function(card) { | |
| card.style.display = 'block'; | |
| }); | |
| updateFindingsCount(allCards.length); | |
| } else { | |
| // Filter by severity | |
| allBadges.forEach(function(badge) { | |
| if (badge.getAttribute('data-severity') === severity) { | |
| badge.classList.add('active'); | |
| badge.classList.remove('inactive'); | |
| } else { | |
| badge.classList.remove('active'); | |
| badge.classList.add('inactive'); | |
| } | |
| }); | |
| var visibleCount = 0; | |
| allCards.forEach(function(card) { | |
| if (card.getAttribute('data-severity') === severity) { | |
| card.style.display = 'block'; | |
| visibleCount++; | |
| } else { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| updateFindingsCount(visibleCount); | |
| // Scroll to findings | |
| const firstCard = document.querySelector('.finding-card[style*="display: block"]'); | |
| if (firstCard) { | |
| firstCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| } | |
| } | |
| function updateFindingsCount(count) { | |
| const countElement = document.getElementById('findings-count-display'); | |
| if (countElement) { | |
| countElement.innerHTML = 'Showing <strong>' + count + '</strong> finding' + (count !== 1 ? 's' : ''); | |
| } | |
| } | |
| </script> | |
| """ | |
| findings = "" | |
| if result.vulnerabilities: | |
| # Deduplicate vulnerabilities based on name, file, and line | |
| seen = set() | |
| unique_vulns = [] | |
| for vuln in result.vulnerabilities: | |
| # Create unique key | |
| key = f"{vuln.name}|{vuln.file_path}|{vuln.line_number}" | |
| if key not in seen: | |
| seen.add(key) | |
| unique_vulns.append(vuln) | |
| findings_counter = f""" | |
| <div id="findings-count-display" style=" | |
| color: #6b7280; | |
| font-size: 14px; | |
| margin: 13px 0 16px 0; | |
| padding: 12px; | |
| background: #f9fafb; | |
| border-radius: 8px; | |
| "> | |
| Showing <strong>{len(unique_vulns)}</strong> findings | |
| </div> | |
| """ | |
| # Sort by severity | |
| severity_order = { | |
| RiskLevel.CRITICAL: 0, | |
| RiskLevel.HIGH: 1, | |
| RiskLevel.MEDIUM: 2, | |
| RiskLevel.LOW: 3, | |
| RiskLevel.INFO: 4 | |
| } | |
| sorted_vulns = sorted( | |
| unique_vulns, | |
| key=lambda v: severity_order.get(v.risk_level, 5) | |
| ) | |
| for vuln in sorted_vulns: | |
| findings += create_finding_card({ | |
| 'name': vuln.name, | |
| 'risk_level': vuln.risk_level.name, | |
| 'file_path': vuln.file_path, | |
| 'line_number': vuln.line_number, | |
| 'description': vuln.description, | |
| 'cwe_id': vuln.cwe_id, | |
| 'cve_ids': vuln.cve_ids, | |
| 'remediation': vuln.remediation | |
| }) | |
| else: | |
| findings_counter = "" | |
| findings = """ | |
| <div style=" | |
| background: #f0fdf4; | |
| border: 1px solid #86efac; | |
| border-radius: 12px; | |
| padding: 40px; | |
| text-align: center; | |
| color: #166534; | |
| "> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" | |
| fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="margin: 0 auto 16px; display: block;"> | |
| <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> | |
| <polyline points="22 4 12 14.01 9 11.01"></polyline> | |
| </svg> | |
| <h3 style="margin: 0 0 8px 0; font-size: 20px; font-weight: 600;">No Vulnerabilities Found</h3> | |
| <p style="margin: 0; font-size: 14px;">Your code looks secure!</p> | |
| </div> | |
| """ | |
| # Generate reports for download | |
| json_report = self.checker.generate_report(result, format="json") | |
| json_path = session_dir / "security_report.json" | |
| with open(json_path, 'w') as f: | |
| f.write(json_report) | |
| md_report = self.checker.generate_report(result, format="markdown") | |
| md_path = session_dir / "security_report.md" | |
| with open(md_path, 'w') as f: | |
| f.write(md_report) | |
| # Schedule cleanup | |
| self.active_sessions[session_id] = { | |
| 'dir': session_dir, | |
| 'created': datetime.now(), | |
| 'json_path': json_path, | |
| 'md_path': md_path | |
| } | |
| progress_msg = f""" | |
| <div style=" | |
| background: #f0fdf4; | |
| border: 1px solid #86efac; | |
| border-radius: 12px; | |
| padding: 20px; | |
| color: #166534; | |
| "> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="display: inline-block; vertical-align: middle; margin-right: 8px;"> | |
| <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> | |
| <polyline points="22 4 12 14.01 9 11.01"></polyline> | |
| </svg> | |
| <strong>Scan complete!</strong><br/> | |
| Files scanned: {result.files_scanned} | Vulnerabilities found: {summary['total_vulnerabilities']} | |
| </div> | |
| """ | |
| yield ( | |
| progress_msg, | |
| gr.update(visible=True), | |
| summary_section + filter_script, | |
| findings_counter, | |
| findings, | |
| str(json_path), | |
| str(md_path) | |
| ) | |
| except Exception as e: | |
| error_msg = f""" | |
| <div style=" | |
| background: #fef2f2; | |
| border: 1px solid #fca5a5; | |
| border-radius: 12px; | |
| padding: 20px; | |
| color: #dc2626; | |
| "> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="display: inline-block; vertical-align: middle; margin-right: 8px;"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="15" y1="9" x2="9" y2="15"></line> | |
| <line x1="9" y1="9" x2="15" y2="15"></line> | |
| </svg> | |
| <strong>Scan failed</strong><br/> | |
| {str(e)} | |
| </div> | |
| """ | |
| yield ( | |
| error_msg, | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| def scan_web_app(url, nvd_check): | |
| """Handle web application scanning.""" | |
| yield ( | |
| create_loading_state("Scanning web application..."), | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| if not url: | |
| yield ( | |
| """<div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="display: inline-block; vertical-align: middle; margin-right: 8px;"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="15" y1="9" x2="9" y2="15"></line> | |
| <line x1="9" y1="9" x2="15" y2="15"></line> | |
| </svg> | |
| <strong>No URL provided</strong><br/> | |
| Please enter a web application URL. | |
| </div>""", | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| return | |
| try: | |
| # Ensure URL has protocol | |
| if not url.startswith(('http://', 'https://')): | |
| url = 'https://' + url | |
| # Run scan | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| result = loop.run_until_complete( | |
| self.checker.scan_web(url, include_nvd=False) | |
| ) | |
| loop.close() | |
| # Generate HTML components (similar to local scan) | |
| summary = result.summary() | |
| summary_section = create_summary_section({ | |
| 'target': url, | |
| 'files_scanned': 0, | |
| 'scan_type': 'web', | |
| 'summary': summary | |
| }) | |
| # Create filter script for severity badges | |
| filter_script = """ | |
| <script> | |
| // Filter function called by severity badges | |
| window.filterBySeverityBadge = function(severity) { | |
| const allBadges = document.querySelectorAll('.severity-badge'); | |
| const allCards = document.querySelectorAll('.finding-card'); | |
| // Check if clicking the active badge (toggle off) | |
| const clickedBadge = document.querySelector('.severity-badge[data-severity="' + severity + '"]'); | |
| const isActive = clickedBadge && clickedBadge.classList.contains('active'); | |
| if (isActive) { | |
| // Show all findings | |
| allBadges.forEach(function(badge) { | |
| badge.classList.remove('active'); | |
| badge.classList.remove('inactive'); | |
| }); | |
| allCards.forEach(function(card) { | |
| card.style.display = 'block'; | |
| }); | |
| updateFindingsCount(allCards.length); | |
| } else { | |
| // Filter by severity | |
| allBadges.forEach(function(badge) { | |
| if (badge.getAttribute('data-severity') === severity) { | |
| badge.classList.add('active'); | |
| badge.classList.remove('inactive'); | |
| } else { | |
| badge.classList.remove('active'); | |
| badge.classList.add('inactive'); | |
| } | |
| }); | |
| var visibleCount = 0; | |
| allCards.forEach(function(card) { | |
| if (card.getAttribute('data-severity') === severity) { | |
| card.style.display = 'block'; | |
| visibleCount++; | |
| } else { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| updateFindingsCount(visibleCount); | |
| // Scroll to findings | |
| const firstCard = document.querySelector('.finding-card[style*="display: block"]'); | |
| if (firstCard) { | |
| firstCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| } | |
| } | |
| function updateFindingsCount(count) { | |
| const countElement = document.getElementById('findings-count-display'); | |
| if (countElement) { | |
| countElement.innerHTML = 'Showing <strong>' + count + '</strong> finding' + (count !== 1 ? 's' : ''); | |
| } | |
| } | |
| </script> | |
| """ | |
| # Deduplicate vulnerabilities | |
| seen = set() | |
| unique_vulns = [] | |
| for vuln in result.vulnerabilities: | |
| # Create unique key (for URL scans, line_number is usually 0, so use name and description) | |
| key = f"{vuln.name}|{vuln.description}" | |
| if key not in seen: | |
| seen.add(key) | |
| unique_vulns.append(vuln) | |
| findings_counter = f""" | |
| <div id="findings-count-display" style="color: #6b7280; font-size: 14px; margin: 13px 0 16px 0; padding: 12px; background: #f9fafb; border-radius: 8px;"> | |
| Showing <strong>{len(unique_vulns)}</strong> findings | |
| </div> | |
| """ | |
| findings = "" | |
| if unique_vulns: | |
| for vuln in unique_vulns: | |
| findings += create_finding_card({ | |
| 'name': vuln.name, | |
| 'risk_level': vuln.risk_level.name, | |
| 'file_path': url, | |
| 'line_number': 0, | |
| 'description': vuln.description, | |
| 'cwe_id': vuln.cwe_id, | |
| 'cve_ids': vuln.cve_ids, | |
| 'remediation': vuln.remediation | |
| }) | |
| else: | |
| findings_counter = "" | |
| findings = """ | |
| <div style="background: #f0fdf4; border: 1px solid #86efac; border-radius: 12px; padding: 40px; text-align: center; color: #166534;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" | |
| fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="margin: 0 auto 16px; display: block;"> | |
| <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> | |
| <polyline points="22 4 12 14.01 9 11.01"></polyline> | |
| </svg> | |
| <h3 style="margin: 0 0 8px 0;">No Issues Found</h3> | |
| <p style="margin: 0;">Web application appears to be configured securely.</p> | |
| </div> | |
| """ | |
| # Generate report | |
| session_id = str(uuid.uuid4()) | |
| session_dir = Path(tempfile.mkdtemp(prefix=f"audit_{session_id}_")) | |
| json_report = self.checker.generate_report(result, format="json") | |
| json_path = session_dir / "security_report.json" | |
| with open(json_path, 'w') as f: | |
| f.write(json_report) | |
| md_report = self.checker.generate_report(result, format="markdown") | |
| md_path = session_dir / "security_report.md" | |
| with open(md_path, 'w') as f: | |
| f.write(md_report) | |
| progress_msg = f""" | |
| <div style="background: #f0fdf4; border: 1px solid #86efac; border-radius: 12px; padding: 20px; color: #166534;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="display: inline-block; vertical-align: middle; margin-right: 8px;"> | |
| <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> | |
| <polyline points="22 4 12 14.01 9 11.01"></polyline> | |
| </svg> | |
| <strong>Scan complete!</strong><br/> | |
| Vulnerabilities found: {summary['total_vulnerabilities']} | |
| </div> | |
| """ | |
| yield ( | |
| progress_msg, | |
| gr.update(visible=True), | |
| summary_section + filter_script, | |
| findings_counter, | |
| findings, | |
| str(json_path), | |
| str(md_path) | |
| ) | |
| except Exception as e: | |
| error_msg = f""" | |
| <div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" | |
| fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | |
| style="display: inline-block; vertical-align: middle; margin-right: 8px;"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="15" y1="9" x2="9" y2="15"></line> | |
| <line x1="9" y1="9" x2="15" y2="15"></line> | |
| </svg> | |
| <strong>Scan failed</strong><br/> | |
| {str(e)} | |
| </div> | |
| """ | |
| yield ( | |
| error_msg, | |
| gr.update(visible=False), | |
| "", "", "", None, None | |
| ) | |
| # Connect event handlers | |
| analyze_btn_local.click( | |
| fn=scan_local_files, | |
| inputs=[file_upload, directory_path, nvd_enrichment], | |
| outputs=[ | |
| progress_box, | |
| results_section, | |
| summary_html, | |
| findings_count, | |
| findings_html, | |
| download_file, | |
| download_file_md | |
| ] | |
| ) | |
| analyze_btn_url.click( | |
| fn=scan_web_app, | |
| inputs=[web_url, nvd_enrichment], | |
| outputs=[ | |
| progress_box, | |
| results_section, | |
| summary_html, | |
| findings_count, | |
| findings_html, | |
| download_file, | |
| download_file_md | |
| ] | |
| ) | |
| def export_json(): | |
| """Export JSON report.""" | |
| return gr.update(visible=True) | |
| export_json_btn.click( | |
| fn=export_json, | |
| outputs=[download_file] | |
| ) | |
| def export_markdown(): | |
| """Export Markdown report.""" | |
| return gr.update(visible=True) | |
| export_md_btn.click( | |
| fn=export_markdown, | |
| outputs=[download_file_md] | |
| ) | |
| return app | |
| def cleanup_old_sessions(self): | |
| """Remove sessions older than cleanup_interval.""" | |
| now = datetime.now() | |
| to_remove = [] | |
| for session_id, session in self.active_sessions.items(): | |
| age = (now - session['created']).total_seconds() | |
| if age > self.cleanup_interval: | |
| shutil.rmtree(session['dir'], ignore_errors=True) | |
| to_remove.append(session_id) | |
| for session_id in to_remove: | |
| del self.active_sessions[session_id] | |
| def launch(self, **kwargs): | |
| """Launch the Gradio application.""" | |
| # Start cleanup background task | |
| def cleanup_loop(): | |
| while True: | |
| time.sleep(600) # Check every 10 minutes | |
| self.cleanup_old_sessions() | |
| cleanup_thread = threading.Thread(target=cleanup_loop, daemon=True) | |
| cleanup_thread.start() | |
| # Create and launch interface | |
| interface = self.create_interface() | |
| interface.queue() | |
| # In Gradio 6.0, theme and css are passed to launch() instead of Blocks() | |
| interface.launch( | |
| theme=self.theme, | |
| css=self.custom_css, | |
| **kwargs | |
| ) | |
| def main(): | |
| """Main entry point.""" | |
| app = ModernSecurityAuditorApp() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False | |
| ) | |
| if __name__ == "__main__": | |
| main() | |