security_auditor_orig / gradio_app.py
MugdhaV
Match Export Markdown Report button style to Export JSON Report
d931aa9
"""
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()