iac-secfix / iac_secfix_space /rendering.py
selcuksntrk's picture
Add IaC-SecFix app with Gradio UI and agent backend
7f7def1 verified
Raw
History Blame Contribute Delete
10.5 kB
from __future__ import annotations
# ── Dark G100 design tokens ──────────────────────────────────────────────────
_BG = "#161616"
_L1 = "#262626" # layer 01 — panels
_L2 = "#393939" # layer 02 — rows / hover
_L3 = "#525252" # layer 03 — borders
_T1 = "#f4f4f4" # text primary
_T2 = "#c6c6c6" # text secondary
_T3 = "#8d8d8d" # text helper / labels
_T4 = "#6f6f6f" # text disabled / placeholder
_BLUE = "#4589ff" # interactive (blue-40)
_BLUE_DIM = "#001d6c" # blue tint bg (blue-90)
_BLUE_LNK = "#78a9ff" # link / code (blue-30)
# status — dark
_ERR_BG = "#2d0709"; _ERR_BD = "#ff8389"; _ERR_FG = "#ff8389"
_WARN_BG = "#302200"; _WARN_BD = "#f1c21b"; _WARN_FG = "#f1c21b"
_OK_BG = "#071908"; _OK_BD = "#42be65"; _OK_FG = "#42be65"
_INFO_BG = _BLUE_DIM; _INFO_BD = _BLUE; _INFO_FG = _BLUE
_FONT = "'IBM Plex Sans', system-ui, -apple-system, sans-serif"
_MONO = "'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace"
# ── SVG icons ────────────────────────────────────────────────────────────────
_SVG_ERROR = (
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
"<path d='M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14 14-6.2 14-14S23.8 2 16 2z"
"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"
"l-5.4 5.4 5.4 5.4L21.4 23z'/></svg>"
)
_SVG_WARNING = (
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
"<path d='M16 2L2 30h28L16 2zm-1.33 10.67h2.67V20h-2.67z"
"M16 26a1.33 1.33 0 110-2.67A1.33 1.33 0 0116 26z'/></svg>"
)
_SVG_CHECK = (
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
"<path d='M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14 14-6.2 14-14S23.8 2 16 2z"
"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>"
)
_SVG_INFO = (
"<svg width='20' height='20' viewBox='0 0 32 32' fill='currentColor'>"
"<path d='M17 22v-8h-4v2h2v6h-2v2h6v-2zM15 8a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"
"M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14 14-6.2 14-14S23.8 2 16 2z'/></svg>"
)
_NOTIFICATION: dict[str, dict] = {
"approved": {"border": _OK_BD, "bg": _OK_BG, "color": _OK_FG, "icon": _SVG_CHECK, "label": "APPROVED"},
"needs_human_review": {"border": _WARN_BD, "bg": _WARN_BG, "color": _WARN_FG, "icon": _SVG_WARNING, "label": "NEEDS HUMAN REVIEW"},
"rejected": {"border": _ERR_BD, "bg": _ERR_BG, "color": _ERR_FG, "icon": _SVG_ERROR, "label": "REJECTED"},
"refused": {"border": _ERR_BD, "bg": _ERR_BG, "color": _ERR_FG, "icon": _SVG_ERROR, "label": "REFUSED"},
}
_DEFAULT_NOTIFICATION = {"border": _L3, "bg": _L1, "color": _T3, "icon": _SVG_INFO, "label": "UNKNOWN"}
def _notification_html(action: str, reason: str) -> str:
cfg = _NOTIFICATION.get(action, _DEFAULT_NOTIFICATION)
return (
f"<div style='display:flex;align-items:flex-start;gap:12px;"
f"background:{cfg['bg']};border-left:4px solid {cfg['border']};"
f"padding:14px 16px;font-family:{_FONT}'>"
f"<span style='color:{cfg['color']};flex-shrink:0;margin-top:1px'>{cfg['icon']}</span>"
f"<div>"
f"<p style='font-size:12px;font-weight:600;letter-spacing:0.32px;text-transform:uppercase;"
f"color:{cfg['color']};margin:0 0 4px'>{cfg['label']}</p>"
f"<p style='font-size:14px;color:{_T2};margin:0;line-height:1.5'>{reason}</p>"
f"</div></div>"
)
def _severity_pill(sev: str) -> str:
dark_colors = {
"CRITICAL": (_ERR_FG, _ERR_BG),
"HIGH": ("#ffb3b8", "#3b0d0e"),
"MEDIUM": (_WARN_FG, _WARN_BG),
"LOW": (_BLUE_LNK, "#001141"),
}
fg, bg = dark_colors.get(sev, (_T3, _L2))
return (
f"<span style='font-family:{_MONO};font-size:11px;font-weight:500;"
f"background:{bg};color:{fg};padding:1px 6px;letter-spacing:0.16px'>{sev}</span>"
)
def render_findings_html(rule_ids: list[str], summary: dict) -> str:
failed = summary.get("failed", len(rule_ids))
crit = int(summary.get("CRITICAL", 0))
high = int(summary.get("HIGH", 0))
med = int(summary.get("MEDIUM", 0))
rows = "".join(
f"<tr><td style='padding:6px 0;border-bottom:1px solid {_L2};"
f"font-family:{_MONO};font-size:12px;color:{_T2}'>{rid}</td></tr>"
for rid in rule_ids
)
table = (
f"<table style='width:100%;border-collapse:collapse'>"
f"<thead><tr><th style='text-align:left;padding:0 0 8px;"
f"font-family:{_FONT};font-size:11px;font-weight:600;letter-spacing:0.32px;"
f"text-transform:uppercase;color:{_T3};border-bottom:1px solid {_L3}'>"
f"Rule ID</th></tr></thead><tbody>{rows}</tbody></table>"
) if rule_ids else f"<p style='color:{_T4};font-size:13px;margin:8px 0'>No findings</p>"
pills = " ".join(filter(None, [
(_severity_pill("CRITICAL") + f" <b style='font-size:13px;color:{_T1}'>{crit}</b>") if crit else "",
(_severity_pill("HIGH") + f" <b style='font-size:13px;color:{_T1}'>{high}</b>") if high else "",
(_severity_pill("MEDIUM") + f" <b style='font-size:13px;color:{_T1}'>{med}</b>") if med else "",
]))
return (
f"<div style='font-family:{_FONT};background:{_L1};padding:16px'>"
f"<div style='display:flex;align-items:baseline;justify-content:space-between;margin-bottom:12px'>"
f"<span style='font-size:28px;font-weight:300;color:{_T1}'>{failed}</span>"
f"<span style='font-size:11px;letter-spacing:0.32px;text-transform:uppercase;color:{_T3}'>failed checks</span>"
f"</div>"
f"<div style='display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap'>{pills}</div>"
f"{table}</div>"
)
def _details_html(
parse_ok: bool,
changed_ratio: float,
resolved: list[str],
unresolved: list[str],
explanation: str,
risk_notes: list[str],
) -> str:
def _row(label: str, value: str) -> str:
return (
f"<tr>"
f"<td style='width:160px;padding:8px 0;border-bottom:1px solid {_L2};"
f"font-size:11px;font-weight:600;letter-spacing:0.32px;text-transform:uppercase;"
f"color:{_T3};vertical-align:top;font-family:{_FONT}'>{label}</td>"
f"<td style='padding:8px 0 8px 16px;border-bottom:1px solid {_L2};"
f"font-size:13px;color:{_T2};font-family:{_MONO}'>{value}</td>"
f"</tr>"
)
def _pill_list(items: list[str], ok: bool) -> str:
if not items:
return f"<span style='color:{_T4}'>—</span>"
fg = _OK_FG if ok else _ERR_FG
bg = _OK_BG if ok else _ERR_BG
pills = "".join(
f"<span style='display:inline-block;margin:2px 4px 2px 0;"
f"background:{bg};color:{fg};padding:2px 8px;font-size:11px;font-weight:500'>{r}</span>"
for r in items
)
return f"<div style='display:flex;flex-wrap:wrap;gap:2px'>{pills}</div>"
risks_html = ""
if risk_notes:
items = "".join(f"<li style='margin-bottom:4px;color:{_T2}'>{n}</li>" for n in risk_notes)
risks_html = (
f"<div style='margin-top:16px;padding:12px;"
f"background:{_WARN_BG};border-left:3px solid {_WARN_BD}'>"
f"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
f"text-transform:uppercase;color:{_WARN_FG};margin:0 0 8px;font-family:{_FONT}'>Risk Notes</p>"
f"<ul style='margin:0;padding-left:16px;font-size:13px'>{items}</ul>"
f"</div>"
)
expl_html = ""
if explanation:
expl_html = (
f"<div style='margin-top:16px;padding:12px 16px;"
f"background:{_INFO_BG};border-left:3px solid {_INFO_BD}'>"
f"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
f"text-transform:uppercase;color:{_INFO_FG};margin:0 0 6px;font-family:{_FONT}'>Explanation</p>"
f"<p style='font-size:13px;color:{_T2};margin:0;line-height:1.6;font-family:{_FONT}'>{explanation}</p>"
f"</div>"
)
table = (
f"<table style='width:100%;border-collapse:collapse;font-family:{_FONT}'>"
f"{_row('Parse OK', 'Yes' if parse_ok else 'No')}"
f"{_row('Changed lines', f'{changed_ratio:.0%}')}"
f"{_row('Resolved', _pill_list(resolved, ok=True))}"
f"{_row('Unresolved', _pill_list(unresolved, ok=False))}"
f"</table>"
)
return (
f"<div style='background:{_L1};padding:16px;font-family:{_FONT}'>"
f"{table}{expl_html}{risks_html}"
f"</div>"
)
def render_result(data: dict) -> tuple[str, str, str, str, str, str]:
decision = data.get("decision") or {}
action = decision.get("action", "unknown")
reason = decision.get("reason", "")
original = data.get("original_file", "")
patch_obj = decision.get("patch") or {}
fixed = patch_obj.get("fixed_file") or data.get("fixed_file", "")
pre_rules: list[str] = data.get("pre_rule_ids") or []
post_rules: list[str] = data.get("post_rule_ids") or []
pre_summary: dict = data.get("pre_summary") or {}
post_summary:dict = data.get("post_summary") or {}
validation = decision.get("validation") or data.get("validation") or {}
resolved: list[str] = validation.get("targeted_resolved") or []
unresolved: list[str] = validation.get("targeted_unresolved") or []
parse_ok: bool = validation.get("parse_ok", False)
changed_ratio: float = validation.get("changed_lines_ratio", 0.0)
explanation = patch_obj.get("explanation") or reason
risk_notes: list[str] = patch_obj.get("risk_notes") or []
return (
original,
fixed,
_notification_html(action, reason),
render_findings_html(pre_rules, pre_summary),
render_findings_html(post_rules, post_summary),
_details_html(parse_ok, changed_ratio, resolved, unresolved, explanation, risk_notes),
)
def error_html(label: str, exc: Exception) -> str:
return _notification_html("rejected", f"<b>{label}:</b> {exc}")