import streamlit as st
import joblib, numpy as np, pandas as pd, re, os, datetime, matplotlib, base64, io
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
from collections import Counter
matplotlib.use("Agg")
st.set_page_config(page_title="Firewall Log Classifier", layout="wide", initial_sidebar_state="collapsed")
MODEL_DIR = os.path.dirname(os.path.abspath(__file__))
@st.cache_resource
def load_artifacts():
m=joblib.load(os.path.join(MODEL_DIR,"model.joblib"))
s=joblib.load(os.path.join(MODEL_DIR,"scaler.joblib"))
l=joblib.load(os.path.join(MODEL_DIR,"label_encoder.joblib"))
return m,s,l
model,scaler,le=load_artifacts()
AC={"allow":"#7ec8e3","deny":"#f4a261","drop":"#f9e07f","reset-both":"#b39ddb"}
for k,v in [("page","home"),("chat_history",[]),("last_result",None),("uploaded_results",[]),("file_name","")]:
if k not in st.session_state: st.session_state[k]=v
def parse_log_line(line):
nums=[int(n) for n in re.findall(r"\b(\d+)\b",line)]; nums+=[0]*(11-len(nums)); return nums[:11]
def predict(features):
a=np.array(features,dtype=float).reshape(1,-1); sc=scaler.transform(a)
lbl=model.predict(sc)[0]; pr=model.predict_proba(sc)[0]
act=le.inverse_transform([lbl])[0]; conf=float(np.max(pr))
pd_={le.inverse_transform([i])[0]:round(float(p)*100,1) for i,p in enumerate(pr)}
return act,conf,pd_
def bc(a): return AC.get(a,"#888")
def add_hist(log,action,conf):
st.session_state.chat_history.append({"time":datetime.datetime.now().strftime("%H:%M:%S"),"log":log[:80]+("..." if len(log)>80 else ""),"action":action,"confidence":conf})
def fig_to_b64(fig):
buf=io.BytesIO(); fig.savefig(buf,format="png",dpi=120,bbox_inches="tight",transparent=True); buf.seek(0)
return base64.b64encode(buf.read()).decode()
def lfig(w=2.5,h=1.5):
fig,ax=plt.subplots(figsize=(w,h))
fig.patch.set_alpha(0); ax.set_facecolor((0.94,0.97,1.0,0.5))
ax.tick_params(colors="#4a7a9a",labelsize=5); ax.xaxis.label.set_color("#4a7a9a"); ax.yaxis.label.set_color("#4a7a9a")
for sp in ax.spines.values(): sp.set_edgecolor("#b0d8f0"); sp.set_linewidth(0.6)
return fig,ax
# ── CSS ───────────────────────────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ── HOME ──────────────────────────────────────────────────────────────────────
if st.session_state.page == "home":
st.markdown("
", unsafe_allow_html=True)
_, cx, _ = st.columns([1, 2.4, 1])
with cx:
st.markdown("""
Hi, I am Log Classifier
How can I help you today?
""", unsafe_allow_html=True)
log_input = st.text_area("log", placeholder="Ask me anything...", height=160,
key="log_text_input", label_visibility="collapsed")
uploaded_file = st.file_uploader("u", type=["txt","csv","log"],
label_visibility="collapsed", key="home_uploader")
if uploaded_file:
content=uploaded_file.read().decode("utf-8",errors="ignore")
lines=[l.strip() for l in content.splitlines() if l.strip()]; results=[]
for line in lines:
try:
feats=parse_log_line(line); act,conf,probs=predict(feats)
results.append({"log":line[:60],"action":act,"confidence":round(conf*100,1),"probabilities":probs})
except: pass
st.session_state.update({"uploaded_results":results,"file_name":uploaded_file.name,"page":"dashboard"}); st.rerun()
# All 5 buttons in one row, spanning the full width of the input box
c1, c2, c3, c4, c5 = st.columns(5)
with c1: new_chat_btn = st.button("New Chat", key="btn_new_chat")
with c2: history_btn = st.button("Chat History", key="btn_chat_history")
with c3: analyze_btn = st.button("Analyze", key="btn_analyze")
with c4: upload_btn = st.button("Upload", key="btn_upload")
with c5: dashboard_btn = st.button("Dashboard", key="btn_dashboard")
if new_chat_btn:
st.session_state.update({"chat_history":[],"last_result":None,"uploaded_results":[],"file_name":"","page":"home"}); st.rerun()
if history_btn:
st.session_state.page = "history"; st.rerun()
if analyze_btn:
if log_input.strip():
try:
feats=parse_log_line(log_input); act,conf,probs=predict(feats)
st.session_state.last_result={"log":log_input,"action":act,"confidence":round(conf*100,1),"probabilities":probs,"features":feats}
add_hist(log_input,act,round(conf*100,1)); st.rerun()
except Exception as e: st.error(f"Prediction error: {e}")
else: st.warning("Please paste a log entry before analyzing.")
if upload_btn: st.info("Drag and drop a file above.")
if dashboard_btn: st.session_state.page="dashboard"; st.rerun()
if st.session_state.last_result:
r=st.session_state.last_result; color=bc(r["action"])
pb="".join([f'{k} {v}%' for k,v in r["probabilities"].items()])
st.markdown(f'''
Prediction Result
{r["action"].upper()}
{r["confidence"]}% confidence
{pb}
''', unsafe_allow_html=True)
# ── HISTORY ───────────────────────────────────────────────────────────────────
elif st.session_state.page == "history":
st.markdown("", unsafe_allow_html=True)
_, hcol, _ = st.columns([1, 2.4, 1])
with hcol:
st.markdown('', unsafe_allow_html=True)
if st.button("← Back to Home", key="hist_back"): st.session_state.page="home"; st.rerun()
st.markdown('
', unsafe_allow_html=True)
st.markdown('Chat History
', unsafe_allow_html=True)
if not st.session_state.chat_history:
st.markdown('No history yet in this session.
', unsafe_allow_html=True)
else:
for item in reversed(st.session_state.chat_history):
color=bc(item["action"])
st.markdown(f'{item["time"]}
{item["log"]}
{item["action"].upper()}{item["confidence"]}% confidence ', unsafe_allow_html=True)
# ── DASHBOARD ─────────────────────────────────────────────────────────────────
elif st.session_state.page == "dashboard":
hd1,hd2=st.columns([6,1])
with hd1: st.markdown('Dashboard Firewall Log Classification
', unsafe_allow_html=True)
with hd2:
if st.button("← Home", key="dash_back"): st.session_state.page="home"; st.rerun()
all_results=[]
if st.session_state.last_result: all_results.append(st.session_state.last_result)
all_results+=list(st.session_state.uploaded_results)
if not all_results:
st.info("No predictions yet.")
else:
actions=[r["action"] for r in all_results]; count=Counter(actions); labels=["allow","deny","drop","reset-both"]
total=len(all_results)
allow_n=count.get("allow",0); block_n=count.get("deny",0)+count.get("drop",0); reset_n=count.get("reset-both",0)
allow_pct=round(allow_n/total*100) if total else 0
block_pct=round(block_n/total*100) if total else 0
# ── Row 1: metric cards ──
card_style="background:rgba(255,255,255,0.60);backdrop-filter:blur(8px);border-radius:12px;padding:7px 12px;box-shadow:0 3px 14px rgba(80,160,220,0.08),inset 0 1px 0 rgba(255,255,255,0.9);"
lbl_style="font-size:0.50rem;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#7ab0d0;margin-bottom:2px;"
sub_style="font-size:0.55rem;color:#90b8d4;margin-top:2px;"
m1,m2,m3,m4=st.columns(4)
m1.markdown(f'Total
{total}
entries classified
',unsafe_allow_html=True)
m2.markdown(f'Allow
{allow_n}
{allow_pct}% of traffic
',unsafe_allow_html=True)
m3.markdown(f'Blocked
{block_n}
{block_pct}% of traffic
',unsafe_allow_html=True)
m4.markdown(f'Reset-Both
{reset_n}
terminated
',unsafe_allow_html=True)
# ── Row 2: charts ──
ch1, ch2 = st.columns(2)
with ch1:
st.markdown('Action Distribution
', unsafe_allow_html=True)
fig_p,ax_p=plt.subplots(figsize=(2.8,1.7))
fig_p.patch.set_alpha(0); ax_p.set_facecolor((0,0,0,0))
non_zero=[(count.get(l,0),bc(l),l) for l in labels if count.get(l,0)>0]
if non_zero:
s_,c_,l_=zip(*non_zero)
wedges,_,at=ax_p.pie(s_,labels=None,colors=c_,autopct="%1.0f%%",startangle=90,
pctdistance=0.68,wedgeprops={"linewidth":2,"edgecolor":"white","width":0.48})
for t in at: t.set_fontsize(6.5); t.set_color("white"); t.set_fontweight("bold")
for w in wedges: w.set_alpha(0.90)
ax_p.text(0,0,f"{total}\nlogs",ha="center",va="center",fontsize=7.5,color="#1a4a72",fontweight="bold",linespacing=1.3)
all_patches=[mpatches.Patch(color=bc(l),alpha=0.9,label=f"{l.upper()} {count.get(l,0)}") for l in labels]
ax_p.legend(handles=all_patches,loc="lower center",bbox_to_anchor=(0.5,-0.06),ncol=2,fontsize=5.5,framealpha=0,labelcolor="#2a5a82")
fig_p.tight_layout(pad=0.1)
st.pyplot(fig_p, use_container_width=False); plt.close(fig_p)
st.markdown('
', unsafe_allow_html=True)
with ch2:
st.markdown('Confidence per Entry — 80% threshold
', unsafe_allow_html=True)
fig_b,ax_b=plt.subplots(figsize=(3.4,1.7))
fig_b.patch.set_alpha(0); ax_b.set_facecolor((0.95,0.97,1.0,0.5))
confs=[r["confidence"] for r in all_results[-12:]]; bar_c=[bc(r["action"]) for r in all_results[-12:]]
x_pos=list(range(len(confs)))
bars=ax_b.bar(x_pos,confs,color=bar_c,edgecolor="white",linewidth=0.8,width=0.62,alpha=0.88)
ax_b.axhline(y=80,color="#4ab0e8",linewidth=0.8,linestyle="--",alpha=0.8)
ax_b.set_ylim(0,120); ax_b.set_yticks([0,50,100]); ax_b.set_yticklabels(["0%","50%","100%"],fontsize=5.5)
ax_b.set_xticks(x_pos); ax_b.set_xticklabels([str(i+1) for i in x_pos],fontsize=5.5)
ax_b.tick_params(colors="#4a7a9a",length=2); ax_b.grid(axis="y",color="#d0eaf8",linewidth=0.5,alpha=0.9)
for sp in ax_b.spines.values(): sp.set_edgecolor("#c8e0f0"); sp.set_linewidth(0.5)
for b,v in zip(bars,confs): ax_b.text(b.get_x()+b.get_width()/2,v+1,f"{v:.0f}%",ha="center",color="#2a5a82",fontsize=4.5,fontweight="bold")
seen={}
for r in all_results:
if r["action"] not in seen: seen[r["action"]]=bc(r["action"])
ax_b.legend([mpatches.Patch(color=c,alpha=0.88) for c in seen.values()],[k.upper() for k in seen],
loc="upper right",fontsize=5,framealpha=0.5,labelcolor="#2a5a82",edgecolor="#d0e8f4")
fig_b.tight_layout(pad=0.2)
st.pyplot(fig_b, use_container_width=False); plt.close(fig_b)
st.markdown('
', unsafe_allow_html=True)
# ── Row 3: white HTML table ──
rows_html="".join([f'| {"("+r["log"][:65]+")" if not isinstance(r["log"],str) else r["log"][:65]} | {r["action"].upper()} | {r["confidence"]}% |
' for r in all_results])
st.markdown(f"""
| Log |
Action |
Confidence |
{rows_html}
""", unsafe_allow_html=True)
# ── FORCE STYLES via JavaScript — runs after React renders, always wins ───────
st.markdown("""
""", unsafe_allow_html=True)