security_auditor / ui_components.py
MugdhaV
Initial deployment: Gradio frontend with Modal backend - Multi-language security scanner with parallel processing
e1e9580
"""
UI Components for Security Auditor Gradio Interface
Generates HTML for badges, cards, and sections matching design mockups
"""
from typing import Dict, List
def create_severity_badge(severity: str, count: int = 0) -> str:
"""
Create severity badge as a visual indicator.
Args:
severity: CRITICAL, HIGH, MEDIUM, LOW, INFO
count: Number of findings
Returns:
HTML string for severity badge
"""
colors = {
"CRITICAL": {"bg": "#fef2f2", "text": "#dc2626", "border": "#fca5a5"},
"HIGH": {"bg": "#fff7ed", "text": "#ea580c", "border": "#fdba74"},
"MEDIUM": {"bg": "#fffbeb", "text": "#d97706", "border": "#fcd34d"},
"LOW": {"bg": "#f0fdfa", "text": "#0d9488", "border": "#5eead4"},
"INFO": {"bg": "#f9fafb", "text": "#6b7280", "border": "#d1d5db"}
}
color = colors.get(severity, colors["INFO"])
return f"""
<div
class="severity-badge"
data-severity="{severity}"
style="
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 16px 24px;
background: {color['bg']};
border: 2px solid {color['border']};
border-radius: 6px;
min-width: 100px;
"
>
<div style="
font-size: 11px;
font-weight: 600;
color: {color['text']};
text-transform: capitalize;
letter-spacing: 0.05em;
margin-bottom: 8px;
">{severity.capitalize()}</div>
<div style="
font-size: 32px;
font-weight: 700;
color: {color['text']};
">{count}</div>
</div>
"""
def create_finding_card(vulnerability: Dict) -> str:
"""
Create HTML for vulnerability finding card.
Args:
vulnerability: Dict with name, severity, file_path, line_number,
description, cwe_id, cve_ids, remediation
Returns:
HTML string for finding card
"""
severity_colors = {
"CRITICAL": "#dc2626",
"HIGH": "#ea580c",
"MEDIUM": "#d97706",
"LOW": "#0d9488",
"INFO": "#6b7280"
}
severity_bg = {
"CRITICAL": "#fef2f2",
"HIGH": "#fff7ed",
"MEDIUM": "#fffbeb",
"LOW": "#f0fdfa",
"INFO": "#f9fafb"
}
severity = vulnerability.get('risk_level', 'INFO')
color = severity_colors.get(severity, severity_colors['INFO'])
bg = severity_bg.get(severity, severity_bg['INFO'])
# Build CWE/CVE tags
tags_html = ""
if vulnerability.get('cwe_id'):
tags_html += f"""
<span style="
display: inline-block;
padding: 4px 12px;
background: #f3f4f6;
color: #4b5563;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
margin-right: 8px;
">{vulnerability['cwe_id']}</span>
"""
for cve in vulnerability.get('cve_ids', [])[:3]: # Show max 3 CVEs
tags_html += f"""
<span style="
display: inline-block;
padding: 4px 12px;
background: #fef2f2;
color: #dc2626;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
margin-right: 8px;
">{cve}</span>
"""
# Build remediation section
remediation_html = ""
if vulnerability.get('remediation'):
# Truncate very long remediation text
remediation_text = vulnerability['remediation']
if len(remediation_text) > 500:
remediation_text = remediation_text[:500] + "..."
remediation_html = f"""
<details style="margin-top: 16px;" class="remediation-details">
<summary style="
cursor: pointer;
padding: 12px 16px;
background: #f9fafb;
border-radius: 8px;
font-weight: 600;
color: #374151;
user-select: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
transition: background 0.2s ease;
" onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#f9fafb'">
<div style="display: flex; align-items: center; gap: 8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="#d97757" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
</svg>
<span>Remediation Guidance</span>
</div>
<svg class="chevron-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="transition: transform 0.2s ease;">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</summary>
<style>
details[open] .chevron-icon {{
transform: rotate(180deg);
}}
</style>
<div style="
padding: 16px;
margin-top: 8px;
background: #f0fdf4;
border-left: 4px solid #10b981;
border-radius: 8px;
">
<pre style="
background: white;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
font-size: 13px;
line-height: 1.5;
color: #1f2937;
white-space: pre-wrap;
word-wrap: break-word;
">{remediation_text}</pre>
</div>
</details>
"""
return f"""
<div class="finding-card" data-severity="{severity}" style="
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<h3 style="
margin: 0;
font-size: 18px;
font-weight: 600;
color: #111827;
">{vulnerability.get('name', 'Unknown Vulnerability')}</h3>
<span style="
padding: 6px 12px;
background: {bg};
color: {color};
border-radius: 6px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
">{severity}</span>
</div>
<div style="margin-bottom: 12px;">
{tags_html}
</div>
<div style="
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
color: #6b7280;
font-size: 14px;
">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="flex-shrink: 0;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
<span style="font-family: 'Fira Code', monospace; color: #374151;">{vulnerability.get('file_path', 'N/A')}:{vulnerability.get('line_number', 'N/A')}</span>
</div>
<p style="
margin: 0 0 16px 0;
color: #4b5563;
line-height: 1.6;
font-size: 14px;
">{vulnerability.get('description', '')}</p>
{remediation_html}
</div>
"""
def create_summary_section(scan_result: Dict) -> str:
"""
Create Analysis Summary section with proper alignment and sentence capitalization.
Args:
scan_result: Dict with target, files_scanned, scan_type, summary data
Returns:
HTML string for summary section
"""
summary = scan_result.get('summary', {})
# Metadata row - Fixed alignment with grid layout
metadata_html = f"""
<div style="
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 24px;
margin-bottom: 24px;
padding: 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
">
<div style="display: flex; flex-direction: column; justify-content: center;">
<div style="
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 8px;
line-height: 1.2;
">Target</div>
<div style="
font-size: 14px;
font-weight: 600;
color: #131314;
font-family: ui-monospace, monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
" title="{scan_result.get('target', 'N/A')}">{scan_result.get('target', 'N/A')}</div>
</div>
<div style="display: flex; flex-direction: column; justify-content: center;">
<div style="
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 8px;
line-height: 1.2;
">Files analyzed</div>
<div style="
font-size: 24px;
font-weight: 700;
color: #131314;
line-height: 1.2;
">{scan_result.get('files_scanned', 0)}</div>
</div>
<div style="display: flex; flex-direction: column; justify-content: center;">
<div style="
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 8px;
line-height: 1.2;
">Total findings</div>
<div style="
font-size: 28px;
font-weight: 700;
color: #dc2626;
line-height: 1.2;
">{summary.get('total_vulnerabilities', 0)}</div>
</div>
<div style="display: flex; flex-direction: column; justify-content: center;">
<div style="
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 8px;
line-height: 1.2;
">Analysis type</div>
<div style="
font-size: 14px;
font-weight: 700;
color: #131314;
text-transform: capitalize;
line-height: 1.2;
">{scan_result.get('scan_type', 'local')}</div>
</div>
</div>
"""
# Severity badges row
by_severity = summary.get('by_severity', {})
badges_html = f"""
<div style="
display: flex;
gap: 16px;
flex-wrap: wrap;
">
{create_severity_badge('CRITICAL', by_severity.get('CRITICAL', 0))}
{create_severity_badge('HIGH', by_severity.get('HIGH', 0))}
{create_severity_badge('MEDIUM', by_severity.get('MEDIUM', 0))}
{create_severity_badge('LOW', by_severity.get('LOW', 0))}
{create_severity_badge('INFO', by_severity.get('INFO', 0))}
</div>
"""
return f"""
<div style="
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
margin-bottom: 12px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
">
<h2 style="
margin: 0 0 20px 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="22" height="22" viewBox="0 0 24 24"
fill="none" stroke="#d97757" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
Analysis Summary
</h2>
{metadata_html}
{badges_html}
</div>
"""
def create_empty_state() -> str:
"""
Create HTML for empty state (no results yet).
Returns:
HTML string for empty state
"""
return """
<div style="
background: white;
border: 2px dashed #e5e7eb;
border-radius: 12px;
padding: 60px 40px;
text-align: center;
margin: 40px 0;
">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
fill="none" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="margin: 0 auto 16px; display: block;">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<h3 style="
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: #131314;
">Ready to Scan</h3>
<p style="
margin: 0;
color: #6b7280;
font-size: 14px;
">Upload files or enter a URL to begin security analysis</p>
</div>
"""
def create_loading_state(message: str = "Scanning...") -> str:
"""
Create HTML for loading state.
Args:
message: Loading message to display
Returns:
HTML string for loading state
"""
return f"""
<div style="
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 40px;
text-align: center;
margin: 40px 0;
">
<div style="
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #f3f4f6;
border-top-color: #f59e0b;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
"></div>
<style>
@keyframes spin {{
0% {{ transform: rotate(0deg); }}
100% {{ transform: rotate(360deg); }}
}}
</style>
<h3 style="
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #111827;
">{message}</h3>
<p style="
margin: 0;
color: #6b7280;
font-size: 14px;
">This may take a few moments...</p>
</div>
"""