TechKay/AI_Security_Advisor
0
1# AI Security Advisor (Static Dashboard)2# - Paste/upload logs OR generate synthetic "live" logs3# - Heuristic detection + optional LLM summary via Hugging Face4# - Downloadable PDF report5 6import os, re, io7from datetime import datetime, timedelta8import random9 10import gradio as gr11import pandas as pd12import matplotlib13matplotlib.use("Agg") # headless for Spaces14import matplotlib.pyplot as plt15import textwrap16 17# LLM client18try:19 from huggingface_hub import InferenceClient20 HAVE_HF = True21except Exception:22 HAVE_HF = False23 24# reportlab25try:26 from reportlab.lib.pagesizes import letter27 from reportlab.pdfgen import canvas28 HAVE_PDF = True29except Exception:30 HAVE_PDF = False31 32# ---------- Sample Logs (baseline) ----------33SAMPLE_LOGS = """\342025-10-21 08:02:14 AUTH host=app01 user=svc_sync event=LOGIN_FAIL src_ip=185.199.220.23 detail=invalid password352025-10-21 08:02:18 AUTH host=app01 user=svc_sync event=LOGIN_FAIL src_ip=185.199.220.23 detail=invalid password362025-10-21 08:03:01 STORAGE host=stg01 event=LATENCY_SPIKE target=iscsi-03 value_ms=289 threshold_ms=50372025-10-21 08:03:44 NET host=core-sw12 event=PKT_DROP iface=eth0 drops=327 interval=60s382025-10-21 08:04:33 EDR host=wks-445 event=BEACON dst_ip=198.51.100.5 status=isolated392025-10-21 08:05:12 COMPUTE host=vm221 event=CPU_HIGH pct=95 duration=5m402025-10-21 08:05:56 STORAGE host=raid-b event=DEGRADED disk=disk2 status=offline412025-10-21 08:07:08 NET host=edge-fw event=OUT_SPIKE vlan=200 outbound_mbps=1200 baseline_mbps=200422025-10-21 08:08:30 COMPUTE host=vm203 event=RESTORE action=snapshot reason=failed_boot432025-10-21 08:09:02 BACKUP host=bkp01 event=JOB_FAIL job=daily_full_1 reason=timeout node=sn-07442025-10-21 08:09:27 AUTH host=netops-gw user=netops event=SSH_FAIL src_ip=192.0.2.14 count=5 interval=1m452025-10-21 08:10:11 NET host=edge-rtr event=BGP_RESET peer=10.0.0.254462025-10-21 08:11:02 FACILITY host=ups-2 event=UPS_ON_BAT runtime=18m47""".strip()48 49# ---------- Log Normalizer ----------50LOG_RE = re.compile(51 r'(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+'52 r'(?P<src>[A-Z]+)\s+'53 r'host=(?P<host>\S+)'54 r'(?:\s+user=(?P<user>\S+))?'55 r'(?:\s+event=(?P<event>\S+))?'56 r'(?:\s+src_ip=(?P<src_ip>\S+))?'57 r'(?:\s+dst_ip=(?P<dst_ip>\S+))?'58 r'(?:\s+detail=(?P<detail>.+?))?'59 r'(?:\s+pct=(?P<pct>\d+))?'60 r'(?:\s+drops=(?P<drops>\d+))?'61 r'(?:\s+value_ms=(?P<value_ms>\d+))?'62 r'(?:\s+baseline_mbps=(?P<bbps>\d+))?'63 r'(?:\s+outbound_mbps=(?P<ombps>\d+))?'64 r'(?:\s+count=(?P<count>\d+))?'65 r'(?:\s+event=\S+)?'66)67 68def parse_logs(raw: str) -> pd.DataFrame:69 rows = []70 for line in (raw or "").splitlines():71 m = LOG_RE.match(line.strip())72 if not m:73 continue74 d = m.groupdict()75 d["ts_dt"] = datetime.strptime(d["ts"], "%Y-%m-%d %H:%M:%S")76 rows.append(d)77 if not rows:78 return pd.DataFrame()79 df = pd.DataFrame(rows).fillna("")80 for col in ("pct", "drops", "value_ms", "bbps", "ombps", "count"):81 if col in df.columns:82 df[col] = pd.to_numeric(df[col], errors="coerce")83 return df84 85# ---------- Heuristic Detector ----------86def detect_candidates(df: pd.DataFrame) -> pd.DataFrame:87 if df.empty:88 return df89 out = []90 for _, r in df.iterrows():91 src = r.get("src", "")92 ev = r.get("event", "")93 sev = None94 conf = 0.695 reason = []96 97 if src == "AUTH" and ("LOGIN_FAIL" in ev or "SSH_FAIL" in ev):98 sev = "MEDIUM"; reason.append("Repeated authentication failures")99 if pd.notna(r.get("count")) and r["count"] and r["count"] >= 5:100 sev = "HIGH"; conf = 0.85101 102 if src == "EDR" and "BEACON" in ev:103 sev = "HIGH"; conf = 0.9; reason.append("Endpoint beacon suggests possible C2")104 105 if src == "STORAGE" and ("DEGRADED" in ev or (pd.notna(r.get("value_ms")) and r["value_ms"] > 200)):106 sev = "HIGH"; conf = max(conf, 0.8); reason.append("Storage issue may impact latency/availability")107 108 if src == "NET" and any(x in ev for x in ("PKT_DROP", "OUT_SPIKE", "BGP_RESET")):109 sev = "HIGH"; conf = max(conf, 0.8); reason.append("Network instability / possible exfil or routing fault")110 111 if src == "COMPUTE" and any(x in ev for x in ("CPU_HIGH", "RESTORE")):112 sev = "MEDIUM"; conf = max(conf, 0.7); reason.append("Compute performance/boot recovery anomaly")113 114 if src == "BACKUP" and "JOB_FAIL" in ev:115 sev = "MEDIUM"; conf = max(conf, 0.7); reason.append("Backup failure could risk RPO/RTO")116 117 if src == "FACILITY" and "UPS_ON_BAT" in ev:118 sev = "HIGH"; conf = max(conf, 0.85); reason.append("Power risk: UPS on battery")119 120 if sev:121 out.append({122 "timestamp": r["ts"],123 "source": src,124 "host": r.get("host", ""),125 "event": ev,126 "severity": sev,127 "confidence": round(conf, 2),128 "note": "; ".join(reason)129 })130 return pd.DataFrame(out)131 132# ---------- Color-coded Incident Table ----------133def render_candidates_md(cand_df: pd.DataFrame) -> str:134 if cand_df.empty:135 return "<div style='color:#9ca3af;'>✅ No candidate incidents detected.</div>"136 137 html = """138 <style>139 .incident-table { width:100%; border-collapse:separate; border-spacing:0 8px; color:#e5e7eb; font-family:Inter;}140 .incident-header th {141 background:rgba(255,255,255,0.08);142 padding:10px;143 text-align:left;144 font-size:14px;145 color:#cbd5e1;146 border-bottom:1px solid rgba(255,255,255,0.1);147 }148 .incident-row {149 background:rgba(255,255,255,0.05);150 border-radius:10px;151 cursor:pointer;152 transition:0.2s;153 }154 .incident-row:hover { background:rgba(255,255,255,0.12); }155 156 .incident-row td { padding:12px; font-size:14px; }157 158 .details {159 display:none;160 background:rgba(255,255,255,0.03);161 padding:12px;162 border-radius:10px;163 margin-bottom:12px;164 font-size:13px;165 color:#cbd5e1;166 }167 .sev-badge { font-weight:700; }168 .sev-high { color:#ff4b5c; }169 .sev-medium { color:#ffb453; }170 .sev-low { color:#4cd27f; }171 172 .sev-dot {173 height:10px; width:10px; border-radius:50%; display:inline-block; margin-right:6px;174 }175 .dot-high { background:#ff4b5c; box-shadow:0 0 6px #ff4b5c; }176 .dot-medium { background:#ffb453; box-shadow:0 0 6px #ffb453; }177 .dot-low { background:#4cd27f; box-shadow:0 0 6px #4cd27f; }178 </style>179 180 <script>181 function toggleDetails(id) {182 var elem = document.getElementById(id);183 elem.style.display = elem.style.display === "none" ? "block" : "none";184 }185 </script>186 187 <table class='incident-table'>188 <thead class='incident-header'>189 <tr>190 <th>Timestamp</th>191 <th>Source</th>192 <th>Host</th>193 <th>Event</th>194 <th>Severity</th>195 <th>Confidence</th>196 </tr>197 </thead>198 <tbody>199 """200 201 for idx, r in cand_df.iterrows():202 sev = r["severity"].upper()203 sev_html = {204 "HIGH": "<span class='sev-dot dot-high'></span><span class='sev-badge sev-high'>HIGH</span>",205 "MEDIUM": "<span class='sev-dot dot-medium'></span><span class='sev-badge sev-medium'>MEDIUM</span>",206 "LOW": "<span class='sev-dot dot-low'></span><span class='sev-badge sev-low'>LOW</span>"207 }.get(sev, sev)208 209 row_id = f"details_{idx}"210 211 html += f"""212 <tr class='incident-row' onclick="toggleDetails('{row_id}')">213 <td>{r['timestamp']}</td>214 <td>{r['source']}</td>215 <td>{r['host']}</td>216 <td>{r['event']}</td>217 <td>{sev_html}</td>218 <td>{r['confidence']}</td>219 </tr>220 221 <tr>222 <td colspan="6">223 <div id="{row_id}" class="details">224 <strong>Full Incident Details:</strong><br>225 <b>Source:</b> {r['source']}<br>226 <b>Host:</b> {r['host']}<br>227 <b>Event:</b> {r['event']}<br>228 <b>Severity:</b> {r['severity']}<br>229 <b>Confidence:</b> {r['confidence']}<br>230 <b>Notes:</b> {r['note']}<br>231 <b>Raw Data:</b> {r.to_dict()}232 </div>233 </td>234 </tr>235 """236 237 html += "</tbody></table>"238 return html239 240 241 242# ---------- Static Charts ----------243def fig_severity_counts(cand_df: pd.DataFrame):244 fig = plt.figure(figsize=(5.2, 3.2), dpi=140)245 plt.style.use("dark_background")246 if cand_df.empty:247 plt.text(0.5, 0.5, "No incidents", ha="center", va="center")248 return fig249 counts = cand_df["severity"].value_counts().reindex(250 ["LOW", "MEDIUM", "HIGH", "CRITICAL"]251 ).fillna(0)252 counts.plot(kind="bar")253 plt.title("Severity Distribution")254 plt.xlabel("Severity")255 plt.ylabel("Count")256 plt.tight_layout()257 return fig258 259def fig_trend(df: pd.DataFrame):260 fig = plt.figure(figsize=(5.6, 3.2), dpi=140)261 plt.style.use("dark_background")262 if df.empty:263 plt.text(0.5, 0.5, "No data", ha="center", va="center")264 return fig265 tmp = df.copy()266 tmp["minute"] = tmp["ts_dt"].dt.floor("min")267 series = tmp.groupby("minute").size()268 series.plot(marker="o")269 plt.title("Event Volume Over Time")270 plt.xlabel("Time (minute)")271 plt.ylabel("Events")272 plt.grid(True, alpha=0.3)273 plt.tight_layout()274 return fig275 276def fig_risk_heatmap(cand_df: pd.DataFrame):277 fig = plt.figure(figsize=(5, 4), dpi=140)278 plt.style.use("dark_background")279 280 if cand_df.empty:281 plt.text(0.5, 0.5, "No incidents", ha="center", va="center")282 return fig283 284 # Convert severity → numeric risk weight285 sev_map = {"LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}286 cand_df["sev_score"] = cand_df["severity"].map(sev_map)287 288 # 2D heatmap matrix289 heat = cand_df.pivot_table(290 values="confidence",291 index="sev_score",292 columns="host",293 aggfunc="mean"294 )295 296 plt.imshow(heat, cmap="inferno", aspect="auto")297 plt.colorbar(label="Mean Confidence")298 plt.title("Risk Heatmap (Severity × Host)")299 plt.xlabel("Host")300 plt.ylabel("Severity Level")301 plt.xticks(range(len(heat.columns)), heat.columns, rotation=45)302 plt.yticks(range(len(heat.index)), heat.index)303 plt.tight_layout()304 return fig305 306 307# ---------- LLM Summary (HF chat) ----------308MODEL = os.getenv("LLM_MODEL", "Qwen/Qwen2.5-7B-Instruct")309HF_TOKEN = os.getenv("HF_USER")310 311def llm_summary(cand_df: pd.DataFrame, raw: str) -> str:312 if cand_df.empty:313 baseline = "No candidate incidents detected. Continue monitoring."314 else:315 sev_counts = cand_df["severity"].value_counts().to_dict()316 top_hosts = cand_df["host"].value_counts().head(3).to_dict()317 baseline = f"Incidents: {len(cand_df)} | Sev: {sev_counts} | Top hosts: {top_hosts}"318 319 if not (HF_TOKEN and HAVE_HF):320 return (321 f"⚠️ LLM unavailable — rule-based summary only.\n\n"322 f"**Overview:** {baseline}\n"323 f"**Action:** Review HIGH severity events first; "324 f"validate auth failures, EDR beacons, and network anomalies."325 )326 327 try:328 client = InferenceClient(token=HF_TOKEN)329 sys = (330 "You are an enterprise AI Security Advisor in a SOC. "331 "Analyze detected authentication, firewall, and endpoint anomalies. "332 "Prioritize HIGH/CRITICAL threats, highlight likely root causes, "333 "affected systems/users, and immediate remediation steps. "334 "Briefly mention false-positive risk and data/privacy considerations. "335 "Return 4–6 concise bullet points. No preambles or explanations of the prompt."336 )337 user = (338 f"Detected incidents (JSON): {cand_df.to_json(orient='records')[:6000]}\n\n"339 f"Raw logs (truncated):\n{raw[:2000]}"340 )341 resp = client.chat.completions.create(342 model=MODEL,343 messages=[{"role": "system", "content": sys},344 {"role": "user", "content": user}],345 temperature=0.4,346 max_tokens=700,347 )348 return resp.choices[0].message["content"].strip()349 except Exception as e:350 return (351 f"⚠️ LLM error; using rule-based summary.\n\n"352 f"**Overview:** {baseline}\n"353 f"**Action:** Triage HIGH first; gather PCAP/EDR/auth history. ({e})"354 )355 356# ---------- Synthetic "Live" Log Generator ----------357AUTH_IPS = ["185.199.220.23", "192.0.2.14", "203.0.113.45"]358HOSTS = ["app01", "netops-gw", "edge-fw", "core-sw12", "wks-445", "vm221"]359EVENT_TEMPLATES = [360 "AUTH host={host} user=svc_sync event=LOGIN_FAIL src_ip={ip} detail=invalid password",361 "AUTH host={host} user=netops event=SSH_FAIL src_ip={ip} count=5 interval=1m",362 "NET host=edge-fw event=OUT_SPIKE vlan=200 outbound_mbps=1300 baseline_mbps=200",363 "NET host=core-sw12 event=PKT_DROP iface=eth0 drops=420 interval=60s",364 "EDR host=wks-445 event=BEACON dst_ip=198.51.100.5 status=isolated",365 "STORAGE host=raid-b event=DEGRADED disk=disk2 status=offline",366]367 368def generate_synthetic_logs(current_logs: str) -> str:369 """Simulate a 'live' snapshot of security logs."""370 base_time = datetime.now().replace(second=0, microsecond=0)371 lines = []372 for i in range(10):373 ts = (base_time + timedelta(seconds=i * 30)).strftime("%Y-%m-%d %H:%M:%S")374 tmpl = random.choice(EVENT_TEMPLATES)375 line = tmpl.format(376 host=random.choice(HOSTS),377 ip=random.choice(AUTH_IPS),378 )379 lines.append(f"{ts} {line}")380 new_block = "\n".join(lines)381 if current_logs.strip():382 return current_logs.strip() + "\n" + new_block383 return new_block384 385# ---------- Core Analysis ----------386def run_analysis(log_text, file_obj):387 if file_obj is not None:388 try:389 with open(file_obj.name, "r", encoding="utf-8", errors="ignore") as f:390 log_text = f.read()391 except Exception as e:392 return ("⚠️ File read error: " + str(e), "", None, None, None, "")393 394 df = parse_logs(log_text or "")395 cand = detect_candidates(df)396 397 total = len(df)398 incidents = len(cand)399 high = (cand["severity"] == "HIGH").sum() if not cand.empty else 0400 401 kpi_html = f"""402<div class="kpi-row">403 <div class="kpi-card">404 <div class="kpi-label">Total Events</div>405 <div class="kpi-value">{total}</div>406 </div>407 <div class="kpi-card">408 <div class="kpi-label">Candidate Incidents</div>409 <div class="kpi-value">{incidents}</div>410 </div>411 <div class="kpi-card kpi-high">412 <div class="kpi-label">High Severity</div>413 <div class="kpi-value">{high}</div>414 </div>415</div>416"""417 418 # Correct HTML table419 cand_md_text = render_candidates_md(cand)420 421 # All charts422 fig1 = fig_severity_counts(cand)423 fig2 = fig_trend(df)424 fig3 = fig_risk_heatmap(cand)425 426 summary = llm_summary(cand, log_text or "")427 428 # FIXED: cand_md_text (NOT cand_html)429 return (kpi_html, cand_md_text, fig1, fig2, fig3, summary)430 431 432# ---------- PDF Report Export ----------433def wrap_pdf_text(text, width=110):434 """Wrap long lines for PDF output."""435 wrapped = []436 for line in text.splitlines():437 wrapped.extend(textwrap.wrap(line, width=width))438 return wrapped439 440def render_candidates_text(cand_df: pd.DataFrame) -> list:441 """Return a plain-text list of incidents for PDF export."""442 if cand_df.empty:443 return ["No candidate incidents detected."]444 445 lines = []446 for _, r in cand_df.iterrows():447 lines.append(f"Timestamp: {r['timestamp']}")448 lines.append(f"Source: {r['source']}")449 lines.append(f"Host: {r['host']}")450 lines.append(f"Event: {r['event']}")451 lines.append(f"Severity: {r['severity']}")452 lines.append(f"Confidence: {r['confidence']}")453 lines.append(f"Notes: {r['note']}")454 lines.append("-" * 50)455 456 return lines457 458def generate_pdf_report(log_text, file_obj):459 if not HAVE_PDF:460 return None461 462 # recompute analysis for the report463 if file_obj is not None:464 try:465 with open(file_obj.name, "r", encoding="utf-8", errors="ignore") as f:466 log_text = f.read()467 except Exception:468 pass469 470 df = parse_logs(log_text or "")471 cand = detect_candidates(df)472 total = len(df)473 incidents = len(cand)474 high = (cand["severity"] == "HIGH").sum() if not cand.empty else 0475 summary = llm_summary(cand, log_text or "")476 cand_md_text = render_candidates_md(cand)477 478 buffer = io.BytesIO()479 c = canvas.Canvas(buffer, pagesize=letter)480 width, height = letter481 482 text_obj = c.beginText(40, height - 50)483 text_obj.setFont("Helvetica-Bold", 14)484 text_obj.textLine("AI Security Advisor — Summary Report")485 text_obj.setFont("Helvetica", 10)486 text_obj.textLine("")487 text_obj.textLine(f"Total events: {total}")488 text_obj.textLine(f"Candidate incidents: {incidents}")489 text_obj.textLine(f"High severity: {high}")490 text_obj.textLine("")491 text_obj.textLine("Executive Summary:")492 text_obj.textLine("")493 494 for line in wrap_pdf_text(summary, width=110):495 text_obj.textLine(line)496 497 498 text_obj.textLine("")499 text_obj.textLine("Candidate Incidents:")500 text_obj.textLine("(Markdown-style table)")501 cand_lines = render_candidates_text(cand)502 for line in cand_lines[:50]:503 text_obj.textLine(line[:110])504 505 506 c.drawText(text_obj)507 c.showPage()508 c.save()509 buffer.seek(0)510 511 # write to temp file for Gradio512 out_path = "security_report.pdf"513 with open(out_path, "wb") as f:514 f.write(buffer.read())515 return out_path516 517# ---------- Dark Theme CSS ----------518 519DARK_CSS = """520body {521 background: radial-gradient(circle at top, #1d2345 0, #030712 50%, #000000 100%);522}523 524/* Make ALL text in the Gradio app light/white */525.gradio-container,526.gradio-container * {527 color: #f9fafb !important;528 font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;529}530 531/* Headings */532h1, h2, h3, h4, h5, h6 {533 color: #f9fafb !important;534}535 536/* Markdown blocks */537.markdown, .markdown * {538 color: #f9fafb !important;539}540 541/* KPI cards */542.kpi-row {543 display: flex;544 gap: 0.75rem;545 margin-bottom: 0.75rem;546}547.kpi-card {548 flex: 1;549 padding: 0.75rem 1rem;550 border-radius: 0.9rem;551 background: linear-gradient(135deg, #0f172a, #111827);552 box-shadow: 0 10px 25px rgba(15,23,42,0.7);553 border: 1px solid rgba(148,163,184,0.35);554}555.kpi-card .kpi-label {556 font-size: 0.75rem;557 text-transform: uppercase;558 letter-spacing: 0.08em;559 color: #9ca3af;560}561.kpi-card .kpi-value {562 font-size: 1.6rem;563 font-weight: 700;564 margin-top: 0.25rem;565 color: #f9fafb;566}567.kpi-card.kpi-high {568 background: linear-gradient(135deg, #7f1d1d, #b91c1c);569 border-color: rgba(248,113,113,0.6);570}571 572/* Buttons */573button, .gr-button {574 border-radius: 999px !important;575}576 577/* ------------------------------- */578/* LOG INPUT TEXTBOX IN BLACK TEXT */579/* ------------------------------- */580.logs-box input,581.logs-box textarea {582 color: #000 !important; /* Black text */583 background: #f3f4f6 !important; /* Light gray background */584 border: 1px solid #d1d5db !important;585 font-family: monospace !important;586}587 588/* Keep the label white */589.logs-box label {590 color: #ffffff !important;591}592 593/* ============================== */594/* BLACK TEXT INPUT / OUTPUT BOX */595/* ============================== */596.black-box * {597 color: #000 !important; /* Black text */598 background: #f3f4f6 !important; /* Light grey background */599 border: 1px solid #d1d5db !important;600 font-family: monospace !important;601}602 603/* Keep labels white */604.black-box label {605 color: #ffffff !important;606}607 608"""609 610 611# ---------- UI ----------612with gr.Blocks(title="AI Security Advisor", css=DARK_CSS) as demo:613 gr.Markdown(614 "## AI Security Advisor\n"615 "An LLM-assisted SOC dashboard that inspects authentication, firewall, and endpoint logs, "616 "highlights potential threats, and summarizes risk posture."617 )618 with gr.Row():619 fig1 = gr.Plot(label="SD")620 fig2 = gr.Plot(label="EVOT")621 fig3 = gr.Plot(label="RHM")622 623 with gr.Row():624 with gr.Column(scale=1):625 gr.Markdown("### Log Input")626 logs_in = gr.Textbox(627 label="Paste Logs",628 lines=14,629 value=SAMPLE_LOGS,630 placeholder="Paste authentication, firewall, or endpoint logs here...",631 elem_classes=["logs-box"]632 633 )634 file_in = gr.File(label="Or Upload .txt Logs", file_types=["text"], elem_classes=["black-box"])635 636 with gr.Row():637 synth_btn = gr.Button("Generate Synthetic Live Snapshot", variant="secondary")638 run_btn = gr.Button("Run Analysis", variant="primary")639 640 gr.Markdown(641 "### SOC Notes\n"642 "- Synthetic logs simulate live authentication, network, and endpoint activity.\n"643 "- Do **not** upload real PII/PHI/PCI data to this demo Space.\n"644 "- LLM summaries are advisory and require human analyst validation."645 )646 647 pdf_btn = gr.Button("Generate PDF Report")648 pdf_out = gr.File(label="Download SOC Report (PDF)", elem_classes=["black-box"])649 650 with gr.Column(scale=2):651 gr.Markdown("### Threat Overview")652 kpi_out = gr.HTML()653 654 gr.Markdown("### Candidate Incidents (Color Coded)")655 cand_md = gr.HTML()656 657 658 gr.Markdown("### AI Security Advisor Summary")659 exec_sum = gr.Markdown()660 661 # wiring662 run_btn.click(663 fn=run_analysis,664 inputs=[logs_in, file_in],665 outputs=[kpi_out, cand_md, fig1, fig2, fig3, exec_sum]666 )667 668 synth_btn.click(669 fn=generate_synthetic_logs,670 inputs=[logs_in],671 outputs=[logs_in]672 )673 674 pdf_btn.click(675 fn=generate_pdf_report,676 inputs=[logs_in, file_in],677 outputs=[pdf_out]678 )679 680if __name__ == "__main__":681 demo.queue().launch()682 