Add IaC-SecFix app with Gradio UI and agent backend
Browse files- iac_secfix_space/rendering.py +193 -43
- iac_secfix_space/ui.py +241 -75
iac_secfix_space/rendering.py
CHANGED
|
@@ -1,30 +1,197 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
"
|
| 8 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
|
| 12 |
-
def
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
failed = summary.get("failed", len(rule_ids))
|
| 20 |
-
crit = summary.get("CRITICAL", 0)
|
| 21 |
-
high = summary.get("HIGH", 0)
|
| 22 |
-
med = summary.get("MEDIUM", 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
for rid in rule_ids:
|
| 26 |
-
lines.append(f"- `{rid}`")
|
| 27 |
-
return "\n".join(lines)
|
| 28 |
|
| 29 |
|
| 30 |
def render_result(data: dict) -> tuple[str, str, str, str, str, str]:
|
|
@@ -46,35 +213,18 @@ def render_result(data: dict) -> tuple[str, str, str, str, str, str]:
|
|
| 46 |
unresolved: list[str] = validation.get("targeted_unresolved") or []
|
| 47 |
parse_ok: bool = validation.get("parse_ok", False)
|
| 48 |
changed_ratio: float = validation.get("changed_lines_ratio", 0.0)
|
| 49 |
-
|
| 50 |
explanation = patch_obj.get("explanation") or reason
|
| 51 |
risk_notes: list[str] = patch_obj.get("risk_notes") or []
|
| 52 |
|
| 53 |
-
status_html = (
|
| 54 |
-
f"<div style='margin:12px 0'>"
|
| 55 |
-
f"{action_badge(action)}"
|
| 56 |
-
f"<p style='margin-top:8px;color:#374151'>{reason}</p>"
|
| 57 |
-
f"</div>"
|
| 58 |
-
)
|
| 59 |
-
|
| 60 |
-
details_parts = [
|
| 61 |
-
"### Validation",
|
| 62 |
-
f"- Parse OK: {'Yes' if parse_ok else 'No'}",
|
| 63 |
-
f"- Changed lines: {changed_ratio:.0%}",
|
| 64 |
-
f"- Resolved: {', '.join(f'`{r}`' for r in resolved) if resolved else 'none'}",
|
| 65 |
-
f"- Unresolved: {', '.join(f'`{r}`' for r in unresolved) if unresolved else 'none'}",
|
| 66 |
-
"",
|
| 67 |
-
"### Explanation",
|
| 68 |
-
explanation,
|
| 69 |
-
]
|
| 70 |
-
if risk_notes:
|
| 71 |
-
details_parts += ["", "### Risk Notes"] + [f"- {n}" for n in risk_notes]
|
| 72 |
-
|
| 73 |
return (
|
| 74 |
original,
|
| 75 |
fixed,
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
_FONT = "'IBM Plex Sans', system-ui, -apple-system, sans-serif"
|
| 4 |
+
_MONO = "'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace"
|
| 5 |
+
|
| 6 |
+
_SVG_ERROR = (
|
| 7 |
+
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
|
| 8 |
+
"<path d='M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14 14-6.2 14-14S23.8 2 16 2z"
|
| 9 |
+
"m5.4 21L16 17.6 10.6 23 9 21.4l5.4-5.4L9 10.6 10.6 9 16 14.4 21.4 9 23 10.6"
|
| 10 |
+
"l-5.4 5.4 5.4 5.4L21.4 23z'/></svg>"
|
| 11 |
+
)
|
| 12 |
+
_SVG_WARNING = (
|
| 13 |
+
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
|
| 14 |
+
"<path d='M16 2L2 30h28L16 2zm-1.33 10.67h2.67V20h-2.67z"
|
| 15 |
+
"M16 26a1.33 1.33 0 110-2.67A1.33 1.33 0 0116 26z'/></svg>"
|
| 16 |
+
)
|
| 17 |
+
_SVG_CHECK = (
|
| 18 |
+
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
|
| 19 |
+
"<path d='M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14 14-6.2 14-14S23.8 2 16 2z"
|
| 20 |
+
"m-3 20.6l-5.6-5.6 1.4-1.4 4.2 4.2 9.2-9.2 1.4 1.4L13 22.6z'/></svg>"
|
| 21 |
+
)
|
| 22 |
+
_SVG_INFO = (
|
| 23 |
+
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
|
| 24 |
+
"<path d='M17 22v-8h-4v2h2v6h-2v2h6v-2zM15 8a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"
|
| 25 |
+
"M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14 14-6.2 14-14S23.8 2 16 2z'/></svg>"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
_NOTIFICATION: dict[str, dict] = {
|
| 29 |
+
"approved": {
|
| 30 |
+
"border": "#24a148", "bg": "#defbe6", "color": "#0e6027",
|
| 31 |
+
"icon": _SVG_CHECK, "label": "APPROVED",
|
| 32 |
+
},
|
| 33 |
+
"needs_human_review": {
|
| 34 |
+
"border": "#f1c21b", "bg": "#fdf6dd", "color": "#695200",
|
| 35 |
+
"icon": _SVG_WARNING, "label": "NEEDS HUMAN REVIEW",
|
| 36 |
+
},
|
| 37 |
+
"rejected": {
|
| 38 |
+
"border": "#da1e28", "bg": "#fff1f1", "color": "#a2191f",
|
| 39 |
+
"icon": _SVG_ERROR, "label": "REJECTED",
|
| 40 |
+
},
|
| 41 |
+
"refused": {
|
| 42 |
+
"border": "#da1e28", "bg": "#fff1f1", "color": "#a2191f",
|
| 43 |
+
"icon": _SVG_ERROR, "label": "REFUSED",
|
| 44 |
+
},
|
| 45 |
+
}
|
| 46 |
+
_DEFAULT_NOTIFICATION = {
|
| 47 |
+
"border": "#8d8d8d", "bg": "#f4f4f4", "color": "#525252",
|
| 48 |
+
"icon": _SVG_INFO, "label": "UNKNOWN",
|
| 49 |
}
|
| 50 |
|
| 51 |
|
| 52 |
+
def _notification_html(action: str, reason: str) -> str:
|
| 53 |
+
cfg = _NOTIFICATION.get(action, _DEFAULT_NOTIFICATION)
|
| 54 |
+
return (
|
| 55 |
+
f"<div style='"
|
| 56 |
+
f"display:flex;align-items:flex-start;gap:12px;"
|
| 57 |
+
f"background:{cfg['bg']};"
|
| 58 |
+
f"border-left:4px solid {cfg['border']};"
|
| 59 |
+
f"padding:14px 16px;"
|
| 60 |
+
f"font-family:{_FONT};"
|
| 61 |
+
f"'>"
|
| 62 |
+
f"<span style='color:{cfg['color']};flex-shrink:0;margin-top:1px'>{cfg['icon']}</span>"
|
| 63 |
+
f"<div>"
|
| 64 |
+
f"<p style='font-size:12px;font-weight:600;letter-spacing:0.32px;"
|
| 65 |
+
f"color:{cfg['color']};margin:0 0 4px;text-transform:uppercase'>{cfg['label']}</p>"
|
| 66 |
+
f"<p style='font-size:14px;color:#525252;margin:0;line-height:1.5'>{reason}</p>"
|
| 67 |
+
f"</div>"
|
| 68 |
+
f"</div>"
|
| 69 |
+
)
|
| 70 |
|
| 71 |
|
| 72 |
+
def _severity_pill(sev: str) -> str:
|
| 73 |
+
colors = {
|
| 74 |
+
"CRITICAL": ("#750e13", "#fff1f1"),
|
| 75 |
+
"HIGH": ("#a2191f", "#fff1f1"),
|
| 76 |
+
"MEDIUM": ("#5d3617", "#fdf6dd"),
|
| 77 |
+
"LOW": ("#0043ce", "#edf5ff"),
|
| 78 |
+
}
|
| 79 |
+
fg, bg = colors.get(sev, ("#525252", "#f4f4f4"))
|
| 80 |
+
return (
|
| 81 |
+
f"<span style='font-family:{_MONO};font-size:11px;font-weight:500;"
|
| 82 |
+
f"background:{bg};color:{fg};padding:1px 6px;"
|
| 83 |
+
f"letter-spacing:0.16px'>{sev}</span>"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def render_findings_html(rule_ids: list[str], summary: dict) -> str:
|
| 88 |
failed = summary.get("failed", len(rule_ids))
|
| 89 |
+
crit = int(summary.get("CRITICAL", 0))
|
| 90 |
+
high = int(summary.get("HIGH", 0))
|
| 91 |
+
med = int(summary.get("MEDIUM", 0))
|
| 92 |
+
|
| 93 |
+
rows = "".join(
|
| 94 |
+
f"<tr>"
|
| 95 |
+
f"<td style='padding:6px 0;border-bottom:1px solid #e0e0e0;"
|
| 96 |
+
f"font-family:{_MONO};font-size:12px;color:#161616'>{rid}</td>"
|
| 97 |
+
f"</tr>"
|
| 98 |
+
for rid in rule_ids
|
| 99 |
+
)
|
| 100 |
+
table = (
|
| 101 |
+
f"<table style='width:100%;border-collapse:collapse'>"
|
| 102 |
+
f"<thead><tr><th style='text-align:left;padding:0 0 8px;"
|
| 103 |
+
f"font-family:{_FONT};font-size:11px;font-weight:600;letter-spacing:0.32px;"
|
| 104 |
+
f"text-transform:uppercase;color:#525252;border-bottom:1px solid #393939'>"
|
| 105 |
+
f"Rule ID</th></tr></thead>"
|
| 106 |
+
f"<tbody>{rows}</tbody></table>"
|
| 107 |
+
) if rule_ids else "<p style='color:#8d8d8d;font-size:13px;margin:8px 0'>No findings</p>"
|
| 108 |
+
|
| 109 |
+
pills = " ".join([
|
| 110 |
+
_severity_pill("CRITICAL") + f" <b style='font-size:13px'>{crit}</b>" if crit else "",
|
| 111 |
+
_severity_pill("HIGH") + f" <b style='font-size:13px'>{high}</b>" if high else "",
|
| 112 |
+
_severity_pill("MEDIUM") + f" <b style='font-size:13px'>{med}</b>" if med else "",
|
| 113 |
+
])
|
| 114 |
+
|
| 115 |
+
return (
|
| 116 |
+
f"<div style='font-family:{_FONT}'>"
|
| 117 |
+
f"<div style='display:flex;align-items:center;justify-content:space-between;"
|
| 118 |
+
f"margin-bottom:12px'>"
|
| 119 |
+
f"<span style='font-size:24px;font-weight:300;color:#161616'>{failed}</span>"
|
| 120 |
+
f"<span style='font-size:12px;letter-spacing:0.32px;text-transform:uppercase;"
|
| 121 |
+
f"color:#525252'>failed checks</span>"
|
| 122 |
+
f"</div>"
|
| 123 |
+
f"<div style='display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap'>{pills}</div>"
|
| 124 |
+
f"{table}"
|
| 125 |
+
f"</div>"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _details_html(
|
| 130 |
+
parse_ok: bool,
|
| 131 |
+
changed_ratio: float,
|
| 132 |
+
resolved: list[str],
|
| 133 |
+
unresolved: list[str],
|
| 134 |
+
explanation: str,
|
| 135 |
+
risk_notes: list[str],
|
| 136 |
+
) -> str:
|
| 137 |
+
def _row(label: str, value: str) -> str:
|
| 138 |
+
return (
|
| 139 |
+
f"<tr>"
|
| 140 |
+
f"<td style='width:160px;padding:8px 0;border-bottom:1px solid #e0e0e0;"
|
| 141 |
+
f"font-size:11px;font-weight:600;letter-spacing:0.32px;"
|
| 142 |
+
f"text-transform:uppercase;color:#525252;vertical-align:top'>{label}</td>"
|
| 143 |
+
f"<td style='padding:8px 0 8px 16px;border-bottom:1px solid #e0e0e0;"
|
| 144 |
+
f"font-size:13px;color:#161616;font-family:{_MONO}'>{value}</td>"
|
| 145 |
+
f"</tr>"
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
def _pill_list(items: list[str], ok: bool) -> str:
|
| 149 |
+
if not items:
|
| 150 |
+
return "<span style='color:#8d8d8d'>—</span>"
|
| 151 |
+
color = "#0e6027" if ok else "#a2191f"
|
| 152 |
+
bg = "#defbe6" if ok else "#fff1f1"
|
| 153 |
+
pills = "".join(
|
| 154 |
+
f"<span style='display:inline-block;margin:2px 4px 2px 0;"
|
| 155 |
+
f"background:{bg};color:{color};padding:2px 8px;"
|
| 156 |
+
f"font-size:11px;font-weight:500'>{r}</span>"
|
| 157 |
+
for r in items
|
| 158 |
+
)
|
| 159 |
+
return f"<div style='display:flex;flex-wrap:wrap;gap:2px'>{pills}</div>"
|
| 160 |
+
|
| 161 |
+
risks_html = ""
|
| 162 |
+
if risk_notes:
|
| 163 |
+
items = "".join(
|
| 164 |
+
f"<li style='margin-bottom:4px;color:#525252'>{n}</li>"
|
| 165 |
+
for n in risk_notes
|
| 166 |
+
)
|
| 167 |
+
risks_html = (
|
| 168 |
+
f"<div style='margin-top:16px;padding:12px;background:#fdf6dd;"
|
| 169 |
+
f"border-left:3px solid #f1c21b'>"
|
| 170 |
+
f"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
|
| 171 |
+
f"text-transform:uppercase;color:#695200;margin:0 0 8px'>Risk Notes</p>"
|
| 172 |
+
f"<ul style='margin:0;padding-left:16px;font-size:13px'>{items}</ul>"
|
| 173 |
+
f"</div>"
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
table = (
|
| 177 |
+
f"<table style='width:100%;border-collapse:collapse;font-family:{_FONT}'>"
|
| 178 |
+
f"{_row('Parse OK', 'Yes' if parse_ok else 'No')}"
|
| 179 |
+
f"{_row('Changed lines', f'{changed_ratio:.0%}')}"
|
| 180 |
+
f"{_row('Resolved', _pill_list(resolved, ok=True))}"
|
| 181 |
+
f"{_row('Unresolved', _pill_list(unresolved, ok=False))}"
|
| 182 |
+
f"</table>"
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
expl_html = (
|
| 186 |
+
f"<div style='margin-top:16px;padding:12px 16px;"
|
| 187 |
+
f"background:#f4f4f4;border-left:3px solid #0f62fe'>"
|
| 188 |
+
f"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
|
| 189 |
+
f"text-transform:uppercase;color:#0043ce;margin:0 0 6px'>Explanation</p>"
|
| 190 |
+
f"<p style='font-size:13px;color:#161616;margin:0;line-height:1.6'>{explanation}</p>"
|
| 191 |
+
f"</div>"
|
| 192 |
+
) if explanation else ""
|
| 193 |
|
| 194 |
+
return f"<div>{table}{expl_html}{risks_html}</div>"
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
def render_result(data: dict) -> tuple[str, str, str, str, str, str]:
|
|
|
|
| 213 |
unresolved: list[str] = validation.get("targeted_unresolved") or []
|
| 214 |
parse_ok: bool = validation.get("parse_ok", False)
|
| 215 |
changed_ratio: float = validation.get("changed_lines_ratio", 0.0)
|
|
|
|
| 216 |
explanation = patch_obj.get("explanation") or reason
|
| 217 |
risk_notes: list[str] = patch_obj.get("risk_notes") or []
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
return (
|
| 220 |
original,
|
| 221 |
fixed,
|
| 222 |
+
_notification_html(action, reason),
|
| 223 |
+
render_findings_html(pre_rules, pre_summary),
|
| 224 |
+
render_findings_html(post_rules, post_summary),
|
| 225 |
+
_details_html(parse_ok, changed_ratio, resolved, unresolved, explanation, risk_notes),
|
| 226 |
)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def error_html(label: str, exc: Exception) -> str:
|
| 230 |
+
return _notification_html("rejected", f"<b>{label}:</b> {exc}")
|
iac_secfix_space/ui.py
CHANGED
|
@@ -10,9 +10,166 @@ import gradio as gr
|
|
| 10 |
|
| 11 |
from .demo_data import load_demos
|
| 12 |
from .hf_inference import LORA_MODEL_ID, build_hf_runtime_config, has_hf_config
|
| 13 |
-
from .rendering import render_result
|
| 14 |
|
| 15 |
-
_N = 6
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def _empty() -> tuple:
|
|
@@ -50,18 +207,14 @@ def _live_outputs(file_content: str, iac_type: str) -> tuple:
|
|
| 50 |
}
|
| 51 |
return render_result(data)
|
| 52 |
except Exception as exc:
|
| 53 |
-
return (
|
| 54 |
-
file_content, "",
|
| 55 |
-
f"<div style='color:red;padding:8px'><b>Error:</b> {exc}</div>",
|
| 56 |
-
"", "", "",
|
| 57 |
-
)
|
| 58 |
|
| 59 |
|
| 60 |
def _lora_outputs(file_content: str, iac_type: str) -> tuple:
|
| 61 |
if not file_content.strip():
|
| 62 |
return _empty()
|
| 63 |
try:
|
| 64 |
-
from iac_secfix_agents import IngestRequest,
|
| 65 |
|
| 66 |
config = build_hf_runtime_config()
|
| 67 |
decision = RemediationOrchestrator(config=config).remediate(
|
|
@@ -83,118 +236,131 @@ def _lora_outputs(file_content: str, iac_type: str) -> tuple:
|
|
| 83 |
}
|
| 84 |
return render_result(data)
|
| 85 |
except Exception as exc:
|
| 86 |
-
return (
|
| 87 |
-
file_content, "",
|
| 88 |
-
f"<div style='color:red;padding:8px'><b>LoRA inference error:</b> {exc}</div>",
|
| 89 |
-
"", "", "",
|
| 90 |
-
)
|
| 91 |
|
| 92 |
|
| 93 |
-
def
|
| 94 |
-
orig = gr.Code(label=label_orig, language="yaml", interactive=interactive_orig, lines=
|
| 95 |
-
fixed = gr.Code(label=label_fixed, language="yaml", interactive=False, lines=
|
| 96 |
return orig, fixed
|
| 97 |
|
| 98 |
|
| 99 |
-
def
|
| 100 |
status = gr.HTML()
|
| 101 |
with gr.Row():
|
| 102 |
-
before = gr.
|
| 103 |
-
after = gr.
|
| 104 |
-
details = gr.
|
| 105 |
return status, before, after, details
|
| 106 |
|
| 107 |
|
| 108 |
def build_demo() -> gr.Blocks:
|
| 109 |
demos = load_demos()
|
| 110 |
choices = list(demos.keys())
|
| 111 |
-
has_live_vllm = bool(os.getenv("IAC_SECFIX_BASE_URL"))
|
| 112 |
has_lora = has_hf_config()
|
|
|
|
| 113 |
|
| 114 |
-
with gr.Blocks(title="IaC-SecFix", theme=gr.themes.
|
| 115 |
-
gr.Markdown(
|
| 116 |
-
f"""
|
| 117 |
-
# IaC-SecFix
|
| 118 |
-
**Scanner-verified multi-agent remediation for Terraform and Kubernetes**
|
| 119 |
-
|
| 120 |
-
Multi-agent loop: **IngestAgent** → **PolicyContextAgent** → **PatchAgent** →
|
| 121 |
-
**ValidationAgent** → **SafetyAgent** → **HumanApprovalGate**
|
| 122 |
|
| 123 |
-
|
| 124 |
-
| Base model: `Qwen/Qwen3.5-9B` | r=8, alpha=16
|
| 125 |
-
| Built for the **AMD Developer Hackathon 2026**
|
| 126 |
-
"""
|
| 127 |
-
)
|
| 128 |
|
| 129 |
with gr.Tabs():
|
| 130 |
-
|
|
|
|
| 131 |
with gr.Tab("Demo Playback"):
|
| 132 |
-
gr.
|
| 133 |
-
"
|
| 134 |
-
"
|
|
|
|
|
|
|
| 135 |
)
|
| 136 |
with gr.Row():
|
| 137 |
-
demo_dd = gr.Dropdown(
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
demo_status, demo_before, demo_after, demo_details =
|
| 142 |
with gr.Row():
|
| 143 |
-
demo_orig, demo_fixed =
|
| 144 |
|
| 145 |
-
|
| 146 |
-
load_btn.click(fn=lambda l: _demo_outputs(l, demos), inputs=[demo_dd], outputs=
|
| 147 |
-
app.load(fn=lambda: _demo_outputs(choices[0] if choices else None, demos), outputs=
|
| 148 |
|
| 149 |
-
# ──
|
| 150 |
if has_lora:
|
| 151 |
with gr.Tab("Fine-tuned LoRA"):
|
| 152 |
-
gr.
|
| 153 |
-
f"
|
| 154 |
-
f"
|
| 155 |
-
"
|
| 156 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
)
|
| 158 |
with gr.Row():
|
| 159 |
-
lora_code = gr.Code(
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
lora_btn = gr.Button("Run with Fine-tuned LoRA", variant="primary")
|
| 164 |
-
lora_status, lora_before, lora_after, lora_details =
|
| 165 |
with gr.Row():
|
| 166 |
-
lora_orig, lora_fixed =
|
| 167 |
lora_btn.click(
|
| 168 |
fn=_lora_outputs,
|
| 169 |
inputs=[lora_code, lora_type],
|
| 170 |
-
outputs=[lora_orig, lora_fixed, lora_status,
|
|
|
|
| 171 |
)
|
| 172 |
|
| 173 |
-
# ──
|
| 174 |
if has_live_vllm:
|
| 175 |
-
with gr.Tab("Live
|
| 176 |
-
gr.
|
| 177 |
-
"
|
| 178 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
)
|
| 180 |
with gr.Row():
|
| 181 |
-
live_code = gr.Code(
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
live_btn = gr.Button("Run Agent Loop", variant="primary")
|
| 186 |
-
live_status, live_before, live_after, live_details =
|
| 187 |
with gr.Row():
|
| 188 |
-
live_orig, live_fixed =
|
| 189 |
live_btn.click(
|
| 190 |
fn=_live_outputs,
|
| 191 |
inputs=[live_code, live_type],
|
| 192 |
-
outputs=[live_orig, live_fixed, live_status,
|
|
|
|
| 193 |
)
|
| 194 |
|
| 195 |
-
gr.
|
| 196 |
-
"---\n**IaC-SecFix** | AMD Developer Hackathon 2026 | "
|
| 197 |
-
"Benchmark B3 (zero-shot+RAG): 50 samples · 32.6% targeted resolution · 0% unsafe actions"
|
| 198 |
-
)
|
| 199 |
|
| 200 |
return app
|
|
|
|
| 10 |
|
| 11 |
from .demo_data import load_demos
|
| 12 |
from .hf_inference import LORA_MODEL_ID, build_hf_runtime_config, has_hf_config
|
| 13 |
+
from .rendering import error_html, render_findings_html, render_result
|
| 14 |
|
| 15 |
+
_N = 6
|
| 16 |
+
|
| 17 |
+
CARBON_CSS = """
|
| 18 |
+
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
| 19 |
+
|
| 20 |
+
/* ── Global reset ──────────────────────────────────────────────────── */
|
| 21 |
+
.gradio-container, .gradio-container * {
|
| 22 |
+
font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif !important;
|
| 23 |
+
border-radius: 0 !important;
|
| 24 |
+
box-sizing: border-box;
|
| 25 |
+
}
|
| 26 |
+
.gradio-container {
|
| 27 |
+
background: #f4f4f4 !important;
|
| 28 |
+
padding: 0 !important;
|
| 29 |
+
max-width: 100% !important;
|
| 30 |
+
}
|
| 31 |
+
body { background: #f4f4f4 !important; }
|
| 32 |
+
|
| 33 |
+
/* ── Code / mono ───────────────────────────────────────────────────── */
|
| 34 |
+
.cm-content, .cm-line, .cm-scroller {
|
| 35 |
+
font-family: 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace !important;
|
| 36 |
+
font-size: 13px !important;
|
| 37 |
+
line-height: 1.6 !important;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* ── Primary button ────────────────────────────────────────────────── */
|
| 41 |
+
button.primary {
|
| 42 |
+
background: #0f62fe !important;
|
| 43 |
+
color: #ffffff !important;
|
| 44 |
+
border: none !important;
|
| 45 |
+
font-size: 14px !important;
|
| 46 |
+
font-weight: 400 !important;
|
| 47 |
+
letter-spacing: 0.16px !important;
|
| 48 |
+
height: 48px !important;
|
| 49 |
+
transition: background 70ms ease !important;
|
| 50 |
+
}
|
| 51 |
+
button.primary:hover { background: #0050e6 !important; }
|
| 52 |
+
button.primary:active { background: #002d9c !important; }
|
| 53 |
+
|
| 54 |
+
/* ── Secondary button ──────────────────────────────────────────────── */
|
| 55 |
+
button.secondary {
|
| 56 |
+
background: transparent !important;
|
| 57 |
+
color: #0f62fe !important;
|
| 58 |
+
border: 1px solid #0f62fe !important;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* ── Tabs ──────────────────────────────────────────────────────────── */
|
| 62 |
+
div[role="tablist"] {
|
| 63 |
+
border-bottom: 1px solid #e0e0e0 !important;
|
| 64 |
+
background: #ffffff !important;
|
| 65 |
+
padding: 0 16px !important;
|
| 66 |
+
gap: 0 !important;
|
| 67 |
+
}
|
| 68 |
+
button[role="tab"] {
|
| 69 |
+
font-size: 14px !important;
|
| 70 |
+
letter-spacing: 0.16px !important;
|
| 71 |
+
color: #525252 !important;
|
| 72 |
+
background: transparent !important;
|
| 73 |
+
border: none !important;
|
| 74 |
+
border-bottom: 3px solid transparent !important;
|
| 75 |
+
padding: 12px 16px !important;
|
| 76 |
+
font-weight: 400 !important;
|
| 77 |
+
margin-bottom: -1px !important;
|
| 78 |
+
transition: color 70ms ease !important;
|
| 79 |
+
}
|
| 80 |
+
button[role="tab"]:hover { color: #161616 !important; }
|
| 81 |
+
button[role="tab"][aria-selected="true"] {
|
| 82 |
+
color: #161616 !important;
|
| 83 |
+
border-bottom: 3px solid #0f62fe !important;
|
| 84 |
+
font-weight: 600 !important;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* ── Labels ────────────────────────────────────────────────────────── */
|
| 88 |
+
label > span:first-child, .label-wrap > span {
|
| 89 |
+
font-size: 11px !important;
|
| 90 |
+
font-weight: 600 !important;
|
| 91 |
+
letter-spacing: 0.32px !important;
|
| 92 |
+
text-transform: uppercase !important;
|
| 93 |
+
color: #525252 !important;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* ── Markdown headings ─────────────────────────────────────────────── */
|
| 97 |
+
.prose h3 {
|
| 98 |
+
font-size: 11px !important;
|
| 99 |
+
font-weight: 600 !important;
|
| 100 |
+
letter-spacing: 0.32px !important;
|
| 101 |
+
text-transform: uppercase !important;
|
| 102 |
+
color: #525252 !important;
|
| 103 |
+
border-bottom: 1px solid #e0e0e0 !important;
|
| 104 |
+
padding-bottom: 8px !important;
|
| 105 |
+
margin: 20px 0 10px !important;
|
| 106 |
+
}
|
| 107 |
+
.prose code {
|
| 108 |
+
font-family: 'IBM Plex Mono', monospace !important;
|
| 109 |
+
background: #f4f4f4 !important;
|
| 110 |
+
border: 1px solid #e0e0e0 !important;
|
| 111 |
+
font-size: 0.85em !important;
|
| 112 |
+
padding: 1px 5px !important;
|
| 113 |
+
color: #393939 !important;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* ── Container blocks ──────────────────────────────────────────────── */
|
| 117 |
+
.block { border-color: #e0e0e0 !important; }
|
| 118 |
+
.tabitem { background: #ffffff !important; padding: 20px !important; }
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
_HEADER_HTML = f"""
|
| 122 |
+
<div style="background:#161616;padding:28px 32px 24px;margin:-8px -8px 0;border-bottom:1px solid #393939">
|
| 123 |
+
<p style="font-size:11px;font-weight:600;letter-spacing:0.32px;text-transform:uppercase;
|
| 124 |
+
color:#6f6f6f;margin:0 0 6px;font-family:'IBM Plex Sans',sans-serif">
|
| 125 |
+
AMD Developer Hackathon 2026
|
| 126 |
+
</p>
|
| 127 |
+
<h1 style="font-size:2rem;font-weight:300;letter-spacing:-0.64px;color:#ffffff;
|
| 128 |
+
margin:0 0 8px;font-family:'IBM Plex Sans',sans-serif">
|
| 129 |
+
IaC-SecFix
|
| 130 |
+
</h1>
|
| 131 |
+
<p style="font-size:14px;color:#a8a8a8;margin:0 0 14px;font-family:'IBM Plex Sans',sans-serif">
|
| 132 |
+
Scanner-verified multi-agent remediation for Terraform and Kubernetes
|
| 133 |
+
</p>
|
| 134 |
+
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center">
|
| 135 |
+
<span style="font-size:11px;color:#6f6f6f;font-family:'IBM Plex Mono',monospace;
|
| 136 |
+
background:#262626;padding:3px 8px">IngestAgent</span>
|
| 137 |
+
<span style="color:#525252;font-size:12px">→</span>
|
| 138 |
+
<span style="font-size:11px;color:#6f6f6f;font-family:'IBM Plex Mono',monospace;
|
| 139 |
+
background:#262626;padding:3px 8px">PolicyContextAgent</span>
|
| 140 |
+
<span style="color:#525252;font-size:12px">→</span>
|
| 141 |
+
<span style="font-size:11px;color:#6f6f6f;font-family:'IBM Plex Mono',monospace;
|
| 142 |
+
background:#262626;padding:3px 8px">PatchAgent</span>
|
| 143 |
+
<span style="color:#525252;font-size:12px">→</span>
|
| 144 |
+
<span style="font-size:11px;color:#6f6f6f;font-family:'IBM Plex Mono',monospace;
|
| 145 |
+
background:#262626;padding:3px 8px">ValidationAgent</span>
|
| 146 |
+
<span style="color:#525252;font-size:12px">→</span>
|
| 147 |
+
<span style="font-size:11px;color:#6f6f6f;font-family:'IBM Plex Mono',monospace;
|
| 148 |
+
background:#262626;padding:3px 8px">SafetyAgent</span>
|
| 149 |
+
<span style="color:#525252;font-size:12px">→</span>
|
| 150 |
+
<span style="font-size:11px;color:#6f6f6f;font-family:'IBM Plex Mono',monospace;
|
| 151 |
+
background:#262626;padding:3px 8px">HumanApprovalGate</span>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
_FOOTER_HTML = """
|
| 157 |
+
<div style="background:#f4f4f4;border-top:1px solid #e0e0e0;padding:16px 32px;
|
| 158 |
+
display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
|
| 159 |
+
<div style="display:flex;gap:24px;flex-wrap:wrap">
|
| 160 |
+
<span style="font-size:12px;color:#525252;font-family:'IBM Plex Sans',sans-serif">
|
| 161 |
+
<b style="color:#161616">IaC-SecFix</b> · AMD Developer Hackathon 2026
|
| 162 |
+
</span>
|
| 163 |
+
</div>
|
| 164 |
+
<div style="display:flex;gap:24px;flex-wrap:wrap">
|
| 165 |
+
<span style="font-size:11px;color:#6f6f6f;font-family:'IBM Plex Sans',sans-serif">
|
| 166 |
+
B3 benchmark · 50 samples ·
|
| 167 |
+
<b style="color:#24a148">32.6%</b> targeted resolution ·
|
| 168 |
+
<b style="color:#24a148">0%</b> unsafe actions
|
| 169 |
+
</span>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
"""
|
| 173 |
|
| 174 |
|
| 175 |
def _empty() -> tuple:
|
|
|
|
| 207 |
}
|
| 208 |
return render_result(data)
|
| 209 |
except Exception as exc:
|
| 210 |
+
return (file_content, "", error_html("Error", exc), "", "", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
|
| 213 |
def _lora_outputs(file_content: str, iac_type: str) -> tuple:
|
| 214 |
if not file_content.strip():
|
| 215 |
return _empty()
|
| 216 |
try:
|
| 217 |
+
from iac_secfix_agents import IngestRequest, RemediationOrchestrator
|
| 218 |
|
| 219 |
config = build_hf_runtime_config()
|
| 220 |
decision = RemediationOrchestrator(config=config).remediate(
|
|
|
|
| 236 |
}
|
| 237 |
return render_result(data)
|
| 238 |
except Exception as exc:
|
| 239 |
+
return (file_content, "", error_html("LoRA inference error", exc), "", "", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
|
| 242 |
+
def _code_pair(label_orig: str, label_fixed: str, interactive_orig: bool = False) -> tuple:
|
| 243 |
+
orig = gr.Code(label=label_orig, language="yaml", interactive=interactive_orig, lines=20)
|
| 244 |
+
fixed = gr.Code(label=label_fixed, language="yaml", interactive=False, lines=20)
|
| 245 |
return orig, fixed
|
| 246 |
|
| 247 |
|
| 248 |
+
def _result_section() -> tuple:
|
| 249 |
status = gr.HTML()
|
| 250 |
with gr.Row():
|
| 251 |
+
before = gr.HTML(label="Findings Before")
|
| 252 |
+
after = gr.HTML(label="Findings After")
|
| 253 |
+
details = gr.HTML()
|
| 254 |
return status, before, after, details
|
| 255 |
|
| 256 |
|
| 257 |
def build_demo() -> gr.Blocks:
|
| 258 |
demos = load_demos()
|
| 259 |
choices = list(demos.keys())
|
|
|
|
| 260 |
has_lora = has_hf_config()
|
| 261 |
+
has_live_vllm = bool(os.getenv("IAC_SECFIX_BASE_URL"))
|
| 262 |
|
| 263 |
+
with gr.Blocks(title="IaC-SecFix", css=CARBON_CSS, theme=gr.themes.Base()) as app:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
+
gr.HTML(_HEADER_HTML)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
with gr.Tabs():
|
| 268 |
+
|
| 269 |
+
# ── Demo Playback ─────────────────────────────────────────────
|
| 270 |
with gr.Tab("Demo Playback"):
|
| 271 |
+
gr.HTML(
|
| 272 |
+
"<p style='font-size:13px;color:#525252;padding:0 0 16px;"
|
| 273 |
+
"font-family:\"IBM Plex Sans\",sans-serif'>"
|
| 274 |
+
"Precomputed benchmark results — no GPU required. "
|
| 275 |
+
"Select an example to load the full agent trace.</p>"
|
| 276 |
)
|
| 277 |
with gr.Row():
|
| 278 |
+
demo_dd = gr.Dropdown(
|
| 279 |
+
choices=choices,
|
| 280 |
+
value=choices[0] if choices else None,
|
| 281 |
+
label="Example",
|
| 282 |
+
scale=5,
|
| 283 |
+
)
|
| 284 |
+
load_btn = gr.Button("Load Example", variant="primary", scale=1)
|
| 285 |
|
| 286 |
+
demo_status, demo_before, demo_after, demo_details = _result_section()
|
| 287 |
with gr.Row():
|
| 288 |
+
demo_orig, demo_fixed = _code_pair("Original File", "Fixed File")
|
| 289 |
|
| 290 |
+
outs = [demo_orig, demo_fixed, demo_status, demo_before, demo_after, demo_details]
|
| 291 |
+
load_btn.click(fn=lambda l: _demo_outputs(l, demos), inputs=[demo_dd], outputs=outs)
|
| 292 |
+
app.load(fn=lambda: _demo_outputs(choices[0] if choices else None, demos), outputs=outs)
|
| 293 |
|
| 294 |
+
# ── Fine-tuned LoRA ───────────────────────────────────────────
|
| 295 |
if has_lora:
|
| 296 |
with gr.Tab("Fine-tuned LoRA"):
|
| 297 |
+
gr.HTML(
|
| 298 |
+
f"<p style='font-size:13px;color:#525252;padding:0 0 16px;"
|
| 299 |
+
f"font-family:\"IBM Plex Sans\",sans-serif'>"
|
| 300 |
+
f"Full agent loop via fine-tuned LoRA adapter "
|
| 301 |
+
f"<a href='https://huggingface.co/{LORA_MODEL_ID}' "
|
| 302 |
+
f"style='color:#0f62fe;text-decoration:none'>"
|
| 303 |
+
f"<code style='font-family:\"IBM Plex Mono\",monospace'>"
|
| 304 |
+
f"{LORA_MODEL_ID}</code></a>. "
|
| 305 |
+
f"Base: <code style='font-family:\"IBM Plex Mono\",monospace'>"
|
| 306 |
+
f"Qwen/Qwen3.5-9B</code> · r=8, alpha=16</p>"
|
| 307 |
)
|
| 308 |
with gr.Row():
|
| 309 |
+
lora_code = gr.Code(
|
| 310 |
+
label="IaC File", language="yaml", interactive=True, lines=20
|
| 311 |
+
)
|
| 312 |
+
lora_type = gr.Dropdown(
|
| 313 |
+
choices=["terraform", "kubernetes"],
|
| 314 |
+
value="terraform",
|
| 315 |
+
label="IaC Type",
|
| 316 |
+
scale=0,
|
| 317 |
+
)
|
| 318 |
lora_btn = gr.Button("Run with Fine-tuned LoRA", variant="primary")
|
| 319 |
+
lora_status, lora_before, lora_after, lora_details = _result_section()
|
| 320 |
with gr.Row():
|
| 321 |
+
lora_orig, lora_fixed = _code_pair("Input", "Fixed")
|
| 322 |
lora_btn.click(
|
| 323 |
fn=_lora_outputs,
|
| 324 |
inputs=[lora_code, lora_type],
|
| 325 |
+
outputs=[lora_orig, lora_fixed, lora_status,
|
| 326 |
+
lora_before, lora_after, lora_details],
|
| 327 |
)
|
| 328 |
|
| 329 |
+
# ── Live vLLM endpoint ────────────────────────────────────────
|
| 330 |
if has_live_vllm:
|
| 331 |
+
with gr.Tab("Live Endpoint"):
|
| 332 |
+
gr.HTML(
|
| 333 |
+
"<p style='font-size:13px;color:#525252;padding:0 0 16px;"
|
| 334 |
+
"font-family:\"IBM Plex Sans\",sans-serif'>"
|
| 335 |
+
"OpenAI-compatible vLLM endpoint. "
|
| 336 |
+
"Set <code style='font-family:\"IBM Plex Mono\",monospace'>"
|
| 337 |
+
"IAC_SECFIX_BASE_URL</code>, "
|
| 338 |
+
"<code style='font-family:\"IBM Plex Mono\",monospace'>"
|
| 339 |
+
"IAC_SECFIX_API_KEY</code>, and "
|
| 340 |
+
"<code style='font-family:\"IBM Plex Mono\",monospace'>"
|
| 341 |
+
"IAC_SECFIX_MODEL</code> in Space secrets.</p>"
|
| 342 |
)
|
| 343 |
with gr.Row():
|
| 344 |
+
live_code = gr.Code(
|
| 345 |
+
label="IaC File", language="yaml", interactive=True, lines=20
|
| 346 |
+
)
|
| 347 |
+
live_type = gr.Dropdown(
|
| 348 |
+
choices=["terraform", "kubernetes"],
|
| 349 |
+
value="terraform",
|
| 350 |
+
label="IaC Type",
|
| 351 |
+
scale=0,
|
| 352 |
+
)
|
| 353 |
live_btn = gr.Button("Run Agent Loop", variant="primary")
|
| 354 |
+
live_status, live_before, live_after, live_details = _result_section()
|
| 355 |
with gr.Row():
|
| 356 |
+
live_orig, live_fixed = _code_pair("Input", "Fixed")
|
| 357 |
live_btn.click(
|
| 358 |
fn=_live_outputs,
|
| 359 |
inputs=[live_code, live_type],
|
| 360 |
+
outputs=[live_orig, live_fixed, live_status,
|
| 361 |
+
live_before, live_after, live_details],
|
| 362 |
)
|
| 363 |
|
| 364 |
+
gr.HTML(_FOOTER_HTML)
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
return app
|