selcuksntrk commited on
Commit
d8495a0
·
verified ·
1 Parent(s): 70a8ba3

Add IaC-SecFix app with Gradio UI and agent backend

Browse files
Files changed (2) hide show
  1. iac_secfix_space/rendering.py +193 -43
  2. 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
- ACTION_STYLES: dict[str, str] = {
5
- "approved": "background:#22c55e;color:white",
6
- "needs_human_review": "background:#f97316;color:white",
7
- "rejected": "background:#ef4444;color:white",
8
- "refused": "background:#dc2626;color:white",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
 
12
- def action_badge(action: str) -> str:
13
- style = ACTION_STYLES.get(action, "background:#6b7280;color:white")
14
- label = action.upper().replace("_", " ")
15
- return f"<span style='{style};padding:5px 14px;border-radius:5px;font-weight:bold;font-size:1.1em'>{label}</span>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
- def render_findings_md(rule_ids: list[str], summary: dict) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- lines = [f"**{failed} failed checks** (CRITICAL: {crit} | HIGH: {high} | MEDIUM: {med})"]
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
- status_html,
77
- render_findings_md(pre_rules, pre_summary),
78
- render_findings_md(post_rules, post_summary),
79
- "\n".join(details_parts),
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 # number of output components
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, OrchestratorSettings, RemediationOrchestrator
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 _file_panels(label_orig: str, label_fixed: str, interactive_orig: bool = False) -> tuple:
94
- orig = gr.Code(label=label_orig, language="yaml", interactive=interactive_orig, lines=22)
95
- fixed = gr.Code(label=label_fixed, language="yaml", interactive=False, lines=22)
96
  return orig, fixed
97
 
98
 
99
- def _result_panels() -> tuple:
100
  status = gr.HTML()
101
  with gr.Row():
102
- before = gr.Markdown(label="Findings Before")
103
- after = gr.Markdown(label="Findings After")
104
- details = gr.Markdown()
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.Soft()) as app:
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
- Fine-tuned LoRA: [`{LORA_MODEL_ID}`](https://huggingface.co/{LORA_MODEL_ID})
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
- # ── Tab 1: Demo Playback ──────────────────────────────────────
 
131
  with gr.Tab("Demo Playback"):
132
- gr.Markdown(
133
- "Precomputed results from the benchmark run — no GPU required. "
134
- "Select an example to see the full agent trace."
 
 
135
  )
136
  with gr.Row():
137
- demo_dd = gr.Dropdown(choices=choices, value=choices[0] if choices else None,
138
- label="Example", scale=4)
139
- load_btn = gr.Button("Load", variant="primary", scale=1)
 
 
 
 
140
 
141
- demo_status, demo_before, demo_after, demo_details = _result_panels()
142
  with gr.Row():
143
- demo_orig, demo_fixed = _file_panels("Original File", "Fixed File")
144
 
145
- demo_outs = [demo_orig, demo_fixed, demo_status, demo_before, demo_after, demo_details]
146
- load_btn.click(fn=lambda l: _demo_outputs(l, demos), inputs=[demo_dd], outputs=demo_outs)
147
- app.load(fn=lambda: _demo_outputs(choices[0] if choices else None, demos), outputs=demo_outs)
148
 
149
- # ── Tab 2: Fine-tuned LoRA ────────────────────────────────────
150
  if has_lora:
151
  with gr.Tab("Fine-tuned LoRA"):
152
- gr.Markdown(
153
- f"Runs the full multi-agent loop using the fine-tuned LoRA adapter "
154
- f"[`{LORA_MODEL_ID}`](https://huggingface.co/{LORA_MODEL_ID}).\n\n"
155
- "**Space secrets required:** `HF_TOKEN` (serverless) "
156
- "or `IAC_SECFIX_HF_ENDPOINT` + `IAC_SECFIX_HF_TOKEN` (dedicated endpoint)."
 
 
 
 
 
157
  )
158
  with gr.Row():
159
- lora_code = gr.Code(label="Paste IaC file here", language="yaml",
160
- interactive=True, lines=22)
161
- lora_type = gr.Dropdown(choices=["terraform", "kubernetes"],
162
- value="terraform", label="IaC Type", scale=0)
 
 
 
 
 
163
  lora_btn = gr.Button("Run with Fine-tuned LoRA", variant="primary")
164
- lora_status, lora_before, lora_after, lora_details = _result_panels()
165
  with gr.Row():
166
- lora_orig, lora_fixed = _file_panels("Input File", "Fixed File")
167
  lora_btn.click(
168
  fn=_lora_outputs,
169
  inputs=[lora_code, lora_type],
170
- outputs=[lora_orig, lora_fixed, lora_status, lora_before, lora_after, lora_details],
 
171
  )
172
 
173
- # ── Tab 3: Live vLLM endpoint (optional) ─────────────────────
174
  if has_live_vllm:
175
- with gr.Tab("Live vLLM Endpoint"):
176
- gr.Markdown(
177
- "Calls the OpenAI-compatible vLLM endpoint. "
178
- "Set `IAC_SECFIX_BASE_URL`, `IAC_SECFIX_API_KEY`, and `IAC_SECFIX_MODEL` in Space secrets."
 
 
 
 
 
 
 
179
  )
180
  with gr.Row():
181
- live_code = gr.Code(label="Paste IaC file here", language="yaml",
182
- interactive=True, lines=22)
183
- live_type = gr.Dropdown(choices=["terraform", "kubernetes"],
184
- value="terraform", label="IaC Type", scale=0)
 
 
 
 
 
185
  live_btn = gr.Button("Run Agent Loop", variant="primary")
186
- live_status, live_before, live_after, live_details = _result_panels()
187
  with gr.Row():
188
- live_orig, live_fixed = _file_panels("Input File", "Fixed File")
189
  live_btn.click(
190
  fn=_live_outputs,
191
  inputs=[live_code, live_type],
192
- outputs=[live_orig, live_fixed, live_status, live_before, live_after, live_details],
 
193
  )
194
 
195
- gr.Markdown(
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> &nbsp;·&nbsp; 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 &nbsp;·&nbsp; 50 samples &nbsp;·&nbsp;
167
+ <b style="color:#24a148">32.6%</b> targeted resolution &nbsp;·&nbsp;
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