import streamlit as st import joblib import json import numpy as np import pandas as pd import shap import matplotlib import matplotlib.pyplot as plt matplotlib.use("Agg") from openai import OpenAI import os # Page Config st.set_page_config( page_title="IDS - XAI Dashboard", page_icon="πŸ›‘οΈ", layout="wide", initial_sidebar_state="expanded", ) # CSS st.markdown(""" """, unsafe_allow_html=True) # Load Assets @st.cache_resource def load_all(): model = joblib.load("models/LightGBM.pkl") le = joblib.load("models/label_encoder.pkl") try: explainer = joblib.load("models/shap_explainer.pkl") except FileNotFoundError: explainer = shap.TreeExplainer(model) with open("data/feature_columns.json") as f: features = json.load(f) return model, le, explainer, features model, le, explainer, features = load_all() # Helpers def severity(label): high = {"DoS Hulk","DDoS","DoS GoldenEye","DoS slowloris","DoS Slowhttptest","Heartbleed"} medium = {"Bot","PortScan","FTP-Patator","SSH-Patator","Web Attack \x96 Brute Force", "Web Attack \x96 Sql Injection","Web Attack \x96 XSS","Infiltration"} if label in high: return "HIGH", "#ef4444" if label in medium: return "MEDIUM", "#f59e0b" if label == "BENIGN": return "BENIGN", "#22c55e" return "LOW", "#a78bfa" def openai_explain(prediction, top_features, feature_values, confidence): api_key = os.environ.get("OPENAI_API_KEY", "") if not api_key: return None, "⚠️ OPENAI_API_KEY belum diset di environment / repo secret." feat_lines = "\n".join(f" - {f}" for f in top_features) val_lines = "\n".join( f" - {k}: {v:.4g}" for k, v in list(feature_values.items())[:15] if v != 0 ) prompt = f"""Kamu adalah sistem penjelasan otomatis untuk Intrusion Detection System berbasis machine learning. Hasil deteksi model LightGBM pada traffic jaringan: - Prediksi : {prediction} - Confidence : {confidence:.2%} Top 5 fitur SHAP yang paling berpengaruh: {feat_lines} Nilai fitur traffic (non-zero): {val_lines} Tulis penjelasan 3–4 kalimat dalam bahasa Indonesia yang profesional dan naratif (tanpa bullet points): 1. Mengapa model memutuskan "{prediction}" 2. Apa yang mencurigakan atau normal dari fitur-fitur tersebut β€” jelaskan maknanya secara konkret \ (misal: "Flow IAT Mean rendah berarti paket dikirim sangat cepat dan berdekatan") 3. Rekomendasi tindakan singkat yang perlu dilakukan jika ini adalah serangan Maksimal 150 kata.""" try: resp = OpenAI(api_key=api_key).chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "Kamu adalah analis keamanan jaringan senior."}, {"role": "user", "content": prompt} ], max_tokens=300, temperature=0.4, ) return resp.choices[0].message.content, None except Exception as e: return None, f"OpenAI error: {e}" def plot_shap_bar(shap_2d, feature_names, pred_class, top_n=10): vals = shap_2d[:, pred_class] pairs = sorted(zip(feature_names, vals), key=lambda x: abs(x[1]), reverse=True)[:top_n] names, values = [p[0] for p in pairs], [p[1] for p in pairs] colors = ["#ef4444" if v > 0 else "#22c55e" for v in values] fig, ax = plt.subplots(figsize=(7, 3.8)) fig.patch.set_facecolor("#141414") ax.set_facecolor("#141414") ax.barh(names[::-1], values[::-1], color=colors[::-1], height=0.55) ax.axvline(0, color="#2a2a2a", linewidth=1.2) ax.set_xlabel("SHAP value", color="#6b7280", fontsize=8) ax.tick_params(colors="#9ca3af", labelsize=7.5) for sp in ax.spines.values(): sp.set_edgecolor("#1f1f1f") ax.grid(axis="x", color="#1f1f1f", linewidth=0.6) from matplotlib.patches import Patch ax.legend( handles=[Patch(color="#ef4444", label="↑ mengarah ke attack"), Patch(color="#22c55e", label="↓ mengarah ke benign")], fontsize=7, facecolor="#1c1c1c", edgecolor="#2a2a2a", labelcolor="#9ca3af", loc="lower right" ) plt.tight_layout(pad=0.5) return fig def section_label(text, color="#f59e0b"): st.markdown( f'
{text}
', unsafe_allow_html=True ) def sub_label(text): st.markdown( f'
{text}
', unsafe_allow_html=True ) # Top SHAP features TOP_SHAP_FEATURES = [ "Flow IAT Mean", "Flow IAT Min", "Bwd Packets/s", "Fwd Header Length", "Init_Win_bytes_forward", ] CONTEXT_FEATURES = [ "Active Max", "Init_Win_bytes_bckward", "Destination Port", "Fwd IAT Std", "Flow Duration", "min_seg_size_forward", ] DISPLAY_FEATURES = TOP_SHAP_FEATURES + CONTEXT_FEATURES # PRESETS @st.cache_data def load_presets(): try: with open("data/preset_samples.json") as f: raw = json.load(f) return { "Custom": {}, "Normal Traffic": raw.get("BENIGN", {}), "DoS Hulk": raw.get("DoS Hulk", {}), "PortScan": raw.get("PortScan", {}), "Bot": raw.get("Bot", {}), } except FileNotFoundError: return {"Custom": {}} PRESETS = load_presets() PRESET_NAMES = list(PRESETS.keys()) def sk(feat): """Safe session_state key for a feature.""" return "inp_" + feat.replace(" ", "_").replace("/", "_per_").replace("-", "_").replace(".", "_") # SIDEBAR with st.sidebar: st.markdown("""
πŸ›‘οΈ IDS Β· XAI
INTRUSION DETECTION SYSTEM
""", unsafe_allow_html=True) st.markdown( '
⚑ QUICK PRESET
', unsafe_allow_html=True ) preset = st.radio("", PRESET_NAMES, label_visibility="collapsed") st.markdown("
", unsafe_allow_html=True) st.markdown("""
MODEL INFO
Algorithm  : LightGBM
Dataset    : CIC-IDS-2017
Balancing  : SMOTE
Classes    : 15
Accuracy   : 99.8%
Macro F1   : 0.905
Macro Prec  : 0.878
Macro Rec   : 0.953
XAI        : SHAP
LLM        : GPT-4o-mini
""", unsafe_allow_html=True) # Inject preset if "last_preset" not in st.session_state: st.session_state["last_preset"] = preset st.session_state["preset_full"] = PRESETS.get(preset, {}) if st.session_state["last_preset"] != preset: p = PRESETS.get(preset, {}) for feat in DISPLAY_FEATURES: st.session_state[sk(feat)] = float(p.get(feat, 0.0)) st.session_state["preset_full"] = p st.session_state["last_preset"] = preset # HEADER st.markdown("""

