selcuksntrk's picture
Add IaC-SecFix app with Gradio UI and agent backend
a17bead verified
Raw
History Blame Contribute Delete
28.3 kB
from __future__ import annotations
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import gradio as gr
from .demo_data import load_demos
from .hf_inference import LORA_MODEL_ID, build_hf_runtime_config, has_hf_config
from .rendering import error_html, render_findings_html, render_result
_N = 6
CARBON_CSS = """
@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');
/* ── Global reset ──────────────────────────────────────────────────── */
.gradio-container, .gradio-container * {
font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif !important;
border-radius: 0 !important;
box-sizing: border-box;
}
.gradio-container {
background: #161616 !important;
padding: 0 !important;
max-width: 100% !important;
}
body { background: #161616 !important; }
/* ── Base text ─────────────────────────────────────────────────────── */
.gradio-container p,
.gradio-container span,
.gradio-container div,
.gradio-container label,
.gradio-container li {
color: #c6c6c6;
}
/* ── Code editor (CodeMirror) ──────────────────────────────────────── */
.cm-editor { background: #262626 !important; }
.cm-gutters {
background: #1e1e1e !important;
border-right: 1px solid #393939 !important;
color: #6f6f6f !important;
}
.cm-content, .cm-line, .cm-scroller {
font-family: 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace !important;
font-size: 13px !important;
line-height: 1.6 !important;
color: #f4f4f4 !important;
background: #262626 !important;
caret-color: #4589ff !important;
}
.cm-activeLine { background: #2d2d2d !important; }
.cm-selectionBackground { background: #001d6c !important; }
.codemirror-wrapper, .codemirror-wrapper > div {
background: #262626 !important;
border: 1px solid #393939 !important;
}
/* ── Primary button ────────────────────────────────────────────────── */
button.primary {
background: #0f62fe !important;
color: #ffffff !important;
border: none !important;
font-size: 14px !important;
font-weight: 400 !important;
letter-spacing: 0.16px !important;
height: 48px !important;
transition: background 70ms ease !important;
}
button.primary:hover { background: #0050e6 !important; }
button.primary:active { background: #002d9c !important; }
/* ── Secondary button ──────────────────────────────────────────────── */
button.secondary {
background: #262626 !important;
color: #78a9ff !important;
border: 1px solid #4589ff !important;
}
button.secondary:hover { background: #353535 !important; }
/* ── Tabs ──────────────────────────────────────────────────────────── */
div[role="tablist"] {
border-bottom: 1px solid #393939 !important;
background: #262626 !important;
padding: 0 16px !important;
gap: 0 !important;
}
button[role="tab"] {
font-size: 14px !important;
letter-spacing: 0.16px !important;
color: #8d8d8d !important;
background: transparent !important;
border: none !important;
border-bottom: 3px solid transparent !important;
padding: 12px 16px !important;
font-weight: 400 !important;
margin-bottom: -1px !important;
transition: color 70ms ease !important;
}
button[role="tab"]:hover { color: #c6c6c6 !important; }
button[role="tab"][aria-selected="true"] {
color: #f4f4f4 !important;
border-bottom: 3px solid #4589ff !important;
font-weight: 600 !important;
}
/* ── Labels ────────────────────────────────────────────────────────── */
label > span:first-child, .label-wrap > span {
font-size: 11px !important;
font-weight: 600 !important;
letter-spacing: 0.32px !important;
text-transform: uppercase !important;
color: #8d8d8d !important;
}
/* ── Inputs / selects ──────────────────────────────────────────────── */
input, textarea, select {
background: #262626 !important;
color: #f4f4f4 !important;
border: 1px solid #525252 !important;
}
input:focus, textarea:focus, select:focus {
border-color: #4589ff !important;
outline: none !important;
}
select option { background: #262626 !important; color: #f4f4f4 !important; }
/* ── Dropdown (Gradio) ─────────────────────────────────────────────── */
.dropdown, .multiselect, [data-testid="dropdown"] {
background: #262626 !important;
border: 1px solid #525252 !important;
color: #f4f4f4 !important;
}
.dropdown ul, .multiselect ul, [data-testid="dropdown"] ul {
background: #262626 !important;
border: 1px solid #525252 !important;
}
.dropdown li, .multiselect li { color: #c6c6c6 !important; }
.dropdown li:hover, .multiselect li:hover { background: #353535 !important; }
/* ── Markdown headings ─────────────────────────────────────────────── */
.prose h3 {
font-size: 11px !important;
font-weight: 600 !important;
letter-spacing: 0.32px !important;
text-transform: uppercase !important;
color: #8d8d8d !important;
border-bottom: 1px solid #393939 !important;
padding-bottom: 8px !important;
margin: 20px 0 10px !important;
}
.prose code {
font-family: 'IBM Plex Mono', monospace !important;
background: #353535 !important;
border: 1px solid #525252 !important;
font-size: 0.85em !important;
padding: 1px 5px !important;
color: #78a9ff !important;
}
/* ── Container blocks ──────────────────────────────────────────────── */
.block {
background: #262626 !important;
border-color: #393939 !important;
}
.tabitem {
background: #1e1e1e !important;
padding: 20px !important;
}
/* ── Scrollbars ────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #161616; }
::-webkit-scrollbar-thumb { background: #525252; }
::-webkit-scrollbar-thumb:hover { background: #6f6f6f; }
"""
_F = "IBM Plex Sans,system-ui,sans-serif"
_M = "IBM Plex Mono,SFMono-Regular,Consolas,monospace"
_HEADER_HTML = """
<div style="background:#161616;padding:28px 32px 24px;margin:-8px -8px 0;border-bottom:1px solid #393939">
<p style="font-size:11px;font-weight:600;letter-spacing:0.32px;text-transform:uppercase;
color:#6f6f6f;margin:0 0 6px;font-family:'IBM Plex Sans',sans-serif">
</p>
<h1 style="font-size:2rem;font-weight:300;letter-spacing:-0.64px;color:#ffffff;
margin:0 0 8px;font-family:'IBM Plex Sans',sans-serif">
IaC-SecFix
</h1>
<p style="font-size:14px;color:#a8a8a8;margin:0 0 20px;font-family:'IBM Plex Sans',sans-serif;max-width:640px">
Paste a Terraform or Kubernetes file. The agent pipeline scans for security misconfigurations,
generates a verified patch, and reports exactly which rules were fixed β€” powered by a fine-tuned
Qwen3.5-9B LoRA model trained on AMD MI300X.
</p>
<div style="display:flex;flex-wrap:wrap;gap:0;align-items:stretch">
<div style="display:flex;align-items:center;gap:6px;background:#262626;
padding:8px 12px;border-left:3px solid #4589ff">
<span style="font-size:11px;font-weight:600;color:#8d8d8d;letter-spacing:0.32px;
text-transform:uppercase;font-family:'IBM Plex Sans',sans-serif">Pipeline</span>
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;background:#1e1e1e;
padding:8px 16px;gap:6px;border-left:1px solid #393939">
<span style="font-size:11px;color:#78a9ff;font-family:'IBM Plex Mono',monospace;
background:#001d6c;padding:2px 8px">1 Β· Ingest</span>
<span style="color:#525252;font-size:11px">β†’</span>
<span style="font-size:11px;color:#78a9ff;font-family:'IBM Plex Mono',monospace;
background:#001d6c;padding:2px 8px">2 Β· Policy Context</span>
<span style="color:#525252;font-size:11px">β†’</span>
<span style="font-size:11px;color:#78a9ff;font-family:'IBM Plex Mono',monospace;
background:#001d6c;padding:2px 8px">3 Β· Patch</span>
<span style="color:#525252;font-size:11px">β†’</span>
<span style="font-size:11px;color:#78a9ff;font-family:'IBM Plex Mono',monospace;
background:#001d6c;padding:2px 8px">4 Β· Validate</span>
<span style="color:#525252;font-size:11px">β†’</span>
<span style="font-size:11px;color:#78a9ff;font-family:'IBM Plex Mono',monospace;
background:#001d6c;padding:2px 8px">5 Β· Safety</span>
<span style="color:#525252;font-size:11px">β†’</span>
<span style="font-size:11px;color:#42be65;font-family:'IBM Plex Mono',monospace;
background:#071908;padding:2px 8px">6 Β· Human Gate</span>
</div>
</div>
</div>
"""
_HOW_IT_WORKS_HTML = """
<div style="background:#262626;border-left:3px solid #4589ff;padding:16px 20px;margin-bottom:20px">
<p style="font-size:11px;font-weight:600;letter-spacing:0.32px;text-transform:uppercase;
color:#4589ff;margin:0 0 12px;font-family:'IBM Plex Sans',sans-serif">How it works</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px">
<div>
<p style="font-size:12px;font-weight:600;color:#f4f4f4;margin:0 0 4px;
font-family:'IBM Plex Sans',sans-serif">β‘  Scan</p>
<p style="font-size:12px;color:#8d8d8d;margin:0;line-height:1.5;
font-family:'IBM Plex Sans',sans-serif">
Checkov runs 500+ rules against your file and records every failed check with its severity.
</p>
</div>
<div>
<p style="font-size:12px;font-weight:600;color:#f4f4f4;margin:0 0 4px;
font-family:'IBM Plex Sans',sans-serif">β‘‘ Contextualize</p>
<p style="font-size:12px;color:#8d8d8d;margin:0;line-height:1.5;
font-family:'IBM Plex Sans',sans-serif">
A policy agent retrieves the CIS/NIST remediation guidance most relevant to each finding.
</p>
</div>
<div>
<p style="font-size:12px;font-weight:600;color:#f4f4f4;margin:0 0 4px;
font-family:'IBM Plex Sans',sans-serif">β‘’ Patch</p>
<p style="font-size:12px;color:#8d8d8d;margin:0;line-height:1.5;
font-family:'IBM Plex Sans',sans-serif">
The fine-tuned Qwen3.5-9B LoRA model rewrites only the unsafe blocks, leaving the rest untouched.
</p>
</div>
<div>
<p style="font-size:12px;font-weight:600;color:#f4f4f4;margin:0 0 4px;
font-family:'IBM Plex Sans',sans-serif">β‘£ Validate</p>
<p style="font-size:12px;color:#8d8d8d;margin:0;line-height:1.5;
font-family:'IBM Plex Sans',sans-serif">
Checkov runs again on the patched file. Only checks that were originally failing are tracked.
</p>
</div>
<div>
<p style="font-size:12px;font-weight:600;color:#f4f4f4;margin:0 0 4px;
font-family:'IBM Plex Sans',sans-serif">β‘€ Safety gate</p>
<p style="font-size:12px;color:#8d8d8d;margin:0;line-height:1.5;
font-family:'IBM Plex Sans',sans-serif">
A safety agent checks the patch introduces no destructive side-effects before approving.
</p>
</div>
<div>
<p style="font-size:12px;font-weight:600;color:#f4f4f4;margin:0 0 4px;
font-family:'IBM Plex Sans',sans-serif">β‘₯ Result</p>
<p style="font-size:12px;color:#8d8d8d;margin:0;line-height:1.5;
font-family:'IBM Plex Sans',sans-serif">
You see the decision (Approved / Needs Review / Rejected), the diff, and the resolved rules.
</p>
</div>
</div>
</div>
"""
_READING_GUIDE_HTML = """
<div style="background:#1e1e1e;border:1px solid #393939;padding:14px 20px;margin-bottom:20px">
<p style="font-size:11px;font-weight:600;letter-spacing:0.32px;text-transform:uppercase;
color:#8d8d8d;margin:0 0 10px;font-family:'IBM Plex Sans',sans-serif">Reading the results</p>
<div style="display:flex;flex-wrap:wrap;gap:16px">
<span style="font-size:12px;color:#8d8d8d;font-family:'IBM Plex Sans',sans-serif">
<b style="color:#42be65">Approved</b> β€” patch verified; all targeted issues resolved, no unsafe changes.
</span>
<span style="font-size:12px;color:#8d8d8d;font-family:'IBM Plex Sans',sans-serif">
<b style="color:#f1c21b">Needs Review</b> β€” patch improves security but at least one issue remains; human sign-off required.
</span>
<span style="font-size:12px;color:#8d8d8d;font-family:'IBM Plex Sans',sans-serif">
<b style="color:#ff8389">Rejected</b> β€” patch made things worse or introduced a safety risk.
</span>
<span style="font-size:12px;color:#8d8d8d;font-family:'IBM Plex Sans',sans-serif">
<b style="color:#c6c6c6">Findings Before / After</b> β€” count and severity of failed checks before and after the patch.
</span>
<span style="font-size:12px;color:#8d8d8d;font-family:'IBM Plex Sans',sans-serif">
<b style="color:#c6c6c6">Analysis Details</b> β€” which rules were resolved, which remain, and how many lines changed.
</span>
</div>
</div>
"""
_FOOTER_HTML = """
<div style="background:#262626;border-top:1px solid #393939;padding:16px 32px;
display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
<div style="display:flex;gap:24px;flex-wrap:wrap">
<span style="font-size:12px;color:#8d8d8d;font-family:'IBM Plex Sans',sans-serif">
<b style="color:#f4f4f4">IaC-SecFix</b> &nbsp;Β·&nbsp; Developed by Selcuk Senturk
</span>
</div>
</div>
"""
def _empty() -> tuple:
return ("",) * _N
def _demo_outputs(label: str | None, demos: dict) -> tuple:
if not label or label not in demos:
return _empty()
return render_result(demos[label])
def _demo_prefill(label: str | None, demos: dict) -> str:
"""Return just the original file content for pre-filling the editor."""
if not label or label not in demos:
return ""
return demos[label].get("original_file", "")
def _lora_outputs(file_content: str, iac_type: str) -> tuple:
if not file_content.strip():
return _empty()
try:
from iac_secfix_agents import IngestRequest, RemediationOrchestrator
config = build_hf_runtime_config()
decision = RemediationOrchestrator(config=config).remediate(
IngestRequest(
file_name="input.tf" if iac_type == "terraform" else "input.yaml",
file_content=file_content,
iac_type=iac_type, # type: ignore[arg-type]
)
)
val = decision.validation
data = {
"original_file": file_content,
"fixed_file": decision.patch.fixed_file if decision.patch else "",
"decision": decision.model_dump(mode="json"),
"pre_rule_ids": [],
"post_rule_ids": val.targeted_unresolved if val else [],
"pre_summary": val.pre_summary if val else {},
"post_summary": val.post_summary if val else {},
}
return render_result(data)
except Exception as exc:
return (file_content, "", error_html("Agent error", exc), "", "", "")
def _live_outputs(file_content: str, iac_type: str) -> tuple:
if not file_content.strip():
return _empty()
try:
from iac_secfix_agents import IngestRequest, RemediationOrchestrator
decision = RemediationOrchestrator().remediate(
IngestRequest(
file_name="input.tf" if iac_type == "terraform" else "input.yaml",
file_content=file_content,
iac_type=iac_type, # type: ignore[arg-type]
)
)
val = decision.validation
data = {
"original_file": file_content,
"fixed_file": decision.patch.fixed_file if decision.patch else "",
"decision": decision.model_dump(mode="json"),
"pre_rule_ids": [],
"post_rule_ids": val.targeted_unresolved if val else [],
"pre_summary": val.pre_summary if val else {},
"post_summary": val.post_summary if val else {},
}
return render_result(data)
except Exception as exc:
return (file_content, "", error_html("Error", exc), "", "", "")
def _result_section() -> tuple:
status = gr.HTML()
with gr.Row():
with gr.Column():
gr.HTML(
"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
"text-transform:uppercase;color:#8d8d8d;margin:0 0 6px;"
f"font-family:{_F}'>Findings before patch</p>"
"<p style='font-size:12px;color:#6f6f6f;margin:0 0 8px;"
f"font-family:{_F}'>Security checks that failed on the original file.</p>"
)
before = gr.HTML()
with gr.Column():
gr.HTML(
"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
"text-transform:uppercase;color:#8d8d8d;margin:0 0 6px;"
f"font-family:{_F}'>Findings after patch</p>"
"<p style='font-size:12px;color:#6f6f6f;margin:0 0 8px;"
f"font-family:{_F}'>Remaining failures after the agent applied its fix.</p>"
)
after = gr.HTML()
gr.HTML(
"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
"text-transform:uppercase;color:#8d8d8d;margin:16px 0 6px;"
f"font-family:{_F}'>Analysis details</p>"
"<p style='font-size:12px;color:#6f6f6f;margin:0 0 8px;"
f"font-family:{_F}'>"
"Resolved / unresolved rules, parse status, and how much of the file changed.</p>"
)
details = gr.HTML()
return status, before, after, details
def build_demo() -> gr.Blocks:
demos = load_demos()
choices = list(demos.keys())
has_lora = has_hf_config()
has_live_vllm = bool(os.getenv("IAC_SECFIX_BASE_URL"))
with gr.Blocks(title="IaC-SecFix", css=CARBON_CSS, theme=gr.themes.Base()) as app:
gr.HTML(_HEADER_HTML)
with gr.Tabs():
# ── Main tab: demo picker + live LoRA (merged) ────────────────
with gr.Tab("Try It"):
gr.HTML(_HOW_IT_WORKS_HTML)
# ── Input row ─────────────────────────────────────────────
gr.HTML(
f"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
f"text-transform:uppercase;color:#8d8d8d;margin:0 0 4px;font-family:{_F}'>"
f"Step 1 β€” choose your input</p>"
f"<p style='font-size:13px;color:#a8a8a8;margin:0 0 12px;font-family:{_F}'>"
f"Pick a prebuilt example to load it into the editor, or paste your own "
f"Terraform&nbsp;/&nbsp;Kubernetes file directly.</p>"
)
with gr.Row():
demo_dd = gr.Dropdown(
choices=choices,
value=choices[0] if choices else None,
label="Prebuilt example",
scale=4,
info="Selecting an example pre-fills the editor below.",
)
iac_type = gr.Dropdown(
choices=["terraform", "kubernetes"],
value="terraform",
label="IaC type",
scale=1,
info="Match this to the file you're analysing.",
)
iac_code = gr.Code(
label="IaC file (editable β€” paste your own or use the example above)",
language="yaml",
interactive=True,
lines=22,
)
# ── Action row ────────────────────────────────────────────
gr.HTML(
f"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
f"text-transform:uppercase;color:#8d8d8d;margin:16px 0 4px;font-family:{_F}'>"
f"Step 2 β€” analyse</p>"
)
with gr.Row():
demo_btn = gr.Button(
"Load precomputed result",
variant="secondary",
scale=1,
)
if has_lora:
run_btn = gr.Button(
"Run fine-tuned model",
variant="primary",
scale=1,
)
if has_lora:
gr.HTML(
f"<p style='font-size:12px;color:#6f6f6f;margin:6px 0 0;font-family:{_F}'>"
f"<b style='color:#a8a8a8'>Load precomputed result</b> β€” instant, no GPU. "
f"Shows the benchmark trace for the selected example.<br>"
f"<b style='color:#a8a8a8'>Run fine-tuned model</b> β€” calls the live "
f"<a href='https://huggingface.co/{LORA_MODEL_ID}' "
f"style='color:#78a9ff;text-decoration:none'>{LORA_MODEL_ID}</a> "
f"endpoint (Qwen3.5-9B, r=8 LoRA). Works on any file you paste.</p>"
)
else:
gr.HTML(
f"<p style='font-size:12px;color:#6f6f6f;margin:6px 0 0;font-family:{_F}'>"
f"<b style='color:#a8a8a8'>Load precomputed result</b> β€” shows the "
f"benchmark trace for the selected example. "
f"To enable live inference, set <code style='color:#78a9ff;font-family:{_M}'>"
f"HF_TOKEN</code> or <code style='color:#78a9ff;font-family:{_M}'>"
f"IAC_SECFIX_HF_ENDPOINT</code> in Space secrets.</p>"
)
# ── Results ───────────────────────────────────────────────
gr.HTML(
f"<p style='font-size:11px;font-weight:600;letter-spacing:0.32px;"
f"text-transform:uppercase;color:#8d8d8d;margin:20px 0 4px;font-family:{_F}'>"
f"Step 3 β€” review results</p>"
)
gr.HTML(_READING_GUIDE_HTML)
status, before, after, details = _result_section()
with gr.Row():
orig_out = gr.Code(
label="Original file",
language="yaml",
interactive=False,
lines=20,
)
fixed_out = gr.Code(
label="Fixed file (agent output)",
language="yaml",
interactive=False,
lines=20,
)
outs = [orig_out, fixed_out, status, before, after, details]
# selecting a demo pre-fills the editor
demo_dd.change(
fn=lambda l: _demo_prefill(l, demos),
inputs=[demo_dd],
outputs=[iac_code],
)
# load precomputed result (uses demo key, not editor content)
demo_btn.click(
fn=lambda l: _demo_outputs(l, demos),
inputs=[demo_dd],
outputs=outs,
)
if has_lora:
run_btn.click(
fn=_lora_outputs,
inputs=[iac_code, iac_type],
outputs=outs,
)
# auto-load first demo on page open
app.load(
fn=lambda: (
_demo_prefill(choices[0] if choices else None, demos),
*_demo_outputs(choices[0] if choices else None, demos),
),
outputs=[iac_code] + outs,
)
# ── Live vLLM endpoint (optional, advanced) ───────────────────
if has_live_vllm:
with gr.Tab("Live Endpoint"):
gr.HTML(
f"<div style='background:#262626;border-left:3px solid #f1c21b;"
f"padding:12px 16px;margin-bottom:16px'>"
f"<p style='font-size:12px;font-weight:600;color:#f1c21b;margin:0 0 4px;"
f"font-family:{_F}'>Advanced β€” custom vLLM endpoint</p>"
f"<p style='font-size:13px;color:#a8a8a8;margin:0;font-family:{_F}'>"
f"This tab calls a self-hosted OpenAI-compatible vLLM server directly. "
f"Configure the endpoint by setting "
f"<code style='color:#78a9ff;font-family:{_M}'>IAC_SECFIX_BASE_URL</code>, "
f"<code style='color:#78a9ff;font-family:{_M}'>IAC_SECFIX_API_KEY</code>, and "
f"<code style='color:#78a9ff;font-family:{_M}'>IAC_SECFIX_MODEL</code> "
f"in the Space secrets panel.</p></div>"
)
with gr.Row():
live_code = gr.Code(
label="IaC file",
language="yaml",
interactive=True,
lines=22,
)
live_type = gr.Dropdown(
choices=["terraform", "kubernetes"],
value="terraform",
label="IaC type",
scale=0,
)
live_btn = gr.Button("Run agent loop", variant="primary")
live_status, live_before, live_after, live_details = _result_section()
with gr.Row():
live_orig = gr.Code(label="Original file", language="yaml",
interactive=False, lines=20)
live_fixed = gr.Code(label="Fixed file", language="yaml",
interactive=False, lines=20)
live_btn.click(
fn=_live_outputs,
inputs=[live_code, live_type],
outputs=[live_orig, live_fixed, live_status,
live_before, live_after, live_details],
)
gr.HTML(_FOOTER_HTML)
return app