Intrusion Detection System

EXPLAINABLE AI DASHBOARD Β· CIC-IDS-2017 Β· LIGHTGBM + SHAP + GPT-4o-mini

LightGBM SHAP GPT-4o-mini

""", unsafe_allow_html=True) # BASIC INPUT section_label("πŸ“‘ NETWORK TRAFFIC FEATURES") sub_label("TOP SHAP FEATURES") shap_cols = st.columns(len(TOP_SHAP_FEATURES)) display_vals = {} for i, feat in enumerate(TOP_SHAP_FEATURES): with shap_cols[i]: display_vals[feat] = st.number_input( feat, step=0.01, format="%.2f", key=sk(feat) ) sub_label("TRAFFIC CONTEXT") ctx_cols = st.columns(len(CONTEXT_FEATURES)) for i, feat in enumerate(CONTEXT_FEATURES): with ctx_cols[i]: display_vals[feat] = st.number_input( feat, step=0.01, format="%.2f", key=sk(feat) ) # ADVANCED INPUT extra_features = [f for f in features if f not in DISPLAY_FEATURES] st.markdown("
", unsafe_allow_html=True) with st.expander("βš™οΈ Advanced Input", expanded=False): st.markdown( '
' 'Pilih fitur tambahan yang ingin kamu atur nilainya secara manual. ' 'Jika preset aktif, nilai fitur lain sudah terisi otomatis dari dataset.' '
', unsafe_allow_html=True ) selected_extra = st.multiselect( "Pilih fitur tambahan:", options=extra_features, default=[], placeholder="Cari nama fitur...", key="adv_selected" ) adv_override = {} if selected_extra: st.markdown("
", unsafe_allow_html=True) adv_cols = st.columns(3) for i, feat in enumerate(selected_extra): safe_key = ( "adv_" + feat.replace(" ", "_").replace("/", "_per_") .replace("-", "_").replace(".", "_") ) preset_val = float(st.session_state.get("preset_full", {}).get(feat, 0.0)) with adv_cols[i % 3]: adv_override[feat] = st.number_input( feat, value=preset_val, step=0.01, format="%.4f", key=safe_key ) else: st.markdown( '
' 'Belum ada fitur dipilih
', unsafe_allow_html=True ) # BUTTONS st.markdown("
", unsafe_allow_html=True) run = st.button("πŸ” Analyze Traffic", use_container_width=True) # BUILD FEATURE VECTOR def build_row(): preset_full = st.session_state.get("preset_full", {}) full = {f: float(preset_full.get(f, 0.0)) for f in features} full.update(display_vals) full.update(adv_override) row = pd.DataFrame([full])[features] display = {k: v for k, v in full.items() if v != 0.0} return row, display # RENDER RESULT def render_result(row, display_map, mode_label=""): pred_enc = model.predict(row)[0] proba = model.predict_proba(row)[0] conf = proba[pred_enc] label = le.inverse_transform([pred_enc])[0] sev, sev_color = severity(label) is_attack = label != "BENIGN" st.markdown( "
", unsafe_allow_html=True ) badge = ( f'' f'{mode_label}' ) if mode_label else "" section_label(f"πŸ”¬ ANALYSIS RESULT {badge}") col_pred, col_shap = st.columns([1, 2], gap="large") # Left column: classification card + probabilities with col_pred: icon = "🚨" if is_attack else "βœ…" bg = "rgba(239,68,68,0.07)" if is_attack else "rgba(34,197,94,0.07)" bord = "#ef4444" if is_attack else "#22c55e" st.markdown(f"""
CLASSIFICATION
{icon} {label}
confidence: {conf:.2%}
⚑ {sev}
""", unsafe_allow_html=True) sub_label("CLASS PROBABILITIES") prob_df = ( pd.DataFrame({"Class": le.classes_, "Prob": proba}) .sort_values("Prob", ascending=False) .head(5) ) fig2, ax2 = plt.subplots(figsize=(4, 2.5)) fig2.patch.set_facecolor("#141414") ax2.set_facecolor("#141414") bar_c = ["#f59e0b" if c == label else "#242424" for c in prob_df["Class"]] ax2.barh(prob_df["Class"][::-1], prob_df["Prob"][::-1], color=bar_c[::-1], height=0.5) ax2.tick_params(colors="#9ca3af", labelsize=7) for sp in ax2.spines.values(): sp.set_edgecolor("#1f1f1f") ax2.grid(axis="x", color="#1f1f1f", linewidth=0.5) ax2.set_xlim(0, 1) fig2.tight_layout(pad=0.4) st.pyplot(fig2, use_container_width=True) plt.close(fig2) # Right column: SHAP bar + top features with col_shap: sub_label("SHAP FEATURE IMPORTANCE") with st.spinner("Computing SHAP values…"): sv = explainer.shap_values(row) if isinstance(sv, np.ndarray) and sv.ndim == 3: shap_2d = sv[0] elif isinstance(sv, list): shap_2d = np.array([s[0] for s in sv]).T else: shap_2d = sv fig_shap = plot_shap_bar(shap_2d, features, pred_enc) st.pyplot(fig_shap, use_container_width=True) plt.close(fig_shap) vals = shap_2d[:, pred_enc] pairs = sorted(zip(features, vals), key=lambda x: abs(x[1]), reverse=True)[:5] top_features_text = [ f"{f} ({'increase' if v > 0 else 'decrease'}, impact={v:.3f})" for f, v in pairs ] sub_label("TOP CONTRIBUTING FEATURES") for tf in top_features_text: dot = "πŸ”΄" if "increase" in tf else "🟒" st.markdown( f'
{dot} {tf}
', unsafe_allow_html=True ) # LLM explanation st.markdown("
", unsafe_allow_html=True) section_label("πŸ€– LLM NATURAL LANGUAGE EXPLANATION") with st.spinner("Generating explanation via GPT-4o-mini…"): explanation, err = openai_explain(label, top_features_text, display_map, conf) if err: st.warning(err) else: st.markdown(f"""
{explanation}
""", unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════════════════════ # TRIGGER # ═══════════════════════════════════════════════════════════════════════════════ if run: row, dmap = build_row() is_adv = bool(adv_override) render_result(row, dmap, mode_label="ADVANCED" if is_adv else "")