Spaces:
Sleeping
Sleeping
Upload 20 files
Browse files- .dockerignore +7 -0
- .env +1 -0
- .gitignore +5 -0
- blockchain/blockchain.py +386 -0
- blockchain/blockchain_blocks_summary.csv +30 -0
- blockchain/blockchain_ledger.json +385 -0
- frontend/dashboard.py +658 -0
- models/request_models.py +8 -0
- models/response_models.py +6 -0
- requirements.txt +80 -5
- routers/blockchain.py +22 -0
- routers/monitor.py +131 -0
- routers/predict.py +136 -0
- routers/reports.py +309 -0
- routers/upload.py +275 -0
- utils/model_loader.py +49 -0
- utils/pcap_converter.py +235 -0
- utils/severity.py +23 -0
.dockerignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.env
|
| 4 |
+
.git
|
| 5 |
+
venv
|
| 6 |
+
env
|
| 7 |
+
.DS_Store
|
.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
GROQ_API_KEY='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'
|
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
instance/
|
| 5 |
+
data/
|
blockchain/blockchain.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import graphviz
|
| 8 |
+
from typing import List, Optional
|
| 9 |
+
import io
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parent.parent # Project root
|
| 12 |
+
ALT = ROOT # Use project root instead of /mnt/data
|
| 13 |
+
UPLOAD_DIR = ALT / "uploads"
|
| 14 |
+
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
| 15 |
+
|
| 16 |
+
# Make sure your merkle module is importable
|
| 17 |
+
if str(ROOT / "Merkle_tree") not in sys.path:
|
| 18 |
+
sys.path.append(str(ROOT / "Merkle_tree"))
|
| 19 |
+
|
| 20 |
+
# Import your Merkle utilities
|
| 21 |
+
try:
|
| 22 |
+
from merkle_ledger import MerkleTree, ThreatLogEntry, sha256_hex
|
| 23 |
+
except Exception as e:
|
| 24 |
+
st.warning("Could not import merkle_ledger module. Make sure merkle_ledger.py is in the same folder.")
|
| 25 |
+
st.stop()
|
| 26 |
+
|
| 27 |
+
st.set_page_config(page_title="Blockchain Demo (Sidebar Nav)", layout="wide")
|
| 28 |
+
st.title("🔐 Blockchain & IDS Demo")
|
| 29 |
+
|
| 30 |
+
# ----------------- Utility: safe timestamp parsing -----------------
|
| 31 |
+
def parse_timestamp_safe(ts) -> Optional[object]:
|
| 32 |
+
"""
|
| 33 |
+
Accepts: None, empty, string, pandas.Timestamp, datetime.
|
| 34 |
+
Returns: datetime (python) or None.
|
| 35 |
+
"""
|
| 36 |
+
if ts is None:
|
| 37 |
+
return None
|
| 38 |
+
try:
|
| 39 |
+
if pd.isna(ts):
|
| 40 |
+
return None
|
| 41 |
+
except Exception:
|
| 42 |
+
pass
|
| 43 |
+
if hasattr(ts, "isoformat"):
|
| 44 |
+
return ts
|
| 45 |
+
if isinstance(ts, pd.Timestamp):
|
| 46 |
+
try:
|
| 47 |
+
return ts.to_pydatetime()
|
| 48 |
+
except Exception:
|
| 49 |
+
return None
|
| 50 |
+
if isinstance(ts, str):
|
| 51 |
+
s = ts.strip()
|
| 52 |
+
if s == "":
|
| 53 |
+
return None
|
| 54 |
+
try:
|
| 55 |
+
return pd.to_datetime(s).to_pydatetime()
|
| 56 |
+
except Exception:
|
| 57 |
+
return None
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
def df_to_entries_safe(df: pd.DataFrame) -> List[ThreatLogEntry]:
|
| 61 |
+
entries = []
|
| 62 |
+
for _, r in df.iterrows():
|
| 63 |
+
ts_parsed = parse_timestamp_safe(r.get("timestamp", None))
|
| 64 |
+
try:
|
| 65 |
+
entry = ThreatLogEntry.create(
|
| 66 |
+
flow_id=str(r.get("flow_id", "")),
|
| 67 |
+
attack_label=str(r.get("attack_label", "")),
|
| 68 |
+
severity=float(r.get("severity", 0.0)) if pd.notna(r.get("severity", None)) else 0.0,
|
| 69 |
+
src_ip=str(r.get("src_ip", "")),
|
| 70 |
+
dst_ip=str(r.get("dst_ip", "")),
|
| 71 |
+
action=str(r.get("action", "")),
|
| 72 |
+
timestamp=ts_parsed
|
| 73 |
+
)
|
| 74 |
+
except Exception as e:
|
| 75 |
+
st.error(f"Failed to create ThreatLogEntry for row {_}: {e}")
|
| 76 |
+
raise
|
| 77 |
+
entries.append(entry)
|
| 78 |
+
return entries
|
| 79 |
+
|
| 80 |
+
# ----------------- Sidebar navigation -----------------
|
| 81 |
+
page = st.sidebar.radio("📑 Pages", ["Merkle Playground", "Upload Logs", "Blockchain Explorer"])
|
| 82 |
+
|
| 83 |
+
# ----------------- Page 1: Merkle Playground -----------------
|
| 84 |
+
if page == "Merkle Playground":
|
| 85 |
+
st.header("🌳 Merkle Playground")
|
| 86 |
+
st.write("Edit a small set of logs, build a Merkle tree, then verify to detect tampering.")
|
| 87 |
+
|
| 88 |
+
n_leaves = st.slider("Number of leaves", min_value=4, max_value=16, value=8)
|
| 89 |
+
|
| 90 |
+
def make_sample(i):
|
| 91 |
+
e = ThreatLogEntry.create(
|
| 92 |
+
flow_id=f"flow_{i+1}",
|
| 93 |
+
attack_label="Benign" if i % 3 == 0 else "DoS Hulk" if i % 3 == 1 else "PortScan",
|
| 94 |
+
severity=round(0.1 + (i % 10) * 0.07, 2),
|
| 95 |
+
src_ip=f"10.0.0.{(i % 6) + 1}",
|
| 96 |
+
dst_ip=f"192.168.0.{(i % 10) + 1}",
|
| 97 |
+
action="",
|
| 98 |
+
)
|
| 99 |
+
return {
|
| 100 |
+
"timestamp": e.timestamp,
|
| 101 |
+
"flow_id": e.flow_id,
|
| 102 |
+
"attack_label": e.attack_label,
|
| 103 |
+
"severity": e.severity,
|
| 104 |
+
"src_ip": e.src_ip,
|
| 105 |
+
"dst_ip": e.dst_ip,
|
| 106 |
+
"action": e.action,
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# Init dataset
|
| 110 |
+
if "merkle_df" not in st.session_state:
|
| 111 |
+
st.session_state.merkle_df = pd.DataFrame([make_sample(i) for i in range(n_leaves)])
|
| 112 |
+
|
| 113 |
+
if len(st.session_state.merkle_df) != n_leaves:
|
| 114 |
+
st.session_state.merkle_df = pd.DataFrame([make_sample(i) for i in range(n_leaves)])
|
| 115 |
+
|
| 116 |
+
try:
|
| 117 |
+
orig_entries = df_to_entries_safe(st.session_state.merkle_df)
|
| 118 |
+
except Exception:
|
| 119 |
+
st.error("Error converting rows to ThreatLogEntry.")
|
| 120 |
+
st.stop()
|
| 121 |
+
|
| 122 |
+
orig_leaf_hashes = [sha256_hex(e.to_canonical_string()) for e in orig_entries]
|
| 123 |
+
orig_tree = MerkleTree([e.to_canonical_string() for e in orig_entries])
|
| 124 |
+
|
| 125 |
+
# ---------------- GRAPH FIRST ----------------
|
| 126 |
+
st.markdown("### 🌲 Merkle Tree Visualization")
|
| 127 |
+
try:
|
| 128 |
+
dot = graphviz.Digraph()
|
| 129 |
+
dot.attr(rankdir="TB", size="14,10")
|
| 130 |
+
dot.attr("node", style="filled", fontname="Helvetica", fontsize="11")
|
| 131 |
+
dot.attr("edge", penwidth="2.5", color="#555555")
|
| 132 |
+
|
| 133 |
+
levels = orig_tree.levels
|
| 134 |
+
colors = ["#90EE90", "#87CEEB", "#FFB6C1", "#FFA07A", "#FF6B6B"]
|
| 135 |
+
|
| 136 |
+
for lvl_idx, level in enumerate(levels):
|
| 137 |
+
color = colors[min(lvl_idx, len(colors) - 1)]
|
| 138 |
+
for pos, h in enumerate(level):
|
| 139 |
+
node = f"n_{lvl_idx}_{pos}"
|
| 140 |
+
label = f"{h[:12]}...\\nLevel {lvl_idx} | Pos {pos}"
|
| 141 |
+
|
| 142 |
+
if lvl_idx == len(levels) - 1:
|
| 143 |
+
dot.node(
|
| 144 |
+
node,
|
| 145 |
+
label=label,
|
| 146 |
+
shape="box",
|
| 147 |
+
fillcolor="#FF4444",
|
| 148 |
+
fontcolor="white",
|
| 149 |
+
penwidth="4",
|
| 150 |
+
style="filled,bold",
|
| 151 |
+
)
|
| 152 |
+
else:
|
| 153 |
+
dot.node(node, label=label, shape="box", fillcolor=color, fontcolor="#333333", penwidth="2")
|
| 154 |
+
|
| 155 |
+
for lvl_idx in range(len(levels) - 1):
|
| 156 |
+
for pos in range(len(levels[lvl_idx])):
|
| 157 |
+
dot.edge(
|
| 158 |
+
f"n_{lvl_idx}_{pos}",
|
| 159 |
+
f"n_{lvl_idx+1}_{pos//2}",
|
| 160 |
+
color=colors[min(lvl_idx + 1, len(colors) - 1)],
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
st.graphviz_chart(dot, use_container_width=True)
|
| 164 |
+
|
| 165 |
+
except Exception as e:
|
| 166 |
+
st.warning(f"Tree visualization failed: {e}")
|
| 167 |
+
st.write("Levels:", orig_tree.levels)
|
| 168 |
+
|
| 169 |
+
# ---------------- ROOT AFTER GRAPH ----------------
|
| 170 |
+
st.markdown("### 🔑 Original Merkle Root")
|
| 171 |
+
st.code(orig_tree.root, language="text")
|
| 172 |
+
|
| 173 |
+
# ---------------- EDIT TABLE BELOW GRAPH ----------------
|
| 174 |
+
st.markdown("### ✏️ Edit Entries")
|
| 175 |
+
edited = st.data_editor(
|
| 176 |
+
st.session_state.merkle_df,
|
| 177 |
+
num_rows="dynamic",
|
| 178 |
+
use_container_width=True,
|
| 179 |
+
key="editor",
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# ---------------- SUMMARY ----------------
|
| 183 |
+
st.markdown("### 📊 Summary")
|
| 184 |
+
col1, col2, col3 = st.columns(3)
|
| 185 |
+
with col1:
|
| 186 |
+
st.metric("Total Leaves", len(orig_entries))
|
| 187 |
+
with col2:
|
| 188 |
+
st.metric("Tree Depth", len(orig_tree.levels))
|
| 189 |
+
with col3:
|
| 190 |
+
st.metric("Root Hash (short)", orig_tree.root[:16] + "...")
|
| 191 |
+
|
| 192 |
+
st.markdown("---")
|
| 193 |
+
|
| 194 |
+
# ---------------- VERIFICATION ----------------
|
| 195 |
+
if st.button("🔍 Verify Edits", type="primary"):
|
| 196 |
+
|
| 197 |
+
try:
|
| 198 |
+
new_entries = df_to_entries_safe(edited)
|
| 199 |
+
except Exception:
|
| 200 |
+
st.error("Invalid timestamps or formatting.")
|
| 201 |
+
st.stop()
|
| 202 |
+
|
| 203 |
+
new_hashes = [sha256_hex(e.to_canonical_string()) for e in new_entries]
|
| 204 |
+
new_tree = MerkleTree([e.to_canonical_string() for e in new_entries])
|
| 205 |
+
tampered = new_tree.root != orig_tree.root
|
| 206 |
+
|
| 207 |
+
changed = [i for i, (a, b) in enumerate(zip(orig_leaf_hashes, new_hashes)) if a != b]
|
| 208 |
+
|
| 209 |
+
col1, col2 = st.columns(2)
|
| 210 |
+
with col1:
|
| 211 |
+
st.write("**Original root:**")
|
| 212 |
+
st.code(orig_tree.root)
|
| 213 |
+
with col2:
|
| 214 |
+
st.write("**New root:**")
|
| 215 |
+
st.code(new_tree.root)
|
| 216 |
+
|
| 217 |
+
if tampered:
|
| 218 |
+
st.error(f"⚠️ TAMPERED — Changed leaf indices: {changed}")
|
| 219 |
+
|
| 220 |
+
highlighted = edited.copy()
|
| 221 |
+
highlighted["_tampered"] = ["✓ YES" if i in changed else "" for i in range(len(highlighted))]
|
| 222 |
+
|
| 223 |
+
st.dataframe(
|
| 224 |
+
highlighted.style.apply(
|
| 225 |
+
lambda row: [
|
| 226 |
+
"background-color: #ffdddd" if row.name in changed else "" for _ in row
|
| 227 |
+
],
|
| 228 |
+
axis=1,
|
| 229 |
+
),
|
| 230 |
+
use_container_width=True,
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
else:
|
| 234 |
+
st.success("✅ No tampering detected. Roots match!")
|
| 235 |
+
st.dataframe(edited, use_container_width=True)
|
| 236 |
+
|
| 237 |
+
# ----------------- Page 2: Upload -----------------
|
| 238 |
+
elif page == "Upload Logs":
|
| 239 |
+
st.header("📤 Upload Logs")
|
| 240 |
+
st.write("Upload a logs file and it will be saved to the uploads folder.")
|
| 241 |
+
|
| 242 |
+
upload = st.file_uploader("Upload CSV or JSON logs", type=["csv", "json", "txt"], accept_multiple_files=False)
|
| 243 |
+
if upload is not None:
|
| 244 |
+
save_path = UPLOAD_DIR / upload.name
|
| 245 |
+
with open(save_path, "wb") as f:
|
| 246 |
+
f.write(upload.getbuffer())
|
| 247 |
+
st.success(f"✅ Saved upload to: `{save_path}`")
|
| 248 |
+
st.info("💡 Use Explorer or Merkle Playground to work with this file later.")
|
| 249 |
+
|
| 250 |
+
# ----------------- Page 3: Explorer -----------------
|
| 251 |
+
elif page == "Blockchain Explorer":
|
| 252 |
+
st.header("🔍 Blockchain Explorer")
|
| 253 |
+
|
| 254 |
+
ledger_path = ALT / "blockchain_ledger.json"
|
| 255 |
+
summary_csv = ALT / "blockchain_blocks_summary.csv"
|
| 256 |
+
|
| 257 |
+
if not ledger_path.exists():
|
| 258 |
+
st.warning(f"⚠️ No ledger found at `{ledger_path.name}`. You can upload one below.")
|
| 259 |
+
uploaded_ledger = st.file_uploader("Upload ledger JSON", type=["json"], key="ledger_upload")
|
| 260 |
+
if uploaded_ledger is not None:
|
| 261 |
+
try:
|
| 262 |
+
ledger_json = json.load(uploaded_ledger)
|
| 263 |
+
with open(ledger_path, "w", encoding="utf-8") as f:
|
| 264 |
+
json.dump(ledger_json, f, indent=2)
|
| 265 |
+
st.success(f"✅ Ledger saved to {ledger_path}")
|
| 266 |
+
st.rerun()
|
| 267 |
+
except Exception as e:
|
| 268 |
+
st.error(f"❌ Failed to save ledger: {e}")
|
| 269 |
+
else:
|
| 270 |
+
try:
|
| 271 |
+
with open(ledger_path, "r", encoding="utf-8") as f:
|
| 272 |
+
ledger = json.load(f)
|
| 273 |
+
blocks = ledger.get("blocks", [])
|
| 274 |
+
|
| 275 |
+
if not blocks:
|
| 276 |
+
st.info("ℹ️ Ledger exists but contains no blocks.")
|
| 277 |
+
else:
|
| 278 |
+
# Blocks table
|
| 279 |
+
rows = []
|
| 280 |
+
for b in blocks:
|
| 281 |
+
idx = b.get("index")
|
| 282 |
+
batch = b.get("batch", {})
|
| 283 |
+
rows.append({
|
| 284 |
+
"Index": idx,
|
| 285 |
+
"Batch ID": batch.get("batch_id"),
|
| 286 |
+
"Merkle Root": batch.get("merkle_root", "")[:16] + "...",
|
| 287 |
+
"Entries": batch.get("entry_count"),
|
| 288 |
+
"Sealed At": b.get("created_at"),
|
| 289 |
+
"Block Hash": b.get("block_hash", "")[:16] + "...",
|
| 290 |
+
})
|
| 291 |
+
|
| 292 |
+
df_blocks = pd.DataFrame(rows)
|
| 293 |
+
st.markdown("### 📦 Blocks")
|
| 294 |
+
st.dataframe(df_blocks, use_container_width=True)
|
| 295 |
+
|
| 296 |
+
# Chain validation
|
| 297 |
+
st.markdown("### ✅ Chain Validation")
|
| 298 |
+
validity = []
|
| 299 |
+
last_hash = ""
|
| 300 |
+
chain_ok = True
|
| 301 |
+
|
| 302 |
+
def batch_dict_to_canonical(batch: dict) -> str:
|
| 303 |
+
# Reconstruct the same canonical string used by Block.create -> MerkleBatch.to_canonical_string
|
| 304 |
+
# Note: batch dict fields must match the names used when the ledger was saved.
|
| 305 |
+
return "|".join(
|
| 306 |
+
[
|
| 307 |
+
str(batch.get("batch_id", "")),
|
| 308 |
+
str(batch.get("sealed_at", "")),
|
| 309 |
+
str(batch.get("merkle_root", "")),
|
| 310 |
+
str(batch.get("entry_count", "")),
|
| 311 |
+
str(batch.get("signature", "")),
|
| 312 |
+
]
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
for b in blocks:
|
| 316 |
+
batch = b.get("batch", {})
|
| 317 |
+
# Recreate canonical header exactly as Block.create does:
|
| 318 |
+
header = "|".join(
|
| 319 |
+
[
|
| 320 |
+
str(b.get("index")),
|
| 321 |
+
batch_dict_to_canonical(batch),
|
| 322 |
+
str(b.get("prev_block_hash", "")),
|
| 323 |
+
str(b.get("created_at", "")),
|
| 324 |
+
]
|
| 325 |
+
)
|
| 326 |
+
expected_hash = sha256_hex(header)
|
| 327 |
+
ok = expected_hash == b.get("block_hash")
|
| 328 |
+
prev_ok = (b.get("prev_block_hash") == last_hash) if last_hash != "" else True
|
| 329 |
+
validity.append({
|
| 330 |
+
"Index": b.get("index"),
|
| 331 |
+
"Hash Valid": "✅" if ok else "❌",
|
| 332 |
+
"Prev Link Valid": "✅" if prev_ok else "❌"
|
| 333 |
+
})
|
| 334 |
+
last_hash = b.get("block_hash")
|
| 335 |
+
if not ok or not prev_ok:
|
| 336 |
+
chain_ok = False
|
| 337 |
+
|
| 338 |
+
if chain_ok:
|
| 339 |
+
st.success("✅ Chain is valid!")
|
| 340 |
+
else:
|
| 341 |
+
st.error("❌ Chain integrity compromised!")
|
| 342 |
+
|
| 343 |
+
st.dataframe(pd.DataFrame(validity), use_container_width=True)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
# Block viewer
|
| 347 |
+
st.markdown("### 🔎 Block Viewer")
|
| 348 |
+
idx_choice = st.number_input("Block index to view", min_value=1, max_value=len(blocks), value=1, step=1)
|
| 349 |
+
chosen = next((b for b in blocks if b.get("index") == int(idx_choice)), None)
|
| 350 |
+
|
| 351 |
+
if chosen:
|
| 352 |
+
st.json(chosen)
|
| 353 |
+
|
| 354 |
+
pub = ledger.get("public_key_pem")
|
| 355 |
+
if pub:
|
| 356 |
+
with st.expander("🔐 Public Key (PEM)"):
|
| 357 |
+
st.code(pub, language="text")
|
| 358 |
+
|
| 359 |
+
if st.button("💾 Download this block JSON"):
|
| 360 |
+
st.download_button(
|
| 361 |
+
"Download block",
|
| 362 |
+
data=json.dumps(chosen, indent=2),
|
| 363 |
+
file_name=f"block_{chosen.get('index')}.json",
|
| 364 |
+
mime="application/json"
|
| 365 |
+
)
|
| 366 |
+
except Exception as e:
|
| 367 |
+
st.error(f"❌ Failed to read ledger: {e}")
|
| 368 |
+
|
| 369 |
+
# Summary CSV
|
| 370 |
+
if summary_csv.exists():
|
| 371 |
+
st.markdown("---")
|
| 372 |
+
st.markdown("### 📊 Blocks Summary CSV")
|
| 373 |
+
try:
|
| 374 |
+
df_summary = pd.read_csv(summary_csv)
|
| 375 |
+
st.dataframe(df_summary.head(200), use_container_width=True)
|
| 376 |
+
|
| 377 |
+
if st.button("💾 Download blocks summary CSV"):
|
| 378 |
+
with open(summary_csv, "rb") as f:
|
| 379 |
+
st.download_button(
|
| 380 |
+
"Download summary CSV",
|
| 381 |
+
data=f,
|
| 382 |
+
file_name="blockchain_blocks_summary.csv",
|
| 383 |
+
mime="text/csv"
|
| 384 |
+
)
|
| 385 |
+
except Exception as e:
|
| 386 |
+
st.error(f"❌ Failed to read summary CSV: {e}")
|
blockchain/blockchain_blocks_summary.csv
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
block_index,batch_id,merkle_root,entry_count,sealed_at,block_hash,prev_block_hash
|
| 2 |
+
1,2,6acfafddf5baf13ac165d3f20c75b67dbc1815e3962443c43f21c9905d177083,10000,2025-11-22T19:16:57Z,a45bd4fbbd550dec8538364d712fe215f3a576877458ffe85e588a0ccdbcf997,
|
| 3 |
+
2,3,186449fff72c5a7bf962fbe6354a8481e7330dc0884fb0ab5db49bbcf513676e,10000,2025-11-22T19:16:57Z,64dc1768c2e5560d895aaa6bcb0f8fba0a4fc8be45156e6415ee643c38fd1b6a,a45bd4fbbd550dec8538364d712fe215f3a576877458ffe85e588a0ccdbcf997
|
| 4 |
+
3,4,bb896cf321529c5e41dc71e508849890fcac031b4eebea67fc4271a08bd76803,10000,2025-11-22T19:16:58Z,c1ca2d8004cd922e26980fb407e0a58e226d7d4a24e1d03cd8629f81acccc708,64dc1768c2e5560d895aaa6bcb0f8fba0a4fc8be45156e6415ee643c38fd1b6a
|
| 5 |
+
4,5,865ddfb1f70f486526ac9918d53f45b8de042bdad764286703ef9715646c0317,10000,2025-11-22T19:16:58Z,194f00146556d1772a906f7eb310743cff6c31519714e8158bbeccef5883cc7b,c1ca2d8004cd922e26980fb407e0a58e226d7d4a24e1d03cd8629f81acccc708
|
| 6 |
+
5,6,21f3c7716b135d92f4ff7aeed4433e3146c24a67b2302a1436a5d924ac2379aa,10000,2025-11-22T19:16:58Z,faa11268c8f982c10e273aa721cb968c957767b5b23befebf5f1ad3679b5355b,194f00146556d1772a906f7eb310743cff6c31519714e8158bbeccef5883cc7b
|
| 7 |
+
6,7,40d0aeefc769dd4b29618933450f0c92a6dc0c216b84f0e04ede7eec579be004,10000,2025-11-22T19:16:58Z,829849616cbc1923a1607930915a5c885a4dc4d72979e6d431e88190d6a780cb,faa11268c8f982c10e273aa721cb968c957767b5b23befebf5f1ad3679b5355b
|
| 8 |
+
7,8,23132677c6725042d04ae403c5b51d79aa210ccb77ecb6bc872d7a5c1f45427e,10000,2025-11-22T19:16:59Z,fd349f81c5e25715d69405149c6db89ba9d1a556c6b66d15fdc9aaef57b6f6d3,829849616cbc1923a1607930915a5c885a4dc4d72979e6d431e88190d6a780cb
|
| 9 |
+
8,9,97b4b53d92e027260779a3a0497fb478a0a7758055d97f57a50c8428ab7f6623,10000,2025-11-22T19:16:59Z,df3f2aaed6aad904cd8acf59d34d50290f29c83e27acbd2c5bbfa83e0187634b,fd349f81c5e25715d69405149c6db89ba9d1a556c6b66d15fdc9aaef57b6f6d3
|
| 10 |
+
9,10,08c50fa39bdc316519fc833b0298c0f57588175bd758f46995b7962d18ffdae5,10000,2025-11-22T19:16:59Z,4bee927e6c99969b786dbba0683085af6538bd505853ffc40bbcf5d4a636787c,df3f2aaed6aad904cd8acf59d34d50290f29c83e27acbd2c5bbfa83e0187634b
|
| 11 |
+
10,11,1e30e4933dffabb042c48fc63c4a4e4d0354cc69f0b4ad68a07ee34246512c59,10000,2025-11-22T19:17:00Z,2e31a182778accc2a0ccb798eb6d7a427a8ea250ca228aed76d96800bb29e346,4bee927e6c99969b786dbba0683085af6538bd505853ffc40bbcf5d4a636787c
|
| 12 |
+
11,12,fb9bbb10b0ab3e17ec351237fb625c00a3fc05306138d597dad5fd51500f3a21,10000,2025-11-22T19:17:00Z,79b1584425440d085f6a023ae69b7836b9df6b1da617e2ca69a684a37234e01d,2e31a182778accc2a0ccb798eb6d7a427a8ea250ca228aed76d96800bb29e346
|
| 13 |
+
12,13,b4b41031c11e05d451073a214d9cfc7d6a159de8e4f2ac2fac8f6b91b97eca6b,10000,2025-11-22T19:17:00Z,de2238a9ce0f32bea61e30e9e8f1983056fa150f6e39395372c7c914e87b07e3,79b1584425440d085f6a023ae69b7836b9df6b1da617e2ca69a684a37234e01d
|
| 14 |
+
13,14,d0cdee06632777e094da1d6b6b5c7cfcb3af86daf546d915ad5bf39cfb2bbf47,10000,2025-11-22T19:17:01Z,c178ae13d6fffd822a63122c6ec07e871879306a2726ad358ac7aba9eef4340a,de2238a9ce0f32bea61e30e9e8f1983056fa150f6e39395372c7c914e87b07e3
|
| 15 |
+
14,15,4d82fc97795ba932b1f271ed918ac4f5965a339787407453829518517987cd64,10000,2025-11-22T19:17:01Z,2525069c543260f8f208a0ef1a423fe7c36b8cffc2feab78c3124797503e51a1,c178ae13d6fffd822a63122c6ec07e871879306a2726ad358ac7aba9eef4340a
|
| 16 |
+
15,16,8ef66e722ae0aad304a527c900513d9fdb1be47ff3351b01b89b8709cb282a05,10000,2025-11-22T19:17:01Z,583fc2a41a94aea3bf5a696f85e797e5153fbcda5bf412f6c27a437809424ea5,2525069c543260f8f208a0ef1a423fe7c36b8cffc2feab78c3124797503e51a1
|
| 17 |
+
16,17,dd429836c08376bed6e9425d9b149fd270efa31732ddfc34b7077ce63e996003,10000,2025-11-22T19:17:02Z,abe5dbb5abafbd3704f8fa1bf394967eab912766fe8206fc811a52b86cba729d,583fc2a41a94aea3bf5a696f85e797e5153fbcda5bf412f6c27a437809424ea5
|
| 18 |
+
17,18,5663054ef7f12bd5ed66cac5ce8ad974552f71fd8e39f179efbdd0047fef2769,10000,2025-11-22T19:17:02Z,0595e6027a5e2fbaec115c75d22f850689102ac45ff0ea19cfbc1d7a0b1e60af,abe5dbb5abafbd3704f8fa1bf394967eab912766fe8206fc811a52b86cba729d
|
| 19 |
+
18,19,b97f2490f8e743851dae9aa89fd797e6ce88a643b22e2870bee5ee20f3ebfcda,10000,2025-11-22T19:17:02Z,75f0a6b245f6c9f9e6679325c051ef5fc3ae42f571c901f18a9a40db27b9c5bb,0595e6027a5e2fbaec115c75d22f850689102ac45ff0ea19cfbc1d7a0b1e60af
|
| 20 |
+
19,20,7a3a09ac28fc14bf15e9f55fb6a188ba144593d14830b45e6b841a56f32f9a2e,10000,2025-11-22T19:17:03Z,a7de9e1275cc4736cf92933da9e2d9eac0c20d4dd5d6cee5631e321e70581a73,75f0a6b245f6c9f9e6679325c051ef5fc3ae42f571c901f18a9a40db27b9c5bb
|
| 21 |
+
20,21,4f92611d565d047d4e2cb41e767ffcafe939a08172342176d0cb44e5c6e79b5b,10000,2025-11-22T19:17:03Z,4ff895d62dced30f43ca3461607b4d644c345f55dcab3e7b439369290c7bad0c,a7de9e1275cc4736cf92933da9e2d9eac0c20d4dd5d6cee5631e321e70581a73
|
| 22 |
+
21,22,253fd455b73d3d0afd62f034f7b0e1b62cf73265377ef874c182a783ae1d1d19,10000,2025-11-22T19:17:03Z,61ba62b8d5192635792183e6004eb8bf7b8cc30af5f440f24c3db83d2aa2874e,4ff895d62dced30f43ca3461607b4d644c345f55dcab3e7b439369290c7bad0c
|
| 23 |
+
22,23,39dc01050c07e6812b36572f0c17183b6bd6ede6ce126108e6e2e19b33458bd8,10000,2025-11-22T19:17:04Z,e14fe45413eb9cf5b094bc739e0bba0806a867c6ec4acb301caf2a4f35978a11,61ba62b8d5192635792183e6004eb8bf7b8cc30af5f440f24c3db83d2aa2874e
|
| 24 |
+
23,24,1eef7d2939cdab5e8f1754f0d16d2c200fbeb63ba53a6c18ee04bad5fde7224e,10000,2025-11-22T19:17:04Z,01052c7193fedcf1f9f8fb2932699e9620cfe0485f30200fadb0c241e81f5db5,e14fe45413eb9cf5b094bc739e0bba0806a867c6ec4acb301caf2a4f35978a11
|
| 25 |
+
24,25,f2db360b97eee849554925281f986af7deae46ca73289fb9ba2e76b22a01be6e,10000,2025-11-22T19:17:04Z,4a10e731d51d2d8f19da1da6f3a0a7181c9bf0665f41f3cce28f21220a8800a1,01052c7193fedcf1f9f8fb2932699e9620cfe0485f30200fadb0c241e81f5db5
|
| 26 |
+
25,26,4c8b0541e6658c76a8847640ceab221c09e3d4e4e2e122cfe2dfb334d7dc5141,10000,2025-11-22T19:17:05Z,a02a76c97cdee3338e011c75eb812e417d8ab8deecd337bc63dc1d0cab7c3279,4a10e731d51d2d8f19da1da6f3a0a7181c9bf0665f41f3cce28f21220a8800a1
|
| 27 |
+
26,27,5b1ad6a115f9edf1bc992160d212b502e3f35472fb4c306c1d21e1f54e73f782,10000,2025-11-22T19:17:05Z,0e43b0b3e902f69eadb068e53243298a5229155f0ecb76e0dda6d6ddc3516e11,a02a76c97cdee3338e011c75eb812e417d8ab8deecd337bc63dc1d0cab7c3279
|
| 28 |
+
27,28,63231a181eb5585ef114d3c076d78977dfe49941b8ee918d5ea83406084d7b66,10000,2025-11-22T19:17:05Z,a4bb71d882bdbbc97aca798903d0c30cead1078b9810162b33ea06a646167be3,0e43b0b3e902f69eadb068e53243298a5229155f0ecb76e0dda6d6ddc3516e11
|
| 29 |
+
28,29,894cac81366f977d3e57e13c2d4c97be976758ecdd160c3553f158a7bf25d6b6,10000,2025-11-22T19:17:06Z,eea819511c1ce3cf3e43e209d90d581b09985c1691344f288ed420d3a1c5c1e1,a4bb71d882bdbbc97aca798903d0c30cead1078b9810162b33ea06a646167be3
|
| 30 |
+
29,30,c870566a387d06dfd75f2d78dd67e016233da1588cfcaebe51925109a1c13930,10000,2025-11-22T19:17:06Z,62965f0f9385c18691826f11315a82394da1355200ce3edb99a2a1472e5f8583,eea819511c1ce3cf3e43e209d90d581b09985c1691344f288ed420d3a1c5c1e1
|
blockchain/blockchain_ledger.json
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"batch_size": 10000,
|
| 3 |
+
"open_entries": [],
|
| 4 |
+
"sealed_batch_count": 29,
|
| 5 |
+
"blocks": [
|
| 6 |
+
{
|
| 7 |
+
"index": 1,
|
| 8 |
+
"batch": {
|
| 9 |
+
"batch_id": 2,
|
| 10 |
+
"sealed_at": "2025-11-22T19:16:57Z",
|
| 11 |
+
"merkle_root": "6acfafddf5baf13ac165d3f20c75b67dbc1815e3962443c43f21c9905d177083",
|
| 12 |
+
"entry_count": 10000,
|
| 13 |
+
"signature": "orU8R+YqabMiruDXq/FlFKI6PVgD1y34Idurem9iG1Hlt08fjDpls0ehrA/fu+PqVSpIrAxhBIdUap1xNVHB8F0psYTeeXMdzz+M5Gh3Xb+jiJiuOqlMwswu6pEVLpcFCB3oVc+KF2/TG3eI3D9N3wMr07IuOxuBonwqcWvBoQCitwqvfes86Rtid6N8Rwx0pKtPkC4ijq/wKP210Zt/9NMEffeTMgFgyANJdR5C8PamSNEjKXspZWhZxWyQ3+9jQh94/UxyhU3nTYXMQWVgCMDrJvFw4D6ksIMS3UjbmK8g8SxNz4240cW5Xa5AO3AV8In1tCXFzQfOeq3Glzctvw=="
|
| 14 |
+
},
|
| 15 |
+
"prev_block_hash": "",
|
| 16 |
+
"block_hash": "a45bd4fbbd550dec8538364d712fe215f3a576877458ffe85e588a0ccdbcf997",
|
| 17 |
+
"created_at": "2025-11-22T19:16:57Z"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"index": 2,
|
| 21 |
+
"batch": {
|
| 22 |
+
"batch_id": 3,
|
| 23 |
+
"sealed_at": "2025-11-22T19:16:57Z",
|
| 24 |
+
"merkle_root": "186449fff72c5a7bf962fbe6354a8481e7330dc0884fb0ab5db49bbcf513676e",
|
| 25 |
+
"entry_count": 10000,
|
| 26 |
+
"signature": "J50WnUzke002+0p5Zs+GR1J8erIqeeLNkI8k8DiXQ1IU6ncIuLoIhXmgXtra3Z19RtCHpx3f8N+0MWEbZ8kc5+zentmi5PpZm049VW1hp7N0h+bP42iyKwO/zjGhAbtN0wdtN4557InnKLG1qXY40Ov1EafvBVgH4dcuc9vxhvh0aLhWYUOfPCPAjqs9+kdeCbY1BQLtpQsO5SuzOK9A9FgLdf6laFB6A/kGut28XRvInbNKWhLizUFkB8sngjlv6Qm9byxyzf6UJQs4OZMhUtfFo2MVy2qpLkLuWCNkvehp0AASG7rG8473ARZVFY5qth//NKnZB2aQv5JaXf8sYg=="
|
| 27 |
+
},
|
| 28 |
+
"prev_block_hash": "a45bd4fbbd550dec8538364d712fe215f3a576877458ffe85e588a0ccdbcf997",
|
| 29 |
+
"block_hash": "64dc1768c2e5560d895aaa6bcb0f8fba0a4fc8be45156e6415ee643c38fd1b6a",
|
| 30 |
+
"created_at": "2025-11-22T19:16:57Z"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"index": 3,
|
| 34 |
+
"batch": {
|
| 35 |
+
"batch_id": 4,
|
| 36 |
+
"sealed_at": "2025-11-22T19:16:58Z",
|
| 37 |
+
"merkle_root": "bb896cf321529c5e41dc71e508849890fcac031b4eebea67fc4271a08bd76803",
|
| 38 |
+
"entry_count": 10000,
|
| 39 |
+
"signature": "YDec8aW8DUXgXevdJsrN0yhLbKq1po5GpDY/5qnPSf2eXNtjd+NzU0DVWrsZhTwvV+36mkLOnSNGcD5UIUBvEN0A/GXTo3SNgrHaSQTmVfB1h/miD1L3aBnGBRdXjdU86KcOC2iJ/31YZW3M9po4jq2AebibGXFgmFLYZdeayR6SWI9nRPl+bNblHCTJAj9lIf1dGpu0AtjJN+qrx+K6/JxnHKcrL7IXBwrAbaa+sIRVNAyz0h5xF7sscrQXZxRpy8+uPRwpvjTOE2KILhJicWkf3iEJBXaIWbboAv7IIFnh2FQM+EN2IfgsLSt4Ny/9dMpweHXQXqkKquP8Hg0E4A=="
|
| 40 |
+
},
|
| 41 |
+
"prev_block_hash": "64dc1768c2e5560d895aaa6bcb0f8fba0a4fc8be45156e6415ee643c38fd1b6a",
|
| 42 |
+
"block_hash": "c1ca2d8004cd922e26980fb407e0a58e226d7d4a24e1d03cd8629f81acccc708",
|
| 43 |
+
"created_at": "2025-11-22T19:16:58Z"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"index": 4,
|
| 47 |
+
"batch": {
|
| 48 |
+
"batch_id": 5,
|
| 49 |
+
"sealed_at": "2025-11-22T19:16:58Z",
|
| 50 |
+
"merkle_root": "865ddfb1f70f486526ac9918d53f45b8de042bdad764286703ef9715646c0317",
|
| 51 |
+
"entry_count": 10000,
|
| 52 |
+
"signature": "kskDt/a+cwmSXxRDPqzRIR0Cj/1yKSzM1Xp9dh8BuheM1hkkkO5gn5scgxWeE2LNRjaKRZZ88u0GXofQLSVbALI4fSZc7ZTEi8g1nSB2WBvDHBlddizPWrLXPrDiu8xgSVirgU+7HCow7YhzVSdaW0Q5Kjvk7fvRbovb/fK9j0O70GFIXoYhFM3MoRVNRE/5l70gvp/0yodx3kg7oRkGIlE7ALfSYnZZaJYseEx4m/mQEVPkNygb4ELtC2CdfBdb2M+k6ktSxtYHTU/ZP2mUD0XTtrJ/Pp/ev8Skksa56O1CrEj1RxSSXbgjenWuCPLjhius83AyF2/7Dz1JEVEz0A=="
|
| 53 |
+
},
|
| 54 |
+
"prev_block_hash": "c1ca2d8004cd922e26980fb407e0a58e226d7d4a24e1d03cd8629f81acccc708",
|
| 55 |
+
"block_hash": "194f00146556d1772a906f7eb310743cff6c31519714e8158bbeccef5883cc7b",
|
| 56 |
+
"created_at": "2025-11-22T19:16:58Z"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"index": 5,
|
| 60 |
+
"batch": {
|
| 61 |
+
"batch_id": 6,
|
| 62 |
+
"sealed_at": "2025-11-22T19:16:58Z",
|
| 63 |
+
"merkle_root": "21f3c7716b135d92f4ff7aeed4433e3146c24a67b2302a1436a5d924ac2379aa",
|
| 64 |
+
"entry_count": 10000,
|
| 65 |
+
"signature": "Mz3BSvPmz0zqbv0jrcvm+kk9giegJfPNxen967tolEeL8V0X0cg02mhttDv1+pEsj8WOb3USmTacYsDU8uei3QTCTPsvJk/nml4ilAxa1XwWuk5x26ajrLm6mF+e8SH2Pj5wu19HtEX8FXPkdk15eEwNhzmTaZVdt3cazHmM5SHuhSnOm29zm6ew8flByHHzCbBPriPKIO6gKyLmkmSUJhwnq8AzQin4JEmFNvTgaxr9V23FnXLSn1BrXJ9T6BzV5YVtpZcaN7PNfqu1LjmQCUiDbuyb1BcNaTYtYB6r6uASixED4tz4GfX3tu5gnZTKTPg3OFHzrfqhS/j9z+mSZA=="
|
| 66 |
+
},
|
| 67 |
+
"prev_block_hash": "194f00146556d1772a906f7eb310743cff6c31519714e8158bbeccef5883cc7b",
|
| 68 |
+
"block_hash": "faa11268c8f982c10e273aa721cb968c957767b5b23befebf5f1ad3679b5355b",
|
| 69 |
+
"created_at": "2025-11-22T19:16:58Z"
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"index": 6,
|
| 73 |
+
"batch": {
|
| 74 |
+
"batch_id": 7,
|
| 75 |
+
"sealed_at": "2025-11-22T19:16:58Z",
|
| 76 |
+
"merkle_root": "40d0aeefc769dd4b29618933450f0c92a6dc0c216b84f0e04ede7eec579be004",
|
| 77 |
+
"entry_count": 10000,
|
| 78 |
+
"signature": "LqAcCueD4c0Ze6LRrLBRGtFmIJ4ydVVzYX/yU7G/XIL0ji6ZhkhPeVhFDfmTmadFKvaLN6BDvTh+bFF8KElV7n9kRhy13yMQG9PGbuY7WZAFDb/C8iQXfXE4ElojcEkEyTY3za+Mfa9XDiCYzlG/3fUkc/s69jcuLzzcQHq+4+YHVU8lPne+Ea5C4hvs1Ld9i77K4SIWCjpaAOR1CAsJgF3lZAkk6LSuZ6GSiPiG3jGM1zSfk7iLoWKJEi4sKXFCP6GGt2Ddj7mzGUPc/EjhzAki4QJ5oNnzeYYqRZeCbEkw7tPQ+ArJt4BZ07WjqT/hIugtxdeNjl+UHUHjwYYuQg=="
|
| 79 |
+
},
|
| 80 |
+
"prev_block_hash": "faa11268c8f982c10e273aa721cb968c957767b5b23befebf5f1ad3679b5355b",
|
| 81 |
+
"block_hash": "829849616cbc1923a1607930915a5c885a4dc4d72979e6d431e88190d6a780cb",
|
| 82 |
+
"created_at": "2025-11-22T19:16:58Z"
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"index": 7,
|
| 86 |
+
"batch": {
|
| 87 |
+
"batch_id": 8,
|
| 88 |
+
"sealed_at": "2025-11-22T19:16:59Z",
|
| 89 |
+
"merkle_root": "23132677c6725042d04ae403c5b51d79aa210ccb77ecb6bc872d7a5c1f45427e",
|
| 90 |
+
"entry_count": 10000,
|
| 91 |
+
"signature": "J3eVEMm8y1BshkvV9JDSpmMQPZD2uwPIpJPg0EauJeqSng5lR4QOiqnK11FucQmayI1WzNDnjGHZMaE1XbUctEwdOmTUY3ouQxKfkQ9gkIkxDgLUnbd6mYwk4aqP//DJaozJ9l5ZZ7i7A/j++R170lXmqYmbo5r5L0ijCLvAoA7RDKMADqM3U1pThND6JDnIP7Tu8XakLqJNnJ2M7KOIY/vm7w4xU72/iizA51OpokawsrPLVa1JTmFCLm/NIeQJNM9CLxl+uWoL0EVdIBDDaYpcaBBTBryIDlvl2dKGIiSSAs0bZpUqIt6wlcYmkc9xUukbCCZ5pSLLo8fqRQGPtg=="
|
| 92 |
+
},
|
| 93 |
+
"prev_block_hash": "829849616cbc1923a1607930915a5c885a4dc4d72979e6d431e88190d6a780cb",
|
| 94 |
+
"block_hash": "fd349f81c5e25715d69405149c6db89ba9d1a556c6b66d15fdc9aaef57b6f6d3",
|
| 95 |
+
"created_at": "2025-11-22T19:16:59Z"
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"index": 8,
|
| 99 |
+
"batch": {
|
| 100 |
+
"batch_id": 9,
|
| 101 |
+
"sealed_at": "2025-11-22T19:16:59Z",
|
| 102 |
+
"merkle_root": "97b4b53d92e027260779a3a0497fb478a0a7758055d97f57a50c8428ab7f6623",
|
| 103 |
+
"entry_count": 10000,
|
| 104 |
+
"signature": "XgQVZGboCSrBvGgPWeVxVA0NwmQHlbQTJlaOtXnT0jD0C8WUui2Y/9mdzctQY2yjlPg7T9Lx+FUb1ypBgftlav1s80ZWMxInmq8DEBIokPtDjImZyT05QuYzmIh68R5+mDJU5ZcZ3AIHedmychvETJAWELsE4CMl7Ll/IN7KiC2xm9YtGkI70/0iQZJtLr2e1pLvigWfPCNUcPEY5U9R7kvLpLeqUxUnpLDnd0lf8TAkX8y5AWGuqHwZUE+2SWGkVkfuO37joCIMOpvIg2/l+ZsSeisNq6YelxMxg45BndyKTnY1sbctr8zBKfKbdg8B1etMSpMno8T4imTZRMfEsg=="
|
| 105 |
+
},
|
| 106 |
+
"prev_block_hash": "fd349f81c5e25715d69405149c6db89ba9d1a556c6b66d15fdc9aaef57b6f6d3",
|
| 107 |
+
"block_hash": "df3f2aaed6aad904cd8acf59d34d50290f29c83e27acbd2c5bbfa83e0187634b",
|
| 108 |
+
"created_at": "2025-11-22T19:16:59Z"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"index": 9,
|
| 112 |
+
"batch": {
|
| 113 |
+
"batch_id": 10,
|
| 114 |
+
"sealed_at": "2025-11-22T19:16:59Z",
|
| 115 |
+
"merkle_root": "08c50fa39bdc316519fc833b0298c0f57588175bd758f46995b7962d18ffdae5",
|
| 116 |
+
"entry_count": 10000,
|
| 117 |
+
"signature": "DSNgtLT0FW9IbuOXpS7HsOH6XENY+PFBsJPlRSUvf0IvkHvXBEkVwFSr0msJv0Xs0p4i1nc+bii857QaQs9svc2EBCCh/UhSwJeHTHDIRRRqwGFwv7L05Gh+IeDfQTpkNg2odvAfUgfC4nnwzYEZTDEeTjx0M4LJMDbE5Efrd7Dg6mcG1ZNrWhjffiSaSAlFgs8bvb0G5jTtghASPxVFHynUVWS/zkMLRzmKUpe3eQULQ0/jxXmrezrdgjQ79RWawzX6WksGuHqAP0maTlqwqLVcgC1acbPY3NQ7tctNjPif082joFjBECWvSgQqPUax5FT1/Xzc3h13qWIJWO7YTQ=="
|
| 118 |
+
},
|
| 119 |
+
"prev_block_hash": "df3f2aaed6aad904cd8acf59d34d50290f29c83e27acbd2c5bbfa83e0187634b",
|
| 120 |
+
"block_hash": "4bee927e6c99969b786dbba0683085af6538bd505853ffc40bbcf5d4a636787c",
|
| 121 |
+
"created_at": "2025-11-22T19:16:59Z"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"index": 10,
|
| 125 |
+
"batch": {
|
| 126 |
+
"batch_id": 11,
|
| 127 |
+
"sealed_at": "2025-11-22T19:17:00Z",
|
| 128 |
+
"merkle_root": "1e30e4933dffabb042c48fc63c4a4e4d0354cc69f0b4ad68a07ee34246512c59",
|
| 129 |
+
"entry_count": 10000,
|
| 130 |
+
"signature": "TMZiRx2AqJqQ31b85+IftfMkMYEbSxlPBflySVNDdYh8M9APW/u4PIlZdP2CSug5NgaDECtXmAEZl3uWyMbq55Lqq73+n820Lb9cwrPteCyqnMz2ns3uCn1eM+BptmX4OcWLkAHUKzaVZnrvC4fZFOMLsyIohD6NrwImcvz3yFJnA2N5+yAE8fP6/1XVrS7jqhjFzE8VhrKs8rMSuVNiKMuut3N7UTIIeD/geBqr10U/9zz0V85DmuNPaMZBMQb4Ixn4/hABgaexR8dEh7UeQRVxtRhQidUYrJ1h8X6ahwPpEtBUiqTbjr4HzbvPQti+zUfYmuZCZc10exA+t4UIYw=="
|
| 131 |
+
},
|
| 132 |
+
"prev_block_hash": "4bee927e6c99969b786dbba0683085af6538bd505853ffc40bbcf5d4a636787c",
|
| 133 |
+
"block_hash": "2e31a182778accc2a0ccb798eb6d7a427a8ea250ca228aed76d96800bb29e346",
|
| 134 |
+
"created_at": "2025-11-22T19:17:00Z"
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"index": 11,
|
| 138 |
+
"batch": {
|
| 139 |
+
"batch_id": 12,
|
| 140 |
+
"sealed_at": "2025-11-22T19:17:00Z",
|
| 141 |
+
"merkle_root": "fb9bbb10b0ab3e17ec351237fb625c00a3fc05306138d597dad5fd51500f3a21",
|
| 142 |
+
"entry_count": 10000,
|
| 143 |
+
"signature": "D96d3nJIznUb32IImUAzAQTZGOxN7EIVmzMsajRsaJb4WfmqE7Lr/FvurZmP/bO15mMKJPBa4t7PA5hIp5ZaYc9LJRaC85Xx7AmOEyjS0WssZvqyZPuVfb/TutOFe7f6GUh4geOWNmkTegVLLhgu2TwpQ/c4E9In9DU7tjVlcFvhOvbGYPObZVLUmY5NtTPpQcLBh5DCT/owsZ7215FtWhJSeF9NXV70Nbv9m5MwLZjmirL4ydqFFWhzNImzY4YdCf4uKw1Y//KT589Cjj/rUvusTbGUT548sSIJENXPuqTj4OR8UZezR52GNHNgiYq9ioVkYCBQhJSPHHbtH/a4cw=="
|
| 144 |
+
},
|
| 145 |
+
"prev_block_hash": "2e31a182778accc2a0ccb798eb6d7a427a8ea250ca228aed76d96800bb29e346",
|
| 146 |
+
"block_hash": "79b1584425440d085f6a023ae69b7836b9df6b1da617e2ca69a684a37234e01d",
|
| 147 |
+
"created_at": "2025-11-22T19:17:00Z"
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"index": 12,
|
| 151 |
+
"batch": {
|
| 152 |
+
"batch_id": 13,
|
| 153 |
+
"sealed_at": "2025-11-22T19:17:00Z",
|
| 154 |
+
"merkle_root": "b4b41031c11e05d451073a214d9cfc7d6a159de8e4f2ac2fac8f6b91b97eca6b",
|
| 155 |
+
"entry_count": 10000,
|
| 156 |
+
"signature": "hUBP3ok+wqBWpPeX2DKbH+Yfzb/Ly2al4tLfbo8B23CkaT/4sA5vje2nt0DzLifJ3s1IipOe3aqrvEp+kFUakfBHRNDR9RzforAfDB6+FVDMPD7+PyVgK+rkqhB3hAV3GRKE73bhP+OzwNp3xDaxka5qODHJj8efo1XMkOUa+1gw5BtmkNoDl12eW2OFCTctBuppwyKEmR4zpWUnNsxwVQF+yD95Wjt83NEWe8OVlvz9iegMOSyN9POzAEMSXzMNECu1mMVYLnrZw4K7p0d4nM3Gqy+QMTQhMsmiT3Bnnl/4J/ILQGX8wTYJkej2ti/EVKDgpfXnwnvVvKTzzQ1NtQ=="
|
| 157 |
+
},
|
| 158 |
+
"prev_block_hash": "79b1584425440d085f6a023ae69b7836b9df6b1da617e2ca69a684a37234e01d",
|
| 159 |
+
"block_hash": "de2238a9ce0f32bea61e30e9e8f1983056fa150f6e39395372c7c914e87b07e3",
|
| 160 |
+
"created_at": "2025-11-22T19:17:00Z"
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"index": 13,
|
| 164 |
+
"batch": {
|
| 165 |
+
"batch_id": 14,
|
| 166 |
+
"sealed_at": "2025-11-22T19:17:01Z",
|
| 167 |
+
"merkle_root": "d0cdee06632777e094da1d6b6b5c7cfcb3af86daf546d915ad5bf39cfb2bbf47",
|
| 168 |
+
"entry_count": 10000,
|
| 169 |
+
"signature": "kozzJR+ykNTUesygy/tTXv8BTcQOY1CcmHORp2n+E5v6drSj2cy2NvEzpY1QW3LykTq+CR2h5faULv5h3Ntt54evmZJ91ovoXDI1JQgRw/TzH7OLs9mf2oWoNSZvUrKmduav+42upIIvg9OT0n5oBuiRspAtyZG4oNS/Jv8JddYCwXADSQrEUyZG5muhBkWiRP11Hx8X344i05k/s41zT9j+IUO+qH9Y/2wFY4NelB80sI4A3UgkaZzn1i0q5FrPv3pQ0vYdw8HllGAUHtLz8Qxt7nMpoSOCAFlIeInL++4z4orJLPIkWx2dmy1FOxq1OKU2Vh9U2SXZXau9NBZ7Iw=="
|
| 170 |
+
},
|
| 171 |
+
"prev_block_hash": "de2238a9ce0f32bea61e30e9e8f1983056fa150f6e39395372c7c914e87b07e3",
|
| 172 |
+
"block_hash": "c178ae13d6fffd822a63122c6ec07e871879306a2726ad358ac7aba9eef4340a",
|
| 173 |
+
"created_at": "2025-11-22T19:17:01Z"
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"index": 14,
|
| 177 |
+
"batch": {
|
| 178 |
+
"batch_id": 15,
|
| 179 |
+
"sealed_at": "2025-11-22T19:17:01Z",
|
| 180 |
+
"merkle_root": "4d82fc97795ba932b1f271ed918ac4f5965a339787407453829518517987cd64",
|
| 181 |
+
"entry_count": 10000,
|
| 182 |
+
"signature": "KJvIz2DgoPMP5eMvz3GA2RLx6c45ZMnke8rTAFlIk/AW4qjGZZmA3BcIzAVqXrbaHnAi6JrHbTCrY1+g0DwBCU22GDp/heHcGoqXtZ06JPra2rbGHmznGK8847L9SsBca+wQ3J/nXBKWoMTT7PE2mewCJVf+7CJf0c+mwvHOhdy6Ll+DuKVmRVNlZnWow10uSXOUxmUYDdotkXgM4ZlY9/T3y5cyk3TIh3nloYD1J1ZHzMnoSY3+GX+t/cVqgEA5CkNY7vO/TKIC7EQtZow8+CwKRpCQOWyIzyJOOU1T+c8sYvRmlxhdP3YHZy+vLitLqxVDKM0kJgPrNU4Wzivv/Q=="
|
| 183 |
+
},
|
| 184 |
+
"prev_block_hash": "c178ae13d6fffd822a63122c6ec07e871879306a2726ad358ac7aba9eef4340a",
|
| 185 |
+
"block_hash": "2525069c543260f8f208a0ef1a423fe7c36b8cffc2feab78c3124797503e51a1",
|
| 186 |
+
"created_at": "2025-11-22T19:17:01Z"
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"index": 15,
|
| 190 |
+
"batch": {
|
| 191 |
+
"batch_id": 16,
|
| 192 |
+
"sealed_at": "2025-11-22T19:17:01Z",
|
| 193 |
+
"merkle_root": "8ef66e722ae0aad304a527c900513d9fdb1be47ff3351b01b89b8709cb282a05",
|
| 194 |
+
"entry_count": 10000,
|
| 195 |
+
"signature": "Wi7N9XxVk+WFC0qDetxSoVvg0JzhurngKftKhIDvrEglLPZwYfZZbLP4I9vfgjsGJandxoVu7/jswvciBY8u7eYMccsHHXejwLvOl9eegRlGaPEFvfj+XK1sk/8wDZfNkp1B9h59qwVFkHv7XurPxA67jQ0XLR/wWRxlo3QGZzA59w9qY/JfY45vPfm8Yic0D+aePH45r/FGPjzJNy6GneJbS8+AXEY/9vbz0iQm1QwR5AhgKiy4PmOcpuQQ1qHpHVOmT3XROtg673opWRjW5RsE4RtDURgSWiiYsbPm94WEuXQ5Zy+ldl0RmOtWX8IAuuE2OAXqRP09NNVyUC9XTA=="
|
| 196 |
+
},
|
| 197 |
+
"prev_block_hash": "2525069c543260f8f208a0ef1a423fe7c36b8cffc2feab78c3124797503e51a1",
|
| 198 |
+
"block_hash": "583fc2a41a94aea3bf5a696f85e797e5153fbcda5bf412f6c27a437809424ea5",
|
| 199 |
+
"created_at": "2025-11-22T19:17:01Z"
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"index": 16,
|
| 203 |
+
"batch": {
|
| 204 |
+
"batch_id": 17,
|
| 205 |
+
"sealed_at": "2025-11-22T19:17:02Z",
|
| 206 |
+
"merkle_root": "dd429836c08376bed6e9425d9b149fd270efa31732ddfc34b7077ce63e996003",
|
| 207 |
+
"entry_count": 10000,
|
| 208 |
+
"signature": "j+da5VuJpW6YfISCQzABBPZl9RyJrwqxv0LcLLPfen+2eloiMuudghqTDX1QhP3D//98YHdahqS3vNDALBV24nppH3bo7f/+Vl94xsK5t0tbloT4KOlV2DAppbGY+bRAirswB7wQwbsBcklZBezLePKsw3yWYygdH37f382Up3W+OUza2O0EjA/Lk289LDsha1yHKEspHVWglsQ6huqxtX28F/K/tc+CrfIVXkhhRojhP9jJsPwQ6nOF/eCqWDmUMIDK5/3K6UC0k7UD/iRgc1MzDGzrFkK9+GNR6eCh2xj+xKcNXgiq+w213nI7clRYX2U1lyVMwUxsrKEpyjLE0A=="
|
| 209 |
+
},
|
| 210 |
+
"prev_block_hash": "583fc2a41a94aea3bf5a696f85e797e5153fbcda5bf412f6c27a437809424ea5",
|
| 211 |
+
"block_hash": "abe5dbb5abafbd3704f8fa1bf394967eab912766fe8206fc811a52b86cba729d",
|
| 212 |
+
"created_at": "2025-11-22T19:17:02Z"
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"index": 17,
|
| 216 |
+
"batch": {
|
| 217 |
+
"batch_id": 18,
|
| 218 |
+
"sealed_at": "2025-11-22T19:17:02Z",
|
| 219 |
+
"merkle_root": "5663054ef7f12bd5ed66cac5ce8ad974552f71fd8e39f179efbdd0047fef2769",
|
| 220 |
+
"entry_count": 10000,
|
| 221 |
+
"signature": "XBIT+qZxRYGkDmBCBhSAGvDr/FPW/2PeAfTll66hB13q0E8YW5KrT/LvZSN9SXPvoe07cTcMCYN/LRiGh101CCAkGdAf/ZX/AqStOmLjwA7MoFOyDwooLUVZ8vh+96vgQPdYU1mqW5a6+ZFV7p1+xxaQ1Bf22K0sHVUY5BMdLsqxACWAOIM+/Q3TwWlEisaYA/WT0E1P3D4BdaylrhxluODU8o6nWwFM+c/rJpb0e+Vie3TFlPqYdWMYnTQhPpYJbPMLwuumLh5tx0yNarvPADfGPlbVnLCEt+szuV3KkGuDB3wJz7Pd3ByNq6PXtPguNdf++wCdX7eY/7sl8t8aHQ=="
|
| 222 |
+
},
|
| 223 |
+
"prev_block_hash": "abe5dbb5abafbd3704f8fa1bf394967eab912766fe8206fc811a52b86cba729d",
|
| 224 |
+
"block_hash": "0595e6027a5e2fbaec115c75d22f850689102ac45ff0ea19cfbc1d7a0b1e60af",
|
| 225 |
+
"created_at": "2025-11-22T19:17:02Z"
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"index": 18,
|
| 229 |
+
"batch": {
|
| 230 |
+
"batch_id": 19,
|
| 231 |
+
"sealed_at": "2025-11-22T19:17:02Z",
|
| 232 |
+
"merkle_root": "b97f2490f8e743851dae9aa89fd797e6ce88a643b22e2870bee5ee20f3ebfcda",
|
| 233 |
+
"entry_count": 10000,
|
| 234 |
+
"signature": "YfGjfWbWkUq0e5nE2wpW6xVqSFpQ+OGDdbnAy9d+2bEmQxEVO9/Yl/HxCPNFBgBuXKqBVMEACe9y/zOvo+SRL2lpW4EcMu5rMxOF2I6JQziWdfvF9GsJ4+ci9zy9BHj/lWYrlb1y6IVQqhnSIjB9Q5GH1gccguSLD5d4xBcTHa8XUHwiHlst+4+vV2dGnCKBJLO14EupV+1KTSMfSc1N49qDxEvVmkxtFalkqhhsZV2Dm0XqZ1ZUebWY2eZRdhYL0FxuumGMU6T5AuMfmg87EB94DXRCIT9Kr5JV/WICqaq6ucuIFOnS9fDwG+obThpsc0+9jG/elgZsZANenOSb4A=="
|
| 235 |
+
},
|
| 236 |
+
"prev_block_hash": "0595e6027a5e2fbaec115c75d22f850689102ac45ff0ea19cfbc1d7a0b1e60af",
|
| 237 |
+
"block_hash": "75f0a6b245f6c9f9e6679325c051ef5fc3ae42f571c901f18a9a40db27b9c5bb",
|
| 238 |
+
"created_at": "2025-11-22T19:17:02Z"
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"index": 19,
|
| 242 |
+
"batch": {
|
| 243 |
+
"batch_id": 20,
|
| 244 |
+
"sealed_at": "2025-11-22T19:17:03Z",
|
| 245 |
+
"merkle_root": "7a3a09ac28fc14bf15e9f55fb6a188ba144593d14830b45e6b841a56f32f9a2e",
|
| 246 |
+
"entry_count": 10000,
|
| 247 |
+
"signature": "k0OyqOP5Qt2eiATYDKhiPg+JMLf/qOkfTTVBkqYkQF4bk34W6+rPeyhmKHzCmPV2EfNhf7NVJyM5VFSgjFpY4oYuq+mqx47JxtH9cq/kAT1Ua9uM8FwhgQWmTgxwLtyE1S//qP5ARjHdpY2SDZo5/IssFKX3QAiuW1ehv3lMnpMhPMTZgqrPHI1m5EIwTsaiL/KalBnmYIIhFaYvdfjKYirXb/Tmemw2GYmN3EUm4JJULoc6QKhs681fr3UK+C/rzq8SPAUVn0LsROVbO5NJuROa39vXMcovkt/XIO0eZU7nZOZ2ZTd8xZ4jcn6QnISMWV37/3wJ3ASfvjtJDR6Udg=="
|
| 248 |
+
},
|
| 249 |
+
"prev_block_hash": "75f0a6b245f6c9f9e6679325c051ef5fc3ae42f571c901f18a9a40db27b9c5bb",
|
| 250 |
+
"block_hash": "a7de9e1275cc4736cf92933da9e2d9eac0c20d4dd5d6cee5631e321e70581a73",
|
| 251 |
+
"created_at": "2025-11-22T19:17:03Z"
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
"index": 20,
|
| 255 |
+
"batch": {
|
| 256 |
+
"batch_id": 21,
|
| 257 |
+
"sealed_at": "2025-11-22T19:17:03Z",
|
| 258 |
+
"merkle_root": "4f92611d565d047d4e2cb41e767ffcafe939a08172342176d0cb44e5c6e79b5b",
|
| 259 |
+
"entry_count": 10000,
|
| 260 |
+
"signature": "Tl6nkPucuZaPzQQsaro6IJuprVcCYouDz0caZI60IRtkHtcPs2XAkxRh4kkfubvYAFnW0ootbbffM3ym1dMB3XopivTqwWn8fvOSomxxhVW/PNSJR8Czt1uVgWLCefLgDbNj+GsGxTS5iriWT2h/KiFHIxD2d9OUcGj4lLEOyuVbdsWWgHG48b3OZkfavRxqTmofbpPoP2tTkV+DA1hn9mgk/qH3+6VKQAcIdvXdpISaBSxUqoJLFfx0YUkpO4bjMf3EFFXHRkuPTdWeimNtgTD/lqMV0EL7EMiijBiipS6ySJyxUNi7OKwNm3cyCKAK1In75XrBwo8ABckE/ym9fA=="
|
| 261 |
+
},
|
| 262 |
+
"prev_block_hash": "a7de9e1275cc4736cf92933da9e2d9eac0c20d4dd5d6cee5631e321e70581a73",
|
| 263 |
+
"block_hash": "4ff895d62dced30f43ca3461607b4d644c345f55dcab3e7b439369290c7bad0c",
|
| 264 |
+
"created_at": "2025-11-22T19:17:03Z"
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"index": 21,
|
| 268 |
+
"batch": {
|
| 269 |
+
"batch_id": 22,
|
| 270 |
+
"sealed_at": "2025-11-22T19:17:03Z",
|
| 271 |
+
"merkle_root": "253fd455b73d3d0afd62f034f7b0e1b62cf73265377ef874c182a783ae1d1d19",
|
| 272 |
+
"entry_count": 10000,
|
| 273 |
+
"signature": "ouPGDamyoFCenaEyGKY7HGH3BfJBNBJM4jPVyJkHZ1/Z06ONlLvvwov4T36qe8IDkX4gyMF08O0Eo8ijhpwHHBhtKGD/FMboXf9EJaQLak5QkP0/1Ibo4GNBcHkT8NzlAUCLagI+y6zAZyr37uOcJXziWJHaDz1sV06/WDFLG0NKSpz0+T46C23fujeDdrm+Y/Safcu8MIR67BZDma8d6ALusRlM+IXvqciyd/JfaCMYumtRaQayGetEhyFIus+UYLDRqo7s+v8Y4nJvqNaO6DBrrNv8QN9WNExLroS3f4Mob7gQY3AXJqFg8K9Vyg+EUc5eT8cth6F05sWRYZHlBw=="
|
| 274 |
+
},
|
| 275 |
+
"prev_block_hash": "4ff895d62dced30f43ca3461607b4d644c345f55dcab3e7b439369290c7bad0c",
|
| 276 |
+
"block_hash": "61ba62b8d5192635792183e6004eb8bf7b8cc30af5f440f24c3db83d2aa2874e",
|
| 277 |
+
"created_at": "2025-11-22T19:17:03Z"
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"index": 22,
|
| 281 |
+
"batch": {
|
| 282 |
+
"batch_id": 23,
|
| 283 |
+
"sealed_at": "2025-11-22T19:17:04Z",
|
| 284 |
+
"merkle_root": "39dc01050c07e6812b36572f0c17183b6bd6ede6ce126108e6e2e19b33458bd8",
|
| 285 |
+
"entry_count": 10000,
|
| 286 |
+
"signature": "WOqnfXvK/tvosnh0QDFubBp8jatWfDo2rO9PCnjDQjXu4vJEoCJitGaVkoADTV5l/dNoTgXHdzE9HmsCu8uC7W7AyybKpf6gX9IPXeyT/WTLQBPIerXfzFZMyRDFwF4Rd5E6DQhftjrruvFTZJjKcFVBEDBVGQiyqCoab7fTzLbUSkk6NY9AEun241bdS4LI3LR73y6f4nDqHDaxagcDPQ8XpILMnI89yCozqstHbyY7IITxlMtO2CkKxCzYuYpT2Ju06dVbQvTBoYotWNPPUBQpafsSkY83JRdhK4lTgzzjX4WHwUbgOogwz/7bn6YD842YbGQ5zZIlBSL2RlyIKw=="
|
| 287 |
+
},
|
| 288 |
+
"prev_block_hash": "61ba62b8d5192635792183e6004eb8bf7b8cc30af5f440f24c3db83d2aa2874e",
|
| 289 |
+
"block_hash": "e14fe45413eb9cf5b094bc739e0bba0806a867c6ec4acb301caf2a4f35978a11",
|
| 290 |
+
"created_at": "2025-11-22T19:17:04Z"
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
"index": 23,
|
| 294 |
+
"batch": {
|
| 295 |
+
"batch_id": 24,
|
| 296 |
+
"sealed_at": "2025-11-22T19:17:04Z",
|
| 297 |
+
"merkle_root": "1eef7d2939cdab5e8f1754f0d16d2c200fbeb63ba53a6c18ee04bad5fde7224e",
|
| 298 |
+
"entry_count": 10000,
|
| 299 |
+
"signature": "eVH7g6Enoeqgo9k/t8b19djauyzbD3Ya1qftgydD/cu2Z9r0R4CJHpHV1K6e2sCjMoGyf2Btfmw+nXHRrE5pb7sNISIR2lNJBntlmTmiYlS1Qy38U13Lm7Y41ceRzTCkS7+zGot3gehJ6IcaSWJeWA7nUHjxC+20AdEbuIyaAp5DLzTl9hSDv3y5U+8pvKAD+pyUpgPgIik/Y9S3w8LfVMNXHcRgPWQKela8LnNNz9geuJ8tGAp4MsQmhKX0bRCHhq5gGVad8on57CS4AIdMfbWNWasCPDl9Zm4WYgmRaMS6qdd99WbcBkubzHnMcei/cp5Xk1ghZnCFBf/yAfFgng=="
|
| 300 |
+
},
|
| 301 |
+
"prev_block_hash": "e14fe45413eb9cf5b094bc739e0bba0806a867c6ec4acb301caf2a4f35978a11",
|
| 302 |
+
"block_hash": "01052c7193fedcf1f9f8fb2932699e9620cfe0485f30200fadb0c241e81f5db5",
|
| 303 |
+
"created_at": "2025-11-22T19:17:04Z"
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"index": 24,
|
| 307 |
+
"batch": {
|
| 308 |
+
"batch_id": 25,
|
| 309 |
+
"sealed_at": "2025-11-22T19:17:04Z",
|
| 310 |
+
"merkle_root": "f2db360b97eee849554925281f986af7deae46ca73289fb9ba2e76b22a01be6e",
|
| 311 |
+
"entry_count": 10000,
|
| 312 |
+
"signature": "W0rKbXjW3cvStfpjsmKo98zJRJwKS6b5bfdEYw2w27i5/70ex4CXbTuVg7A5VcAcS6OXONihGBn3QFUV8j4JZlIpnJJVsfAnhbg0bbaJgr1s98pKF0Lx7vZfimVX/a5wbdLmOY655kETcQxASyVUIGCb/8sOAqb/rMcZBpfnBYX2jNScXL+Cp89Hh9WVqng/YG6KWNnZacJJMxfnId91ohIV3EuOjrJB4xCA6Ohy+Pnp8Hy7OP4UqYBP39S3rulFrUGu6tiOlo3H1WGz6e75RCHeX6ovFKJIiUuxJDJNktk8/rNV8x5vQJijwyvhUDwXaWzySMbm4rLufrZtAS/tLg=="
|
| 313 |
+
},
|
| 314 |
+
"prev_block_hash": "01052c7193fedcf1f9f8fb2932699e9620cfe0485f30200fadb0c241e81f5db5",
|
| 315 |
+
"block_hash": "4a10e731d51d2d8f19da1da6f3a0a7181c9bf0665f41f3cce28f21220a8800a1",
|
| 316 |
+
"created_at": "2025-11-22T19:17:04Z"
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
"index": 25,
|
| 320 |
+
"batch": {
|
| 321 |
+
"batch_id": 26,
|
| 322 |
+
"sealed_at": "2025-11-22T19:17:05Z",
|
| 323 |
+
"merkle_root": "4c8b0541e6658c76a8847640ceab221c09e3d4e4e2e122cfe2dfb334d7dc5141",
|
| 324 |
+
"entry_count": 10000,
|
| 325 |
+
"signature": "RdS+AvjSUovp6EtbQuzSniWW42UeAymTP8mS2+YGvAOPb9gJAzeNRzZomD8z+wG3JKAQ+IS/qPTTMPI0boVMgDSOrsTcfBuRs723hvvCO/r0j+vMd7oSSuQCcpVHgRgU9n/YazioNZqnXWZ70leTyxaJTe3HSrA55FAYkuqD2q4bjTWbeFXcAI7uwJjwRYwTQNTTrIWXqCsyPRTNKRKhCFMCv2GXUZTPCdLeOKadjZWKKSEJLYPz7DUa1ibjtMGYHA0HqK9hnJJVpn8OckJNo1znXuOJY+h0mAy+Tz8jK45DIRMIBvuNeXtyVLMDbMrcrx1mqBhJhm32zJeb7CvIIw=="
|
| 326 |
+
},
|
| 327 |
+
"prev_block_hash": "4a10e731d51d2d8f19da1da6f3a0a7181c9bf0665f41f3cce28f21220a8800a1",
|
| 328 |
+
"block_hash": "a02a76c97cdee3338e011c75eb812e417d8ab8deecd337bc63dc1d0cab7c3279",
|
| 329 |
+
"created_at": "2025-11-22T19:17:05Z"
|
| 330 |
+
},
|
| 331 |
+
{
|
| 332 |
+
"index": 26,
|
| 333 |
+
"batch": {
|
| 334 |
+
"batch_id": 27,
|
| 335 |
+
"sealed_at": "2025-11-22T19:17:05Z",
|
| 336 |
+
"merkle_root": "5b1ad6a115f9edf1bc992160d212b502e3f35472fb4c306c1d21e1f54e73f782",
|
| 337 |
+
"entry_count": 10000,
|
| 338 |
+
"signature": "bsFUkfrstgZSPdw1a380HpkI13gpUP2gzli4N3SWAOhm7D7uVd4fUfRCESgrvNueI1QDXX0Vq3NEl1RRSMS2DriRiLx5UafStgtwX4wJOWh0TbTFAY4BGELU1H++36xysBMnjsIIsE8AeDE36TrrXKr/SfvgkRjIv7IVJKmbNXmId0SchesHZv0dbIinwLbGpkv1noCj+inlRhJ9LJJHV8pJsljNdTV3U+Kuea6fx1zZJPeqfioGIL941d5GLnksFZ3P9nbz9tQy9LN3pMD6I2CdoZNm5D7ESPXN7jUVz06pMSuISa/O+Ndz3k4GUqNFnYDhR+ANxYfEh7pBZJhgaw=="
|
| 339 |
+
},
|
| 340 |
+
"prev_block_hash": "a02a76c97cdee3338e011c75eb812e417d8ab8deecd337bc63dc1d0cab7c3279",
|
| 341 |
+
"block_hash": "0e43b0b3e902f69eadb068e53243298a5229155f0ecb76e0dda6d6ddc3516e11",
|
| 342 |
+
"created_at": "2025-11-22T19:17:05Z"
|
| 343 |
+
},
|
| 344 |
+
{
|
| 345 |
+
"index": 27,
|
| 346 |
+
"batch": {
|
| 347 |
+
"batch_id": 28,
|
| 348 |
+
"sealed_at": "2025-11-22T19:17:05Z",
|
| 349 |
+
"merkle_root": "63231a181eb5585ef114d3c076d78977dfe49941b8ee918d5ea83406084d7b66",
|
| 350 |
+
"entry_count": 10000,
|
| 351 |
+
"signature": "B5fA65MbJGGYBrNb9Wdxrwe6yUkRAw3aATCKuwG+O2TMdiQGhY2i2OC/umnFryYGsjRL8ccV4BZ1pLginSTpL+HkDDaZ3HJeXSDZVT2nS7mkz+z33b4YP2YJwVsBvVaQNWQaKYf52vzBQDSF5Ts40X+MGyUzqF581hBJnOlgxq0RqK1xDUGjyuiOUbeqiXviJN/Ik0eJ8EN/6QoEpZs80E6l8bq3FyqBRIiAVBMd1T8VILtT8cqK8Oeq2Upb/Eh0M72G9MSqFN3V4kG4Y78vFApHMlNbcnmtXGZJ5FnbyYMLG7aqsq3wFlbi2+4lj0eeAIwhg+5gVp2z27iQoy08DQ=="
|
| 352 |
+
},
|
| 353 |
+
"prev_block_hash": "0e43b0b3e902f69eadb068e53243298a5229155f0ecb76e0dda6d6ddc3516e11",
|
| 354 |
+
"block_hash": "a4bb71d882bdbbc97aca798903d0c30cead1078b9810162b33ea06a646167be3",
|
| 355 |
+
"created_at": "2025-11-22T19:17:05Z"
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
"index": 28,
|
| 359 |
+
"batch": {
|
| 360 |
+
"batch_id": 29,
|
| 361 |
+
"sealed_at": "2025-11-22T19:17:06Z",
|
| 362 |
+
"merkle_root": "894cac81366f977d3e57e13c2d4c97be976758ecdd160c3553f158a7bf25d6b6",
|
| 363 |
+
"entry_count": 10000,
|
| 364 |
+
"signature": "WSQ1KTMh1FYJexPLaRX/3rEl0Yh51HI8QyeAD+FM8I6Gijv2bZu6OR84+ElfxqzuYb6e+c+mO6VarS0GVUdBu8+bCEyEkvmr+9rs2RQ8iTcMokLFXAHEUAB8KdsB0Nu6iyIc/cuFVnlshBYIxDXmgLcyHVQkAQGUjPNDMFF8ZOl/cvmPiP02LNfkuGqEiQqL4soSqwti9APKI92lO/FmI/gbvQ3BoDSeZ7gPXzEQmXlzGl5Npqqo2dwvg+OWQ5tiEGFDg8Xwr+UzFyrLGB/XoFVNgU4G+rx4VLKPt7dBNBHk1jNW5oAl4o4MvaGhPlbkqHlKJEStqH3QLG+/ioxx6w=="
|
| 365 |
+
},
|
| 366 |
+
"prev_block_hash": "a4bb71d882bdbbc97aca798903d0c30cead1078b9810162b33ea06a646167be3",
|
| 367 |
+
"block_hash": "eea819511c1ce3cf3e43e209d90d581b09985c1691344f288ed420d3a1c5c1e1",
|
| 368 |
+
"created_at": "2025-11-22T19:17:06Z"
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"index": 29,
|
| 372 |
+
"batch": {
|
| 373 |
+
"batch_id": 30,
|
| 374 |
+
"sealed_at": "2025-11-22T19:17:06Z",
|
| 375 |
+
"merkle_root": "c870566a387d06dfd75f2d78dd67e016233da1588cfcaebe51925109a1c13930",
|
| 376 |
+
"entry_count": 10000,
|
| 377 |
+
"signature": "HxYqKt6P+H3vC8HMwdO5nMoL8tPU+LMMRXkVAbLJ0UKprZPI+M/hcUxA18qd4yhWORO0NIsorkFm41HEqBhNJOl0k7SNZLIxgCRY0bRprfEsgldHlILjv4ItUa59qpXh+ZejbnK2dfe/t9GShgwf+bSrcullWc6M7woAIE4fLnC0XbtoyUjVPwh17Xbg2HCn1Bsv/2KuLT/pYrOg9wSvubUj2xedNTsBb2cMxYTCmeL4+aD/as0EcXg30ORLiG9Wfq54tUGD1SyTOlkd60lm3afVuDLbLURDxAJQ+1mYa9hHgQD5YraU37ZpZXc5O8ZWe8Mc9DQ1iZbYtMzAJ6yYtg=="
|
| 378 |
+
},
|
| 379 |
+
"prev_block_hash": "eea819511c1ce3cf3e43e209d90d581b09985c1691344f288ed420d3a1c5c1e1",
|
| 380 |
+
"block_hash": "62965f0f9385c18691826f11315a82394da1355200ce3edb99a2a1472e5f8583",
|
| 381 |
+
"created_at": "2025-11-22T19:17:06Z"
|
| 382 |
+
}
|
| 383 |
+
],
|
| 384 |
+
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApce+84VPBf4npojIggpk\nEQY59UizBU0Aitm6a2W9CBY46j40mGsqoljzgsqxcfHyz0P5i7lP4j0e/G5ihl0D\nkvyYfxyGbP9XRRl60qBEyJsqHCeSid7bTa8P28Hw4lqEbI1nTOldo4PSzpoQ61/K\nztym/Ts2tDSEjp2H6vqZFzBrqbhYszB9IVfXyvS/V3bUfbfYrJnmrLYkePWrzvIF\nOkQm3Dw/Ct19nsdtrKjRFFDzoDfkGV3n0h3Q+r3WQ3BksYzRI7lAGpGzMhuk67mw\nTm6pD7jrYU3Uh+ZTs0Ulb6qo2Y+BcrNy0HlNW8fdb42Gr7PAXvN/LDIUKk48glst\nLQIDAQAB\n-----END PUBLIC KEY-----\n"
|
| 385 |
+
}
|
frontend/dashboard.py
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
import time
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import requests
|
| 9 |
+
import json
|
| 10 |
+
import base64
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
# API Configuration
|
| 14 |
+
API_URL = "http://localhost:8000"
|
| 15 |
+
|
| 16 |
+
# Page config
|
| 17 |
+
st.set_page_config(
|
| 18 |
+
page_title="Network IDS Dashboard",
|
| 19 |
+
page_icon="🛡️",
|
| 20 |
+
layout="wide",
|
| 21 |
+
initial_sidebar_state="expanded"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Custom CSS
|
| 25 |
+
st.markdown("""
|
| 26 |
+
<style>
|
| 27 |
+
.threat-high { background-color: #ff4444; color: white; padding: 5px 10px; border-radius: 5px; }
|
| 28 |
+
.threat-medium { background-color: #ff8800; color: white; padding: 5px 10px; border-radius: 5px; }
|
| 29 |
+
.threat-low { background-color: #ffbb33; color: white; padding: 5px 10px; border-radius: 5px; }
|
| 30 |
+
.benign { background-color: #00C851; color: white; padding: 5px 10px; border-radius: 5px; }
|
| 31 |
+
</style>
|
| 32 |
+
""", unsafe_allow_html=True)
|
| 33 |
+
|
| 34 |
+
# Load test data
|
| 35 |
+
@st.cache_data
|
| 36 |
+
def load_data():
|
| 37 |
+
# Try to find data relative to this file
|
| 38 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 39 |
+
# Expected: backend/frontend/dashboard.py -> backend/data/test.csv
|
| 40 |
+
data_path = os.path.join(current_dir, '..', 'data', 'test.csv')
|
| 41 |
+
|
| 42 |
+
if not os.path.exists(data_path):
|
| 43 |
+
st.error(f"Data file not found at {data_path}")
|
| 44 |
+
return pd.DataFrame(), data_path
|
| 45 |
+
|
| 46 |
+
# Limit to 100,000 rows as requested
|
| 47 |
+
df = pd.read_csv(data_path, nrows=100000)
|
| 48 |
+
return df, data_path
|
| 49 |
+
|
| 50 |
+
# API Helper Functions
|
| 51 |
+
def api_predict_flow(features):
|
| 52 |
+
try:
|
| 53 |
+
# Ensure features are native Python types (float) not numpy types
|
| 54 |
+
# Handle None values which might come from backend NaNs
|
| 55 |
+
clean_features = {k: float(v) if v is not None else 0.0 for k, v in features.items()}
|
| 56 |
+
response = requests.post(f"{API_URL}/predict/", json={"features": clean_features})
|
| 57 |
+
if response.status_code == 200:
|
| 58 |
+
return response.json()
|
| 59 |
+
else:
|
| 60 |
+
st.error(f"Prediction failed: {response.text}")
|
| 61 |
+
return None
|
| 62 |
+
except Exception as e:
|
| 63 |
+
st.error(f"API Error: {e}")
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
def api_batch_predict(features_list):
|
| 67 |
+
try:
|
| 68 |
+
# Limit batch size to avoid timeouts if necessary, or handle in chunks
|
| 69 |
+
# For this demo, we'll send chunks of 1000
|
| 70 |
+
results = []
|
| 71 |
+
chunk_size = 1000
|
| 72 |
+
|
| 73 |
+
progress_bar = st.progress(0)
|
| 74 |
+
total = len(features_list)
|
| 75 |
+
|
| 76 |
+
for i in range(0, total, chunk_size):
|
| 77 |
+
chunk = features_list[i:i+chunk_size]
|
| 78 |
+
# Convert to list of dicts with native types
|
| 79 |
+
clean_chunk = [{k: float(v) if v is not None else 0.0 for k, v in item.items()} for item in chunk]
|
| 80 |
+
|
| 81 |
+
response = requests.post(f"{API_URL}/predict/batch", json={"items": clean_chunk})
|
| 82 |
+
if response.status_code == 200:
|
| 83 |
+
results.extend(response.json()['results'])
|
| 84 |
+
else:
|
| 85 |
+
st.error(f"Batch prediction failed: {response.text}")
|
| 86 |
+
return []
|
| 87 |
+
|
| 88 |
+
progress_bar.progress(min((i + chunk_size) / total, 1.0))
|
| 89 |
+
|
| 90 |
+
progress_bar.empty()
|
| 91 |
+
return results
|
| 92 |
+
except Exception as e:
|
| 93 |
+
st.error(f"API Error: {e}")
|
| 94 |
+
return []
|
| 95 |
+
|
| 96 |
+
def api_generate_report(attack_summary, classification_report, threat_statistics):
|
| 97 |
+
try:
|
| 98 |
+
payload = {
|
| 99 |
+
"attack_summary": attack_summary,
|
| 100 |
+
"classification_report": classification_report,
|
| 101 |
+
"threat_statistics": threat_statistics
|
| 102 |
+
}
|
| 103 |
+
response = requests.post(f"{API_URL}/reports/generate", json=payload)
|
| 104 |
+
if response.status_code == 200:
|
| 105 |
+
return response.json().get("report"), None
|
| 106 |
+
else:
|
| 107 |
+
return None, f"API Error: {response.text}"
|
| 108 |
+
except Exception as e:
|
| 109 |
+
return None, str(e)
|
| 110 |
+
|
| 111 |
+
def api_get_monitor_flow(index):
|
| 112 |
+
try:
|
| 113 |
+
response = requests.get(f"{API_URL}/monitor/next/{index}")
|
| 114 |
+
if response.status_code == 200:
|
| 115 |
+
return response.json()
|
| 116 |
+
else:
|
| 117 |
+
st.error(f"Backend Error ({response.status_code}): {response.text}")
|
| 118 |
+
return None
|
| 119 |
+
except Exception as e:
|
| 120 |
+
st.error(f"Connection Error: {e}")
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
def api_get_feature_importance():
|
| 124 |
+
try:
|
| 125 |
+
response = requests.get(f"{API_URL}/predict/feature-importance")
|
| 126 |
+
if response.status_code == 200:
|
| 127 |
+
return response.json()
|
| 128 |
+
return None
|
| 129 |
+
except:
|
| 130 |
+
return None
|
| 131 |
+
|
| 132 |
+
# Initialize session state
|
| 133 |
+
if 'predictions' not in st.session_state:
|
| 134 |
+
st.session_state.predictions = [] # List of result dicts
|
| 135 |
+
if 'monitoring' not in st.session_state:
|
| 136 |
+
st.session_state.monitoring = False
|
| 137 |
+
if 'current_index' not in st.session_state:
|
| 138 |
+
st.session_state.current_index = 0
|
| 139 |
+
if 'threat_log' not in st.session_state:
|
| 140 |
+
st.session_state.threat_log = []
|
| 141 |
+
|
| 142 |
+
# Sidebar
|
| 143 |
+
st.sidebar.title("🛡️ Network IDS Control")
|
| 144 |
+
page = st.sidebar.radio("Navigation",
|
| 145 |
+
["📊 Dashboard", "🔴 Live Monitor", "📈 Analytics", "🎯 Manual Prediction"])
|
| 146 |
+
|
| 147 |
+
if st.sidebar.button("🗑️ Clear Cache & Reset"):
|
| 148 |
+
st.cache_data.clear()
|
| 149 |
+
st.session_state.predictions = []
|
| 150 |
+
st.session_state.threat_log = []
|
| 151 |
+
st.rerun()
|
| 152 |
+
|
| 153 |
+
df, data_path = load_data()
|
| 154 |
+
st.sidebar.markdown("---")
|
| 155 |
+
st.sidebar.success(f"📂 **Data Source:**\n`{os.path.abspath(data_path)}`")
|
| 156 |
+
st.sidebar.info(f"📊 **Loaded Flows:** {len(df):,}")
|
| 157 |
+
|
| 158 |
+
if df.empty:
|
| 159 |
+
st.stop()
|
| 160 |
+
|
| 161 |
+
# Prepare feature columns (exclude labels)
|
| 162 |
+
feature_cols = [col for col in df.columns if col not in ['Attack_type', 'Attack_encode']]
|
| 163 |
+
|
| 164 |
+
# Main Dashboard
|
| 165 |
+
if page == "📊 Dashboard":
|
| 166 |
+
st.title("🛡️ Network Intrusion Detection System")
|
| 167 |
+
st.markdown("---")
|
| 168 |
+
|
| 169 |
+
# Run batch prediction if not done
|
| 170 |
+
if len(st.session_state.predictions) == 0:
|
| 171 |
+
st.info(f"Starting analysis on full dataset ({len(df):,} flows). This may take a while...")
|
| 172 |
+
|
| 173 |
+
results = []
|
| 174 |
+
chunk_size = 2000
|
| 175 |
+
total_rows = len(df)
|
| 176 |
+
progress_bar = st.progress(0)
|
| 177 |
+
status_text = st.empty()
|
| 178 |
+
|
| 179 |
+
start_time = time.time()
|
| 180 |
+
|
| 181 |
+
# Process full dataset in chunks to manage memory and avoid timeouts
|
| 182 |
+
for start_idx in range(0, total_rows, chunk_size):
|
| 183 |
+
end_idx = min(start_idx + chunk_size, total_rows)
|
| 184 |
+
|
| 185 |
+
# Slice and convert only this chunk
|
| 186 |
+
chunk_df = df.iloc[start_idx:end_idx]
|
| 187 |
+
chunk_features = chunk_df[feature_cols].to_dict(orient='records')
|
| 188 |
+
|
| 189 |
+
# Clean features (handle NaNs/None)
|
| 190 |
+
clean_chunk = [{k: float(v) if pd.notnull(v) else 0.0 for k, v in item.items()} for item in chunk_features]
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
response = requests.post(f"{API_URL}/predict/batch", json={"items": clean_chunk})
|
| 194 |
+
if response.status_code == 200:
|
| 195 |
+
results.extend(response.json()['results'])
|
| 196 |
+
else:
|
| 197 |
+
st.error(f"Batch failed at index {start_idx}: {response.text}")
|
| 198 |
+
# Continue or break? Breaking is safer to avoid cascading errors
|
| 199 |
+
break
|
| 200 |
+
except Exception as e:
|
| 201 |
+
st.error(f"Connection error at index {start_idx}: {e}")
|
| 202 |
+
break
|
| 203 |
+
|
| 204 |
+
# Update progress
|
| 205 |
+
progress = end_idx / total_rows
|
| 206 |
+
progress_bar.progress(progress)
|
| 207 |
+
|
| 208 |
+
# Calculate ETA
|
| 209 |
+
elapsed = time.time() - start_time
|
| 210 |
+
if elapsed > 0:
|
| 211 |
+
rate = end_idx / elapsed
|
| 212 |
+
remaining = (total_rows - end_idx) / rate
|
| 213 |
+
status_text.caption(f"Processed {end_idx:,}/{total_rows:,} flows ({rate:.0f} flows/s). Est. remaining: {remaining:.0f}s")
|
| 214 |
+
|
| 215 |
+
st.session_state.predictions = results
|
| 216 |
+
status_text.empty()
|
| 217 |
+
progress_bar.empty()
|
| 218 |
+
|
| 219 |
+
if len(results) < total_rows:
|
| 220 |
+
st.warning(f"Analysis completed partially. Processed {len(results)}/{total_rows} flows.")
|
| 221 |
+
else:
|
| 222 |
+
st.success(f"Analysis complete! Processed all {len(results):,} flows.")
|
| 223 |
+
|
| 224 |
+
results = st.session_state.predictions
|
| 225 |
+
|
| 226 |
+
if not results:
|
| 227 |
+
st.warning("No predictions available. Backend might be down.")
|
| 228 |
+
st.stop()
|
| 229 |
+
|
| 230 |
+
# Extract data for metrics
|
| 231 |
+
pred_labels = [r['attack'] for r in results]
|
| 232 |
+
severities = [r['severity'] for r in results]
|
| 233 |
+
|
| 234 |
+
# KPI Metrics
|
| 235 |
+
col1, col2, col3, col4, col5 = st.columns(5)
|
| 236 |
+
|
| 237 |
+
total_flows = len(results)
|
| 238 |
+
benign_count = pred_labels.count('Benign')
|
| 239 |
+
malicious_count = total_flows - benign_count
|
| 240 |
+
detection_rate = (malicious_count / total_flows * 100) if total_flows > 0 else 0
|
| 241 |
+
|
| 242 |
+
with col1:
|
| 243 |
+
st.metric("Total Flows", f"{total_flows:,}")
|
| 244 |
+
with col2:
|
| 245 |
+
st.metric("Benign", f"{benign_count:,}", delta=f"{benign_count/total_flows*100:.1f}%")
|
| 246 |
+
with col3:
|
| 247 |
+
st.metric("Malicious", f"{malicious_count:,}", delta=f"{malicious_count/total_flows*100:.1f}%", delta_color="inverse")
|
| 248 |
+
with col4:
|
| 249 |
+
st.metric("Detection Rate", f"{detection_rate:.2f}%")
|
| 250 |
+
with col5:
|
| 251 |
+
st.metric("Threats Logged", len(st.session_state.threat_log))
|
| 252 |
+
|
| 253 |
+
st.markdown("---")
|
| 254 |
+
|
| 255 |
+
# Charts Row 1
|
| 256 |
+
col1, col2 = st.columns(2)
|
| 257 |
+
|
| 258 |
+
with col1:
|
| 259 |
+
st.subheader("Attack Distribution")
|
| 260 |
+
attack_counts = pd.Series(pred_labels).value_counts()
|
| 261 |
+
|
| 262 |
+
fig = px.pie(values=attack_counts.values, names=attack_counts.index,
|
| 263 |
+
color_discrete_sequence=px.colors.qualitative.Set3,
|
| 264 |
+
hole=0.4)
|
| 265 |
+
fig.update_traces(textposition='inside', textinfo='percent+label')
|
| 266 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 267 |
+
|
| 268 |
+
with col2:
|
| 269 |
+
st.subheader("Attack Type Counts")
|
| 270 |
+
fig = px.bar(x=attack_counts.index, y=attack_counts.values,
|
| 271 |
+
labels={'x': 'Attack Type', 'y': 'Count'},
|
| 272 |
+
color=attack_counts.index,
|
| 273 |
+
color_discrete_sequence=px.colors.qualitative.Bold)
|
| 274 |
+
fig.update_layout(showlegend=False)
|
| 275 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 276 |
+
|
| 277 |
+
# Charts Row 2
|
| 278 |
+
col1, col2 = st.columns(2)
|
| 279 |
+
|
| 280 |
+
with col1:
|
| 281 |
+
st.subheader("Protocol Distribution (All Traffic)")
|
| 282 |
+
# Use full processed data
|
| 283 |
+
processed_df = df.head(len(results))
|
| 284 |
+
protocol_counts = processed_df['Protocol'].value_counts().head(10)
|
| 285 |
+
fig = px.bar(x=protocol_counts.index, y=protocol_counts.values,
|
| 286 |
+
labels={'x': 'Protocol', 'y': 'Flow Count'},
|
| 287 |
+
color=protocol_counts.values,
|
| 288 |
+
color_continuous_scale='Reds')
|
| 289 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 290 |
+
|
| 291 |
+
with col2:
|
| 292 |
+
st.subheader("Protocol Distribution (Malicious Only)")
|
| 293 |
+
# Filter malicious from processed data
|
| 294 |
+
malicious_mask = [p != 'Benign' for p in pred_labels]
|
| 295 |
+
malicious_df = processed_df[malicious_mask]
|
| 296 |
+
|
| 297 |
+
if len(malicious_df) > 0:
|
| 298 |
+
mal_protocol = malicious_df['Protocol'].value_counts().head(10)
|
| 299 |
+
fig = px.bar(x=mal_protocol.index, y=mal_protocol.values,
|
| 300 |
+
labels={'x': 'Protocol', 'y': 'Attack Count'},
|
| 301 |
+
color=mal_protocol.values,
|
| 302 |
+
color_continuous_scale='OrRd')
|
| 303 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 304 |
+
else:
|
| 305 |
+
st.info("No malicious traffic detected")
|
| 306 |
+
|
| 307 |
+
# Recent Detections
|
| 308 |
+
st.markdown("---")
|
| 309 |
+
st.subheader("🚨 Recent Threat Detections")
|
| 310 |
+
|
| 311 |
+
# Filter malicious
|
| 312 |
+
malicious_indices = [i for i, r in enumerate(results) if r['attack'] != 'Benign']
|
| 313 |
+
|
| 314 |
+
if malicious_indices:
|
| 315 |
+
recent_indices = malicious_indices[-10:][::-1]
|
| 316 |
+
threat_data = []
|
| 317 |
+
|
| 318 |
+
for idx in recent_indices:
|
| 319 |
+
res = results[idx]
|
| 320 |
+
# Map back to original DF for protocol info (assuming 1:1 mapping with sample)
|
| 321 |
+
orig_row = df.iloc[idx]
|
| 322 |
+
|
| 323 |
+
threat_data.append({
|
| 324 |
+
'Flow ID': idx,
|
| 325 |
+
'Attack Type': res['attack'],
|
| 326 |
+
'Severity': f"{res['severity']:.2f}",
|
| 327 |
+
'Protocol': orig_row.get('Protocol', 'N/A'),
|
| 328 |
+
'Action': res['action'],
|
| 329 |
+
'Fwd Packets': int(orig_row.get('Total Fwd Packets', 0)),
|
| 330 |
+
'Bwd Packets': int(orig_row.get('Total Backward Packets', 0))
|
| 331 |
+
})
|
| 332 |
+
|
| 333 |
+
st.dataframe(pd.DataFrame(threat_data), use_container_width=True, hide_index=True)
|
| 334 |
+
else:
|
| 335 |
+
st.success("✅ No threats detected in current sample")
|
| 336 |
+
|
| 337 |
+
# Live Monitor
|
| 338 |
+
elif page == "🔴 Live Monitor":
|
| 339 |
+
st.title("🔴 Real-Time Threat Monitor")
|
| 340 |
+
st.markdown("Simulated real-time monitoring using test data")
|
| 341 |
+
|
| 342 |
+
col1, col2, col3 = st.columns([2, 2, 1])
|
| 343 |
+
with col1:
|
| 344 |
+
if st.button("▶️ Start Monitoring" if not st.session_state.monitoring else "⏸️ Pause Monitoring"):
|
| 345 |
+
st.session_state.monitoring = not st.session_state.monitoring
|
| 346 |
+
|
| 347 |
+
with col2:
|
| 348 |
+
if st.button("🔄 Reset"):
|
| 349 |
+
st.session_state.current_index = 0
|
| 350 |
+
st.session_state.threat_log = []
|
| 351 |
+
st.rerun()
|
| 352 |
+
|
| 353 |
+
with col3:
|
| 354 |
+
speed = st.slider("Speed", 1, 10, 2)
|
| 355 |
+
|
| 356 |
+
placeholder = st.empty()
|
| 357 |
+
|
| 358 |
+
if st.session_state.monitoring:
|
| 359 |
+
while st.session_state.monitoring:
|
| 360 |
+
idx = st.session_state.current_index
|
| 361 |
+
|
| 362 |
+
# 1. Get Flow Data from Backend Monitor Endpoint
|
| 363 |
+
monitor_data = api_get_monitor_flow(idx)
|
| 364 |
+
|
| 365 |
+
if not monitor_data or monitor_data.get('end'):
|
| 366 |
+
st.session_state.monitoring = False
|
| 367 |
+
st.info("Monitoring complete or backend unreachable")
|
| 368 |
+
break
|
| 369 |
+
|
| 370 |
+
flow_data = monitor_data['flow']
|
| 371 |
+
|
| 372 |
+
# 2. Send to Predict Endpoint
|
| 373 |
+
# Filter flow_data to only feature columns expected by model
|
| 374 |
+
pred_features = {k: v for k, v in flow_data.items() if k in feature_cols}
|
| 375 |
+
|
| 376 |
+
prediction = api_predict_flow(pred_features)
|
| 377 |
+
|
| 378 |
+
if prediction:
|
| 379 |
+
attack_label = prediction['attack']
|
| 380 |
+
severity = prediction['severity']
|
| 381 |
+
action = prediction['action']
|
| 382 |
+
|
| 383 |
+
# Log threats
|
| 384 |
+
if attack_label != 'Benign':
|
| 385 |
+
st.session_state.threat_log.append({
|
| 386 |
+
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 387 |
+
'flow_id': idx,
|
| 388 |
+
'attack_type': attack_label,
|
| 389 |
+
'severity': severity,
|
| 390 |
+
'action': action
|
| 391 |
+
})
|
| 392 |
+
|
| 393 |
+
with placeholder.container():
|
| 394 |
+
st.markdown(f"### Flow #{idx} - {datetime.now().strftime('%H:%M:%S')}")
|
| 395 |
+
|
| 396 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 397 |
+
col1.metric("Attack Type", attack_label)
|
| 398 |
+
col2.metric("Severity", f"{severity:.2f}")
|
| 399 |
+
col3.metric("Protocol", flow_data.get('Protocol', 'N/A'))
|
| 400 |
+
col4.metric("Action", action)
|
| 401 |
+
|
| 402 |
+
if attack_label != 'Benign':
|
| 403 |
+
st.error(f"🚨 THREAT DETECTED: {attack_label}")
|
| 404 |
+
|
| 405 |
+
st.markdown("**Key Flow Characteristics:**")
|
| 406 |
+
key_features = ['Total Fwd Packets', 'Total Backward Packets',
|
| 407 |
+
'Flow Bytes/s', 'SYN Flag Count', 'ACK Flag Count']
|
| 408 |
+
|
| 409 |
+
cols = st.columns(len(key_features))
|
| 410 |
+
for i, feat in enumerate(key_features):
|
| 411 |
+
val = flow_data.get(feat, 0)
|
| 412 |
+
cols[i].metric(feat, f"{float(val):.2f}")
|
| 413 |
+
else:
|
| 414 |
+
st.success("✅ Benign Traffic - Allowed")
|
| 415 |
+
|
| 416 |
+
st.progress((idx % 100) / 100) # Simple progress bar
|
| 417 |
+
|
| 418 |
+
# Show recent threat log
|
| 419 |
+
if len(st.session_state.threat_log) > 0:
|
| 420 |
+
st.markdown("---")
|
| 421 |
+
st.markdown("**Recent Threats:**")
|
| 422 |
+
log_df = pd.DataFrame(st.session_state.threat_log[-5:][::-1])
|
| 423 |
+
st.dataframe(log_df, use_container_width=True, hide_index=True)
|
| 424 |
+
|
| 425 |
+
st.session_state.current_index += 1
|
| 426 |
+
time.sleep(1.0 / speed)
|
| 427 |
+
|
| 428 |
+
# Analytics
|
| 429 |
+
elif page == "📈 Analytics":
|
| 430 |
+
st.title("📈 Detailed Analytics")
|
| 431 |
+
|
| 432 |
+
if len(st.session_state.predictions) == 0:
|
| 433 |
+
st.warning("Please visit the Dashboard page first to run the initial analysis.")
|
| 434 |
+
st.stop()
|
| 435 |
+
|
| 436 |
+
results = st.session_state.predictions
|
| 437 |
+
pred_labels = [r['attack'] for r in results]
|
| 438 |
+
|
| 439 |
+
# Confusion Matrix (if labels exist in loaded df)
|
| 440 |
+
if 'Attack_encode' in df.columns:
|
| 441 |
+
# We need to align results with the DF.
|
| 442 |
+
# Assuming results correspond to df.head(len(results))
|
| 443 |
+
sample_df = df.head(len(results))
|
| 444 |
+
y_true = sample_df['Attack_encode'].values
|
| 445 |
+
|
| 446 |
+
# Filter out NaN values
|
| 447 |
+
valid_mask = ~np.isnan(y_true)
|
| 448 |
+
|
| 449 |
+
if valid_mask.sum() > 0:
|
| 450 |
+
y_true_clean = y_true[valid_mask]
|
| 451 |
+
# Convert predictions to encode if possible, or map names to indices
|
| 452 |
+
# ATTACK_MAP = {0: 'Benign', 1: 'DoS', 2: 'BruteForce', 3: 'Scan', 4: 'Malware', 5: 'WebAttack'}
|
| 453 |
+
# Inverse map
|
| 454 |
+
NAME_TO_ENCODE = {'Benign': 0, 'DoS': 1, 'BruteForce': 2, 'Scan': 3, 'Malware': 4, 'WebAttack': 5, 'Unknown': -1}
|
| 455 |
+
|
| 456 |
+
preds_clean = [NAME_TO_ENCODE.get(p, -1) for p in np.array(pred_labels)[valid_mask]]
|
| 457 |
+
|
| 458 |
+
st.subheader("Confusion Matrix")
|
| 459 |
+
from sklearn.metrics import confusion_matrix
|
| 460 |
+
|
| 461 |
+
# Filter out unknowns if any
|
| 462 |
+
valid_preds_mask = [p != -1 for p in preds_clean]
|
| 463 |
+
if any(valid_preds_mask):
|
| 464 |
+
y_true_final = y_true_clean[valid_preds_mask]
|
| 465 |
+
preds_final = np.array(preds_clean)[valid_preds_mask]
|
| 466 |
+
|
| 467 |
+
cm = confusion_matrix(y_true_final, preds_final)
|
| 468 |
+
|
| 469 |
+
attack_names = ['Benign', 'DoS', 'BruteForce', 'Scan', 'Malware', 'WebAttack']
|
| 470 |
+
# Ensure labels match unique values present or use fixed list if we are sure
|
| 471 |
+
|
| 472 |
+
fig = px.imshow(cm,
|
| 473 |
+
labels=dict(x="Predicted", y="Actual", color="Count"),
|
| 474 |
+
x=attack_names[:len(cm)], # Simplified, might need proper alignment
|
| 475 |
+
y=attack_names[:len(cm)],
|
| 476 |
+
color_continuous_scale='Blues',
|
| 477 |
+
text_auto=True)
|
| 478 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 479 |
+
|
| 480 |
+
# Report Generation
|
| 481 |
+
st.markdown("---")
|
| 482 |
+
st.subheader("📄 Generate Comprehensive Threat Report")
|
| 483 |
+
|
| 484 |
+
col1, col2 = st.columns([3, 1])
|
| 485 |
+
with col1:
|
| 486 |
+
st.info("The report will include attack classifications, MITRE ATT&CK framework recommendations, and actionable security measures.")
|
| 487 |
+
|
| 488 |
+
with col2:
|
| 489 |
+
if st.button("📥 Generate Report", type="primary", use_container_width=True):
|
| 490 |
+
with st.spinner("Generating comprehensive threat report using AI..."):
|
| 491 |
+
# Prepare stats
|
| 492 |
+
attack_counts = pd.Series(pred_labels).value_counts().to_dict()
|
| 493 |
+
|
| 494 |
+
# Mock classification report summary
|
| 495 |
+
report_summary = {
|
| 496 |
+
"analyzed_flows": len(results),
|
| 497 |
+
"attack_distribution": attack_counts
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
threat_stats = {
|
| 501 |
+
"total_flows": len(results),
|
| 502 |
+
"malicious_flows": len([p for p in pred_labels if p != 'Benign']),
|
| 503 |
+
"benign_flows": pred_labels.count('Benign'),
|
| 504 |
+
"threat_percentage": len([p for p in pred_labels if p != 'Benign']) / len(results) * 100
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
report_text, error = api_generate_report(attack_counts, report_summary, threat_stats)
|
| 508 |
+
|
| 509 |
+
if error:
|
| 510 |
+
st.error(error)
|
| 511 |
+
st.info("💡 To use report generation, please set your GROQ_API_KEY in Streamlit secrets (st.secrets) or as an environment variable.")
|
| 512 |
+
else:
|
| 513 |
+
st.success("✅ Report generated successfully!")
|
| 514 |
+
st.markdown("### 📋 Report Preview")
|
| 515 |
+
with st.expander("View Generated Report", expanded=True):
|
| 516 |
+
st.markdown(report_text)
|
| 517 |
+
|
| 518 |
+
# Download
|
| 519 |
+
b64 = base64.b64encode(report_text.encode()).decode()
|
| 520 |
+
href = f'<a href="data:text/plain;base64,{b64}" download="threat_report.txt" style="background-color: #764ba2; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">📄 Download Text Report</a>'
|
| 521 |
+
st.markdown(href, unsafe_allow_html=True)
|
| 522 |
+
|
| 523 |
+
# Feature Importance
|
| 524 |
+
st.markdown("---")
|
| 525 |
+
st.subheader("Top 15 Important Features")
|
| 526 |
+
|
| 527 |
+
fi_data = api_get_feature_importance()
|
| 528 |
+
if fi_data:
|
| 529 |
+
importances = None
|
| 530 |
+
if 'importances' in fi_data and fi_data['importances']:
|
| 531 |
+
importances = np.array(fi_data['importances'])
|
| 532 |
+
indices = np.argsort(importances)[::-1][:15]
|
| 533 |
+
top_features = [feature_cols[i] for i in indices]
|
| 534 |
+
top_importances = importances[indices]
|
| 535 |
+
elif 'importances_dict' in fi_data:
|
| 536 |
+
# Sort dict by value
|
| 537 |
+
sorted_items = sorted(fi_data['importances_dict'].items(), key=lambda x: x[1], reverse=True)[:15]
|
| 538 |
+
top_features = [k for k, v in sorted_items]
|
| 539 |
+
top_importances = [v for k, v in sorted_items]
|
| 540 |
+
|
| 541 |
+
if importances is not None or 'importances_dict' in fi_data:
|
| 542 |
+
fig = px.bar(x=top_features,
|
| 543 |
+
y=top_importances,
|
| 544 |
+
labels={'x': 'Feature', 'y': 'Importance'},
|
| 545 |
+
color=top_importances,
|
| 546 |
+
color_continuous_scale='Viridis')
|
| 547 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 548 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 549 |
+
else:
|
| 550 |
+
st.info("Feature importance not available from backend model.")
|
| 551 |
+
|
| 552 |
+
# Feature correlation for malicious traffic
|
| 553 |
+
st.markdown("---")
|
| 554 |
+
st.subheader("Feature Correlation Heatmap (Malicious Traffic)")
|
| 555 |
+
|
| 556 |
+
sample_df = df.head(len(results))
|
| 557 |
+
malicious_mask = [p != 'Benign' for p in pred_labels]
|
| 558 |
+
malicious_df = sample_df[malicious_mask]
|
| 559 |
+
|
| 560 |
+
if len(malicious_df) > 0:
|
| 561 |
+
key_features = ['Total Fwd Packets', 'Total Backward Packets',
|
| 562 |
+
'Flow Bytes/s', 'Flow Packets/s', 'Packet Length Mean',
|
| 563 |
+
'SYN Flag Count', 'ACK Flag Count', 'PSH Flag Count']
|
| 564 |
+
key_features = [f for f in key_features if f in df.columns]
|
| 565 |
+
|
| 566 |
+
corr = malicious_df[key_features].corr()
|
| 567 |
+
|
| 568 |
+
fig = px.imshow(corr,
|
| 569 |
+
labels=dict(color="Correlation"),
|
| 570 |
+
x=key_features, y=key_features,
|
| 571 |
+
color_continuous_scale='RdBu_r',
|
| 572 |
+
zmin=-1, zmax=1,
|
| 573 |
+
text_auto='.2f')
|
| 574 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 575 |
+
|
| 576 |
+
# Statistical summary by attack type
|
| 577 |
+
st.markdown("---")
|
| 578 |
+
st.subheader("Statistical Summary by Attack Type")
|
| 579 |
+
|
| 580 |
+
df_with_pred = sample_df.copy()
|
| 581 |
+
df_with_pred['Predicted_Attack'] = pred_labels
|
| 582 |
+
|
| 583 |
+
selected_attack = st.selectbox("Select Attack Type",
|
| 584 |
+
sorted(df_with_pred['Predicted_Attack'].unique()))
|
| 585 |
+
|
| 586 |
+
attack_subset = df_with_pred[df_with_pred['Predicted_Attack'] == selected_attack]
|
| 587 |
+
|
| 588 |
+
summary_features = ['Total Fwd Packets', 'Total Backward Packets',
|
| 589 |
+
'Flow Bytes/s', 'Packet Length Mean', 'Flow IAT Mean']
|
| 590 |
+
summary_features = [f for f in summary_features if f in df.columns]
|
| 591 |
+
|
| 592 |
+
st.dataframe(attack_subset[summary_features].describe(), use_container_width=True)
|
| 593 |
+
|
| 594 |
+
# Manual Prediction
|
| 595 |
+
elif page == "🎯 Manual Prediction":
|
| 596 |
+
st.title("🎯 Manual Flow Classification")
|
| 597 |
+
st.markdown("Upload a CSV or input features manually")
|
| 598 |
+
|
| 599 |
+
tab1, tab2 = st.tabs(["📁 Upload CSV", "⌨️ Manual Input"])
|
| 600 |
+
|
| 601 |
+
with tab1:
|
| 602 |
+
uploaded_file = st.file_uploader("Upload CSV file", type=['csv'])
|
| 603 |
+
if uploaded_file:
|
| 604 |
+
test_df = pd.read_csv(uploaded_file)
|
| 605 |
+
st.success(f"Loaded {len(test_df)} flows")
|
| 606 |
+
|
| 607 |
+
if st.button("🔍 Classify Flows"):
|
| 608 |
+
features_list = test_df[feature_cols].to_dict(orient='records')
|
| 609 |
+
with st.spinner("Classifying..."):
|
| 610 |
+
results = api_batch_predict(features_list)
|
| 611 |
+
|
| 612 |
+
if results:
|
| 613 |
+
# Add results to DF
|
| 614 |
+
test_df['Prediction'] = [r['attack'] for r in results]
|
| 615 |
+
test_df['Severity'] = [r['severity'] for r in results]
|
| 616 |
+
test_df['Action'] = [r['action'] for r in results]
|
| 617 |
+
|
| 618 |
+
st.dataframe(test_df, use_container_width=True)
|
| 619 |
+
|
| 620 |
+
csv = test_df.to_csv(index=False)
|
| 621 |
+
st.download_button("📥 Download Results", csv,
|
| 622 |
+
"predictions.csv", "text/csv")
|
| 623 |
+
|
| 624 |
+
with tab2:
|
| 625 |
+
st.markdown("**Enter flow features:**")
|
| 626 |
+
|
| 627 |
+
input_data = {}
|
| 628 |
+
cols = st.columns(3)
|
| 629 |
+
|
| 630 |
+
# Dynamically create inputs for all features
|
| 631 |
+
for i, feature in enumerate(feature_cols):
|
| 632 |
+
col_idx = i % 3
|
| 633 |
+
with cols[col_idx]:
|
| 634 |
+
input_data[feature] = st.number_input(feature, value=0.0,
|
| 635 |
+
format="%.2f", key=feature)
|
| 636 |
+
|
| 637 |
+
if st.button("🔍 Classify Flow"):
|
| 638 |
+
with st.spinner("Querying API..."):
|
| 639 |
+
result = api_predict_flow(input_data)
|
| 640 |
+
|
| 641 |
+
if result:
|
| 642 |
+
st.markdown("---")
|
| 643 |
+
st.markdown("### 🎯 Prediction Result")
|
| 644 |
+
|
| 645 |
+
col1, col2, col3 = st.columns(3)
|
| 646 |
+
col1.metric("Attack Type", result['attack'])
|
| 647 |
+
col2.metric("Severity", f"{result['severity']:.2f}")
|
| 648 |
+
col3.metric("Recommended Action", result['action'])
|
| 649 |
+
|
| 650 |
+
if result['attack'] != 'Benign':
|
| 651 |
+
st.error(f"🚨 THREAT DETECTED: {result['attack']}")
|
| 652 |
+
st.markdown(f"**Recommended Action:** {result['action']}")
|
| 653 |
+
else:
|
| 654 |
+
st.success("✅ Benign Traffic - Allow")
|
| 655 |
+
|
| 656 |
+
# Footer
|
| 657 |
+
st.sidebar.markdown("---")
|
| 658 |
+
st.sidebar.info(f"Dashboard v1.0 | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
models/request_models.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import Dict, List, Any
|
| 3 |
+
|
| 4 |
+
class FlowFeatures(BaseModel):
|
| 5 |
+
features: Dict[str, Any]
|
| 6 |
+
|
| 7 |
+
class BatchRequest(BaseModel):
|
| 8 |
+
items: List[Dict[str, Any]]
|
models/response_models.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
class PredictionResponse(BaseModel):
|
| 4 |
+
attack: str
|
| 5 |
+
severity: float
|
| 6 |
+
action: str
|
requirements.txt
CHANGED
|
@@ -1,5 +1,80 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
altair==5.5.0
|
| 2 |
+
annotated-doc==0.0.4
|
| 3 |
+
annotated-types==0.7.0
|
| 4 |
+
anyio==4.11.0
|
| 5 |
+
appdirs==1.4.4
|
| 6 |
+
attrs==25.4.0
|
| 7 |
+
blinker==1.9.0
|
| 8 |
+
cachetools==6.2.2
|
| 9 |
+
certifi==2025.11.12
|
| 10 |
+
charset-normalizer==3.4.4
|
| 11 |
+
click==8.3.1
|
| 12 |
+
cloudpickle==3.1.2
|
| 13 |
+
colorama==0.4.6
|
| 14 |
+
contourpy==1.3.3
|
| 15 |
+
cycler==0.12.1
|
| 16 |
+
distro==1.9.0
|
| 17 |
+
dotenv==0.9.9
|
| 18 |
+
fastapi==0.121.3
|
| 19 |
+
fonttools==4.60.1
|
| 20 |
+
gitdb==4.0.12
|
| 21 |
+
GitPython==3.1.45
|
| 22 |
+
groq==0.36.0
|
| 23 |
+
h11==0.16.0
|
| 24 |
+
httpcore==1.0.9
|
| 25 |
+
httpx==0.28.1
|
| 26 |
+
idna==3.11
|
| 27 |
+
Jinja2==3.1.6
|
| 28 |
+
joblib==1.5.2
|
| 29 |
+
jsonschema==4.25.1
|
| 30 |
+
jsonschema-specifications==2025.9.1
|
| 31 |
+
kiwisolver==1.4.9
|
| 32 |
+
llvmlite==0.45.1
|
| 33 |
+
lxml==6.0.2
|
| 34 |
+
MarkupSafe==3.0.3
|
| 35 |
+
matplotlib==3.10.7
|
| 36 |
+
narwhals==2.12.0
|
| 37 |
+
numba==0.62.1
|
| 38 |
+
numpy==2.3.5
|
| 39 |
+
packaging==25.0
|
| 40 |
+
pandas==2.3.3
|
| 41 |
+
pillow==12.0.0
|
| 42 |
+
plotly==6.5.0
|
| 43 |
+
protobuf==6.33.1
|
| 44 |
+
pyarrow==21.0.0
|
| 45 |
+
pydantic==2.12.4
|
| 46 |
+
pydantic_core==2.41.5
|
| 47 |
+
pydeck==0.9.1
|
| 48 |
+
pyparsing==3.2.5
|
| 49 |
+
pyshark==0.6
|
| 50 |
+
python-dateutil==2.9.0.post0
|
| 51 |
+
python-dotenv==1.2.1
|
| 52 |
+
python-multipart==0.0.20
|
| 53 |
+
pytz==2025.2
|
| 54 |
+
referencing==0.37.0
|
| 55 |
+
reportlab==4.4.5
|
| 56 |
+
requests==2.32.5
|
| 57 |
+
rpds-py==0.29.0
|
| 58 |
+
scikit-learn==1.7.2
|
| 59 |
+
scipy==1.16.3
|
| 60 |
+
shap==0.50.0
|
| 61 |
+
six==1.17.0
|
| 62 |
+
slicer==0.0.8
|
| 63 |
+
smmap==5.0.2
|
| 64 |
+
sniffio==1.3.1
|
| 65 |
+
starlette==0.50.0
|
| 66 |
+
streamlit==1.51.0
|
| 67 |
+
tenacity==9.1.2
|
| 68 |
+
termcolor==3.2.0
|
| 69 |
+
threadpoolctl==3.6.0
|
| 70 |
+
toml==0.10.2
|
| 71 |
+
tornado==6.5.2
|
| 72 |
+
tqdm==4.67.1
|
| 73 |
+
typing-inspection==0.4.2
|
| 74 |
+
typing_extensions==4.15.0
|
| 75 |
+
tzdata==2025.2
|
| 76 |
+
unicorn==2.1.4
|
| 77 |
+
urllib3==2.5.0
|
| 78 |
+
uvicorn==0.38.0
|
| 79 |
+
watchdog==6.0.0
|
| 80 |
+
xgboost==3.1.2
|
routers/blockchain.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
# Path to the ledger file
|
| 8 |
+
# backend/routers/blockchain.py -> parent = backend/routers -> parent.parent = backend
|
| 9 |
+
# ledger is in backend/blockchain/blockchain_ledger.json
|
| 10 |
+
LEDGER_PATH = Path(__file__).resolve().parent.parent / "blockchain" / "blockchain_ledger.json"
|
| 11 |
+
|
| 12 |
+
@router.get("/ledger")
|
| 13 |
+
def get_ledger():
|
| 14 |
+
if not LEDGER_PATH.exists():
|
| 15 |
+
# Return empty structure if not found, or error
|
| 16 |
+
return {"blocks": [], "sealed_batch_count": 0, "open_entries": []}
|
| 17 |
+
try:
|
| 18 |
+
with open(LEDGER_PATH, "r", encoding="utf-8") as f:
|
| 19 |
+
data = json.load(f)
|
| 20 |
+
return data
|
| 21 |
+
except Exception as e:
|
| 22 |
+
raise HTTPException(status_code=500, detail=str(e))
|
routers/monitor.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
router = APIRouter()
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Robust data path finding
|
| 9 |
+
DATA_PATH = "data/test.csv"
|
| 10 |
+
if not os.path.exists(DATA_PATH):
|
| 11 |
+
# Try finding it relative to this file
|
| 12 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 13 |
+
parent_dir = os.path.dirname(current_dir)
|
| 14 |
+
DATA_PATH = os.path.join(parent_dir, "data", "test.csv")
|
| 15 |
+
|
| 16 |
+
if os.path.exists(DATA_PATH):
|
| 17 |
+
df = pd.read_csv(DATA_PATH)
|
| 18 |
+
else:
|
| 19 |
+
print(f"Warning: Test data not found at {DATA_PATH}")
|
| 20 |
+
df = pd.DataFrame() # Empty dataframe to prevent crash on import
|
| 21 |
+
|
| 22 |
+
@router.get("/next/{index}")
|
| 23 |
+
def get_flow(index: int):
|
| 24 |
+
try:
|
| 25 |
+
if index >= len(df):
|
| 26 |
+
return {"end": True}
|
| 27 |
+
|
| 28 |
+
row = df.iloc[index]
|
| 29 |
+
# Handle NaN values for JSON serialization
|
| 30 |
+
# Use a simpler approach if where() fails
|
| 31 |
+
d = row.to_dict()
|
| 32 |
+
clean_d = {}
|
| 33 |
+
for k, v in d.items():
|
| 34 |
+
if pd.isna(v):
|
| 35 |
+
clean_d[k] = None
|
| 36 |
+
else:
|
| 37 |
+
clean_d[k] = v
|
| 38 |
+
|
| 39 |
+
return {
|
| 40 |
+
"index": index,
|
| 41 |
+
"flow": clean_d,
|
| 42 |
+
"end": False
|
| 43 |
+
}
|
| 44 |
+
except Exception as e:
|
| 45 |
+
import traceback
|
| 46 |
+
return {"error": str(e), "trace": traceback.format_exc(), "end": True}
|
| 47 |
+
|
| 48 |
+
# Import necessary modules for prediction
|
| 49 |
+
from routers.predict import model, predict_with_model, ATTACK_MAP
|
| 50 |
+
import numpy as np
|
| 51 |
+
|
| 52 |
+
@router.get("/stats")
|
| 53 |
+
def get_dashboard_stats():
|
| 54 |
+
try:
|
| 55 |
+
if df.empty:
|
| 56 |
+
return {"error": "No data loaded"}
|
| 57 |
+
|
| 58 |
+
# Limit to 100,000 rows for stats calculation as requested
|
| 59 |
+
limit = 100000
|
| 60 |
+
stats_df = df.head(limit)
|
| 61 |
+
|
| 62 |
+
# Perform Prediction on the loaded data (replicating dashboard1.py logic)
|
| 63 |
+
# We need to predict to get the actual current model's view of the data
|
| 64 |
+
# Filter out non-feature columns explicitly
|
| 65 |
+
feature_cols = [col for col in stats_df.columns if col not in ['Attack_type', 'Attack_encode']]
|
| 66 |
+
X = stats_df[feature_cols]
|
| 67 |
+
|
| 68 |
+
# Predict
|
| 69 |
+
if model:
|
| 70 |
+
preds = predict_with_model(model, X)
|
| 71 |
+
# Convert predictions to labels
|
| 72 |
+
# preds might be floats from XGBoost, cast to int
|
| 73 |
+
pred_labels = [int(p) if isinstance(p, (int, float, np.number)) else int(p) for p in preds]
|
| 74 |
+
pred_names = [ATTACK_MAP.get(p, 'Unknown') for p in pred_labels]
|
| 75 |
+
else:
|
| 76 |
+
# Fallback if model not loaded (shouldn't happen if app started correctly)
|
| 77 |
+
return {"error": "Model not loaded"}
|
| 78 |
+
|
| 79 |
+
# 1. Total Flows
|
| 80 |
+
total_flows = len(stats_df)
|
| 81 |
+
|
| 82 |
+
# 2. Attack Distribution (based on PREDICTIONS)
|
| 83 |
+
attack_counts = {}
|
| 84 |
+
for name in pred_names:
|
| 85 |
+
attack_counts[name] = attack_counts.get(name, 0) + 1
|
| 86 |
+
|
| 87 |
+
# 3. Protocol Distribution (All)
|
| 88 |
+
if 'Protocol' in stats_df.columns:
|
| 89 |
+
protocol_counts = stats_df['Protocol'].value_counts().head(10).to_dict()
|
| 90 |
+
else:
|
| 91 |
+
protocol_counts = {}
|
| 92 |
+
|
| 93 |
+
# 4. Protocol Distribution (Malicious)
|
| 94 |
+
malicious_protocol_counts = {}
|
| 95 |
+
recent_threats = []
|
| 96 |
+
|
| 97 |
+
# Filter malicious based on predictions
|
| 98 |
+
# Create a temporary dataframe with predictions for filtering
|
| 99 |
+
temp_df = stats_df.copy()
|
| 100 |
+
temp_df['Predicted_Attack'] = pred_names
|
| 101 |
+
|
| 102 |
+
malicious_df = temp_df[temp_df['Predicted_Attack'] != 'Benign']
|
| 103 |
+
|
| 104 |
+
if not malicious_df.empty:
|
| 105 |
+
if 'Protocol' in malicious_df.columns:
|
| 106 |
+
malicious_protocol_counts = malicious_df['Protocol'].value_counts().head(10).to_dict()
|
| 107 |
+
|
| 108 |
+
# 5. Recent Threats (Take last 20 malicious flows)
|
| 109 |
+
threats_df = malicious_df.tail(20).iloc[::-1]
|
| 110 |
+
|
| 111 |
+
for idx, row in threats_df.iterrows():
|
| 112 |
+
recent_threats.append({
|
| 113 |
+
"id": int(idx),
|
| 114 |
+
"attack": row['Predicted_Attack'],
|
| 115 |
+
"protocol": row['Protocol'] if 'Protocol' in row else "Unknown",
|
| 116 |
+
"severity": "High", # Simplified for summary
|
| 117 |
+
"fwd_packets": int(row.get('Total Fwd Packets', 0)),
|
| 118 |
+
"bwd_packets": int(row.get('Total Backward Packets', 0))
|
| 119 |
+
})
|
| 120 |
+
|
| 121 |
+
return {
|
| 122 |
+
"total_flows": total_flows,
|
| 123 |
+
"attack_counts": attack_counts,
|
| 124 |
+
"protocol_counts": protocol_counts,
|
| 125 |
+
"malicious_protocol_counts": malicious_protocol_counts,
|
| 126 |
+
"recent_threats": recent_threats
|
| 127 |
+
}
|
| 128 |
+
except Exception as e:
|
| 129 |
+
import traceback
|
| 130 |
+
print(traceback.format_exc())
|
| 131 |
+
return {"error": str(e)}
|
routers/predict.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from utils.model_loader import load_model
|
| 3 |
+
from utils.severity import calculate_severity
|
| 4 |
+
from models.request_models import FlowFeatures, BatchRequest
|
| 5 |
+
from models.response_models import PredictionResponse
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import xgboost as xgb
|
| 9 |
+
|
| 10 |
+
router = APIRouter()
|
| 11 |
+
|
| 12 |
+
# Load model on startup (or lazy load)
|
| 13 |
+
try:
|
| 14 |
+
model = load_model()
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print(f"Error loading model: {e}")
|
| 17 |
+
model = None
|
| 18 |
+
|
| 19 |
+
ATTACK_MAP = {0: "Benign", 1: "DoS", 2: "BruteForce", 3: "Scan", 4: "Malware", 5: "WebAttack"}
|
| 20 |
+
|
| 21 |
+
ACTIONS = {
|
| 22 |
+
'DoS': 'BLOCK IP + Rate Limiting',
|
| 23 |
+
'BruteForce': 'BLOCK IP + Account Lockout',
|
| 24 |
+
'Scan': 'LOG + Monitor Suspicious Activity',
|
| 25 |
+
'Malware': 'QUARANTINE + Deep Scan',
|
| 26 |
+
'WebAttack': 'BLOCK Request + WAF Rule Update',
|
| 27 |
+
'Benign': 'Allow Traffic'
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def predict_with_model(model, data):
|
| 31 |
+
"""
|
| 32 |
+
Wrapper to handle prediction for both XGBoost (DMatrix) and Sklearn (DataFrame) models.
|
| 33 |
+
"""
|
| 34 |
+
if model is None:
|
| 35 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 36 |
+
|
| 37 |
+
# Check if it's an XGBoost Booster
|
| 38 |
+
if isinstance(model, xgb.Booster):
|
| 39 |
+
dmatrix = xgb.DMatrix(data)
|
| 40 |
+
return model.predict(dmatrix)
|
| 41 |
+
|
| 42 |
+
# Default to sklearn-style predict
|
| 43 |
+
try:
|
| 44 |
+
return model.predict(data)
|
| 45 |
+
except Exception as e:
|
| 46 |
+
# Fallback: try converting to DMatrix
|
| 47 |
+
try:
|
| 48 |
+
dmatrix = xgb.DMatrix(data)
|
| 49 |
+
return model.predict(dmatrix)
|
| 50 |
+
except:
|
| 51 |
+
raise e
|
| 52 |
+
|
| 53 |
+
@router.post("/", response_model=PredictionResponse)
|
| 54 |
+
def predict_flow(data: FlowFeatures):
|
| 55 |
+
try:
|
| 56 |
+
# Sanitize features: ensure all values are numeric
|
| 57 |
+
clean_features = {}
|
| 58 |
+
for k, v in data.features.items():
|
| 59 |
+
try:
|
| 60 |
+
clean_features[k] = float(v)
|
| 61 |
+
except (ValueError, TypeError):
|
| 62 |
+
clean_features[k] = 0.0
|
| 63 |
+
|
| 64 |
+
# Convert dict to DataFrame
|
| 65 |
+
df = pd.DataFrame([clean_features])
|
| 66 |
+
|
| 67 |
+
# Remove target columns if present (they shouldn't be in features, but just in case)
|
| 68 |
+
cols_to_drop = [col for col in df.columns if col in ['Attack_type', 'Attack_encode']]
|
| 69 |
+
if cols_to_drop:
|
| 70 |
+
df = df.drop(columns=cols_to_drop)
|
| 71 |
+
|
| 72 |
+
# Predict
|
| 73 |
+
raw_pred = predict_with_model(model, df)
|
| 74 |
+
pred = int(raw_pred[0]) if isinstance(raw_pred[0], (int, float, np.number)) else int(raw_pred[0])
|
| 75 |
+
|
| 76 |
+
attack = ATTACK_MAP.get(pred, "Unknown")
|
| 77 |
+
|
| 78 |
+
# Calculate severity
|
| 79 |
+
severity = calculate_severity(data.features, attack)
|
| 80 |
+
action = ACTIONS.get(attack, "Monitor")
|
| 81 |
+
|
| 82 |
+
return PredictionResponse(
|
| 83 |
+
attack=attack,
|
| 84 |
+
severity=severity,
|
| 85 |
+
action=action
|
| 86 |
+
)
|
| 87 |
+
except Exception as e:
|
| 88 |
+
import traceback
|
| 89 |
+
print(traceback.format_exc())
|
| 90 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@router.post("/batch")
|
| 94 |
+
def batch_predict(data: BatchRequest):
|
| 95 |
+
try:
|
| 96 |
+
df = pd.DataFrame(data.items)
|
| 97 |
+
preds = predict_with_model(model, df)
|
| 98 |
+
|
| 99 |
+
results = []
|
| 100 |
+
for i, p in enumerate(preds):
|
| 101 |
+
pred_val = int(p) if isinstance(p, (int, float, np.number)) else int(p)
|
| 102 |
+
attack = ATTACK_MAP.get(pred_val, "Unknown")
|
| 103 |
+
|
| 104 |
+
# Get features for this item to calculate severity
|
| 105 |
+
# Note: data.items is a list of dicts
|
| 106 |
+
sev = calculate_severity(data.items[i], attack)
|
| 107 |
+
|
| 108 |
+
results.append({
|
| 109 |
+
"attack": attack,
|
| 110 |
+
"severity": sev,
|
| 111 |
+
"action": ACTIONS.get(attack, "Monitor")
|
| 112 |
+
})
|
| 113 |
+
|
| 114 |
+
return {"results": results}
|
| 115 |
+
except Exception as e:
|
| 116 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 117 |
+
|
| 118 |
+
@router.get("/feature-importance")
|
| 119 |
+
def get_feature_importance():
|
| 120 |
+
try:
|
| 121 |
+
if model is None:
|
| 122 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 123 |
+
|
| 124 |
+
# Check for feature_importances_ (sklearn style)
|
| 125 |
+
if hasattr(model, 'feature_importances_'):
|
| 126 |
+
return {"importances": model.feature_importances_.tolist()}
|
| 127 |
+
|
| 128 |
+
# Check for get_score (XGBoost native)
|
| 129 |
+
if hasattr(model, 'get_score'):
|
| 130 |
+
# This returns a dict {feature_name: score}
|
| 131 |
+
# We might need to map it back to feature indices if names aren't preserved or match input
|
| 132 |
+
return {"importances_dict": model.get_score(importance_type='weight')}
|
| 133 |
+
|
| 134 |
+
return {"importances": []}
|
| 135 |
+
except Exception as e:
|
| 136 |
+
raise HTTPException(status_code=500, detail=str(e))
|
routers/reports.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from typing import Dict, List
|
| 4 |
+
import io
|
| 5 |
+
import base64
|
| 6 |
+
from reportlab.lib.pagesizes import letter, A4
|
| 7 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 8 |
+
from reportlab.lib.units import inch
|
| 9 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, PageBreak, Table, TableStyle
|
| 10 |
+
from reportlab.lib import colors
|
| 11 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 12 |
+
import matplotlib
|
| 13 |
+
matplotlib.use('Agg') # Non-interactive backend
|
| 14 |
+
import matplotlib.pyplot as plt
|
| 15 |
+
import numpy as np
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
import os
|
| 18 |
+
from groq import Groq
|
| 19 |
+
|
| 20 |
+
router = APIRouter()
|
| 21 |
+
|
| 22 |
+
class ReportRequest(BaseModel):
|
| 23 |
+
attack_summary: Dict[str, int]
|
| 24 |
+
classification_report: Dict
|
| 25 |
+
threat_statistics: Dict
|
| 26 |
+
attack_counts: Dict[str, int]
|
| 27 |
+
protocol_counts: Dict[str, int]
|
| 28 |
+
|
| 29 |
+
def create_pie_chart(data: Dict[str, int], title: str) -> str:
|
| 30 |
+
"""Create a pie chart and return as base64 image"""
|
| 31 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 32 |
+
|
| 33 |
+
labels = list(data.keys())
|
| 34 |
+
sizes = list(data.values())
|
| 35 |
+
colors_list = ['#00C851', '#ff4444', '#ff8800', '#33b5e5', '#aa66cc', '#2BBBAD']
|
| 36 |
+
|
| 37 |
+
ax.pie(sizes, labels=labels, autopct='%1.1f%%', colors=colors_list[:len(labels)], startangle=90)
|
| 38 |
+
ax.set_title(title, fontsize=14, fontweight='bold')
|
| 39 |
+
|
| 40 |
+
# Save to bytes
|
| 41 |
+
buf = io.BytesIO()
|
| 42 |
+
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
| 43 |
+
buf.seek(0)
|
| 44 |
+
plt.close()
|
| 45 |
+
|
| 46 |
+
return buf
|
| 47 |
+
|
| 48 |
+
def create_bar_chart(data: Dict[str, int], title: str, color='#33b5e5') -> str:
|
| 49 |
+
"""Create a bar chart and return as base64 image"""
|
| 50 |
+
fig, ax = plt.subplots(figsize=(8, 4))
|
| 51 |
+
|
| 52 |
+
labels = list(data.keys())
|
| 53 |
+
values = list(data.values())
|
| 54 |
+
|
| 55 |
+
ax.barh(labels, values, color=color)
|
| 56 |
+
ax.set_xlabel('Count', fontsize=10)
|
| 57 |
+
ax.set_title(title, fontsize=14, fontweight='bold')
|
| 58 |
+
ax.grid(axis='x', alpha=0.3)
|
| 59 |
+
|
| 60 |
+
# Save to bytes
|
| 61 |
+
buf = io.BytesIO()
|
| 62 |
+
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
| 63 |
+
buf.seek(0)
|
| 64 |
+
plt.close()
|
| 65 |
+
|
| 66 |
+
return buf
|
| 67 |
+
|
| 68 |
+
@router.post("/generate-pdf")
|
| 69 |
+
async def generate_pdf_report(data: ReportRequest):
|
| 70 |
+
try:
|
| 71 |
+
# Create PDF buffer
|
| 72 |
+
buffer = io.BytesIO()
|
| 73 |
+
doc = SimpleDocTemplate(buffer, pagesize=letter, topMargin=0.5*inch, bottomMargin=0.5*inch)
|
| 74 |
+
|
| 75 |
+
# Styles
|
| 76 |
+
styles = getSampleStyleSheet()
|
| 77 |
+
title_style = ParagraphStyle(
|
| 78 |
+
'CustomTitle',
|
| 79 |
+
parent=styles['Heading1'],
|
| 80 |
+
fontSize=24,
|
| 81 |
+
textColor=colors.HexColor('#1a1a2e'),
|
| 82 |
+
spaceAfter=30,
|
| 83 |
+
alignment=TA_CENTER,
|
| 84 |
+
fontName='Helvetica-Bold'
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
heading_style = ParagraphStyle(
|
| 88 |
+
'CustomHeading',
|
| 89 |
+
parent=styles['Heading2'],
|
| 90 |
+
fontSize=16,
|
| 91 |
+
textColor=colors.HexColor('#2d4059'),
|
| 92 |
+
spaceAfter=12,
|
| 93 |
+
spaceBefore=20,
|
| 94 |
+
fontName='Helvetica-Bold'
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
body_style = ParagraphStyle(
|
| 98 |
+
'CustomBody',
|
| 99 |
+
parent=styles['BodyText'],
|
| 100 |
+
fontSize=11,
|
| 101 |
+
leading=16,
|
| 102 |
+
spaceAfter=12,
|
| 103 |
+
alignment=TA_LEFT
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# Build content
|
| 107 |
+
content = []
|
| 108 |
+
|
| 109 |
+
# Title
|
| 110 |
+
content.append(Paragraph("Network Intrusion Detection System", title_style))
|
| 111 |
+
content.append(Paragraph("Comprehensive Threat Analysis Report", styles['Heading2']))
|
| 112 |
+
content.append(Spacer(1, 0.2*inch))
|
| 113 |
+
|
| 114 |
+
# Metadata
|
| 115 |
+
meta_data = [
|
| 116 |
+
['Report Generated:', datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
|
| 117 |
+
['Total Flows Analyzed:', str(data.threat_statistics.get('total', 0))],
|
| 118 |
+
['Malicious Flows:', str(data.threat_statistics.get('malicious', 0))],
|
| 119 |
+
['Benign Flows:', str(data.threat_statistics.get('benign', 0))],
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
meta_table = Table(meta_data, colWidths=[2*inch, 3*inch])
|
| 123 |
+
meta_table.setStyle(TableStyle([
|
| 124 |
+
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#e8eaf6')),
|
| 125 |
+
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
|
| 126 |
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 127 |
+
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
| 128 |
+
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 129 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 130 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
|
| 131 |
+
]))
|
| 132 |
+
content.append(meta_table)
|
| 133 |
+
content.append(Spacer(1, 0.3*inch))
|
| 134 |
+
|
| 135 |
+
# Executive Summary (AI Generated)
|
| 136 |
+
content.append(Paragraph("Executive Summary", heading_style))
|
| 137 |
+
|
| 138 |
+
# Generate AI summary using Groq
|
| 139 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 140 |
+
client = Groq(api_key=api_key)
|
| 141 |
+
|
| 142 |
+
summary_prompt = f"""Generate a professional executive summary for a network security report with these statistics:
|
| 143 |
+
|
| 144 |
+
Total Flows: {data.threat_statistics.get('total', 0)}
|
| 145 |
+
Malicious: {data.threat_statistics.get('malicious', 0)}
|
| 146 |
+
Attack Distribution: {data.attack_summary}
|
| 147 |
+
|
| 148 |
+
Provide a 3-4 sentence executive summary in plain text (no markdown) covering:
|
| 149 |
+
1. Overall security posture
|
| 150 |
+
2. Key threats detected
|
| 151 |
+
3. Critical recommendations
|
| 152 |
+
|
| 153 |
+
Keep it professional and concise."""
|
| 154 |
+
|
| 155 |
+
try:
|
| 156 |
+
response = client.chat.completions.create(
|
| 157 |
+
messages=[{"role": "user", "content": summary_prompt}],
|
| 158 |
+
model="llama-3.1-8b-instant",
|
| 159 |
+
temperature=0.7,
|
| 160 |
+
max_tokens=200
|
| 161 |
+
)
|
| 162 |
+
summary_text = response.choices[0].message.content
|
| 163 |
+
except:
|
| 164 |
+
summary_text = f"Analysis of {data.threat_statistics.get('total', 0)} network flows revealed {data.threat_statistics.get('malicious', 0)} malicious activities. The system successfully identified multiple attack vectors requiring immediate attention."
|
| 165 |
+
|
| 166 |
+
content.append(Paragraph(summary_text, body_style))
|
| 167 |
+
content.append(Spacer(1, 0.2*inch))
|
| 168 |
+
|
| 169 |
+
# Attack Distribution Chart
|
| 170 |
+
content.append(Paragraph("Attack Distribution Analysis", heading_style))
|
| 171 |
+
pie_buf = create_pie_chart(data.attack_counts, "Attack Type Distribution")
|
| 172 |
+
img = Image(pie_buf, width=5*inch, height=3.3*inch)
|
| 173 |
+
content.append(img)
|
| 174 |
+
content.append(Spacer(1, 0.2*inch))
|
| 175 |
+
|
| 176 |
+
# Attack Statistics Table
|
| 177 |
+
attack_data = [['Attack Type', 'Count', 'Percentage']]
|
| 178 |
+
total = sum(data.attack_counts.values())
|
| 179 |
+
for attack, count in sorted(data.attack_counts.items(), key=lambda x: x[1], reverse=True):
|
| 180 |
+
percentage = f"{(count/total*100):.1f}%" if total > 0 else "0%"
|
| 181 |
+
attack_data.append([attack, str(count), percentage])
|
| 182 |
+
|
| 183 |
+
attack_table = Table(attack_data, colWidths=[2.5*inch, 1.5*inch, 1.5*inch])
|
| 184 |
+
attack_table.setStyle(TableStyle([
|
| 185 |
+
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3f51b5')),
|
| 186 |
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 187 |
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 188 |
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 189 |
+
('FONTSIZE', (0, 0), (-1, 0), 12),
|
| 190 |
+
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
| 191 |
+
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
| 192 |
+
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
| 193 |
+
]))
|
| 194 |
+
content.append(attack_table)
|
| 195 |
+
content.append(PageBreak())
|
| 196 |
+
|
| 197 |
+
# Protocol Distribution
|
| 198 |
+
content.append(Paragraph("Protocol Distribution Analysis", heading_style))
|
| 199 |
+
bar_buf = create_bar_chart(data.protocol_counts, "Top Protocols by Flow Count", '#33b5e5')
|
| 200 |
+
img2 = Image(bar_buf, width=6*inch, height=3*inch)
|
| 201 |
+
content.append(img2)
|
| 202 |
+
content.append(Spacer(1, 0.3*inch))
|
| 203 |
+
|
| 204 |
+
# Recommendations
|
| 205 |
+
content.append(Paragraph("Security Recommendations", heading_style))
|
| 206 |
+
recommendations = [
|
| 207 |
+
"Implement rate limiting and DDoS protection for high-volume attack vectors",
|
| 208 |
+
"Enable multi-factor authentication to prevent brute force attacks",
|
| 209 |
+
"Deploy Web Application Firewall (WAF) rules for detected web attack patterns",
|
| 210 |
+
"Conduct regular security audits and penetration testing",
|
| 211 |
+
"Update intrusion detection signatures based on identified attack patterns"
|
| 212 |
+
]
|
| 213 |
+
|
| 214 |
+
for i, rec in enumerate(recommendations, 1):
|
| 215 |
+
content.append(Paragraph(f"{i}. {rec}", body_style))
|
| 216 |
+
|
| 217 |
+
content.append(Spacer(1, 0.3*inch))
|
| 218 |
+
|
| 219 |
+
# Footer
|
| 220 |
+
content.append(Spacer(1, 0.5*inch))
|
| 221 |
+
footer_style = ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, textColor=colors.grey, alignment=TA_CENTER)
|
| 222 |
+
content.append(Paragraph("This report was generated by SkyFort IDS - Network Intrusion Detection System", footer_style))
|
| 223 |
+
content.append(Paragraph(f"Report ID: RPT-{datetime.now().strftime('%Y%m%d-%H%M%S')}", footer_style))
|
| 224 |
+
|
| 225 |
+
# Build PDF
|
| 226 |
+
doc.build(content)
|
| 227 |
+
|
| 228 |
+
# Get PDF bytes
|
| 229 |
+
pdf_bytes = buffer.getvalue()
|
| 230 |
+
buffer.close()
|
| 231 |
+
|
| 232 |
+
# Return as base64
|
| 233 |
+
pdf_base64 = base64.b64encode(pdf_bytes).decode('utf-8')
|
| 234 |
+
|
| 235 |
+
return {
|
| 236 |
+
"pdf": pdf_base64,
|
| 237 |
+
"filename": f"threat_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
except Exception as e:
|
| 241 |
+
import traceback
|
| 242 |
+
print(traceback.format_exc())
|
| 243 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 244 |
+
|
| 245 |
+
@router.post("/generate")
|
| 246 |
+
async def generate_text_report(data: ReportRequest):
|
| 247 |
+
"""Generate a text-based threat report"""
|
| 248 |
+
try:
|
| 249 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 250 |
+
client = Groq(api_key=api_key)
|
| 251 |
+
|
| 252 |
+
# Prepare statistics
|
| 253 |
+
total = data.threat_statistics.get('total', 0)
|
| 254 |
+
malicious = data.threat_statistics.get('malicious', 0)
|
| 255 |
+
benign = data.threat_statistics.get('benign', 0)
|
| 256 |
+
|
| 257 |
+
# Create attack summary string
|
| 258 |
+
attack_summary = ", ".join([f"{k}: {v}" for k, v in data.attack_counts.items()])
|
| 259 |
+
|
| 260 |
+
prompt = f"""Generate a professional cybersecurity threat analysis report based on these statistics:
|
| 261 |
+
|
| 262 |
+
Total Network Flows Analyzed: {total}
|
| 263 |
+
Malicious Flows Detected: {malicious}
|
| 264 |
+
Benign Flows: {benign}
|
| 265 |
+
Attack Distribution: {attack_summary}
|
| 266 |
+
|
| 267 |
+
Create a comprehensive report with the following sections:
|
| 268 |
+
1. Executive Summary (2-3 sentences)
|
| 269 |
+
2. Threat Overview (detailed analysis of detected attacks)
|
| 270 |
+
3. Attack Pattern Analysis (breakdown by type)
|
| 271 |
+
4. Risk Assessment
|
| 272 |
+
5. Recommended Actions (5-7 specific mitigation strategies)
|
| 273 |
+
6. Conclusion
|
| 274 |
+
|
| 275 |
+
Format the report professionally with clear section headers. Keep it factual and actionable."""
|
| 276 |
+
|
| 277 |
+
response = client.chat.completions.create(
|
| 278 |
+
messages=[{"role": "user", "content": prompt}],
|
| 279 |
+
model="llama-3.1-8b-instant",
|
| 280 |
+
temperature=0.7,
|
| 281 |
+
max_tokens=1500
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
report_text = response.choices[0].message.content
|
| 285 |
+
|
| 286 |
+
# Add header and footer
|
| 287 |
+
full_report = f"""
|
| 288 |
+
╔══════════════════════════════════════════════════════════════════╗
|
| 289 |
+
║ NETWORK INTRUSION DETECTION SYSTEM (IDS) ║
|
| 290 |
+
║ THREAT ANALYSIS REPORT ║
|
| 291 |
+
╚══════════════════════════════════════════════════════════════════╝
|
| 292 |
+
|
| 293 |
+
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
| 294 |
+
Report ID: RPT-{datetime.now().strftime('%Y%m%d-%H%M%S')}
|
| 295 |
+
|
| 296 |
+
{report_text}
|
| 297 |
+
|
| 298 |
+
═══════════════════════════════════════════════════════════════════
|
| 299 |
+
This report was automatically generated by SkyFort IDS
|
| 300 |
+
For questions or concerns, contact your security team
|
| 301 |
+
═══════════════════════════════════════════════════════════════════
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
return {"report": full_report}
|
| 305 |
+
|
| 306 |
+
except Exception as e:
|
| 307 |
+
import traceback
|
| 308 |
+
print(traceback.format_exc())
|
| 309 |
+
raise HTTPException(status_code=500, detail=str(e))
|
routers/upload.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, UploadFile, File, HTTPException
|
| 2 |
+
from typing import Dict
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import io
|
| 5 |
+
import os
|
| 6 |
+
import tempfile
|
| 7 |
+
from routers.predict import model, predict_with_model, ATTACK_MAP
|
| 8 |
+
# from utils.pcap_converter import convert_pcap_to_csv # Temporarily commented
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
router = APIRouter()
|
| 12 |
+
|
| 13 |
+
# @router.post("/convert-pcap")
|
| 14 |
+
# async def convert_pcap(file: UploadFile = File(...)):
|
| 15 |
+
# """
|
| 16 |
+
# Convert uploaded PCAP file to CSV and return it as a download
|
| 17 |
+
# """
|
| 18 |
+
# try:
|
| 19 |
+
# filename = file.filename.lower()
|
| 20 |
+
# if not (filename.endswith('.pcap') or filename.endswith('.pcapng')):
|
| 21 |
+
# raise HTTPException(status_code=400, detail="Only .pcap or .pcapng files are allowed")
|
| 22 |
+
#
|
| 23 |
+
# # Save PCAP to temp file
|
| 24 |
+
# with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1]) as tmp:
|
| 25 |
+
# content = await file.read()
|
| 26 |
+
# tmp.write(content)
|
| 27 |
+
# tmp_path = tmp.name
|
| 28 |
+
#
|
| 29 |
+
# try:
|
| 30 |
+
# # Convert to DataFrame
|
| 31 |
+
# df = convert_pcap_to_csv(tmp_path)
|
| 32 |
+
#
|
| 33 |
+
# # Convert to CSV string
|
| 34 |
+
# stream = io.StringIO()
|
| 35 |
+
# df.to_csv(stream, index=False)
|
| 36 |
+
# response = stream.getvalue()
|
| 37 |
+
#
|
| 38 |
+
# # Return as file
|
| 39 |
+
# from fastapi.responses import Response
|
| 40 |
+
# return Response(
|
| 41 |
+
# content=response,
|
| 42 |
+
# media_type="text/csv",
|
| 43 |
+
# headers={"Content-Disposition": f"attachment; filename={filename}.csv"}
|
| 44 |
+
# )
|
| 45 |
+
#
|
| 46 |
+
# finally:
|
| 47 |
+
# # Cleanup temp file
|
| 48 |
+
# if os.path.exists(tmp_path):
|
| 49 |
+
# os.remove(tmp_path)
|
| 50 |
+
#
|
| 51 |
+
# except Exception as e:
|
| 52 |
+
# import traceback
|
| 53 |
+
# print(traceback.format_exc())
|
| 54 |
+
# raise HTTPException(status_code=500, detail=f"Error converting file: {str(e)}")
|
| 55 |
+
|
| 56 |
+
@router.post("/upload")
|
| 57 |
+
async def analyze_csv(file: UploadFile = File(...)):
|
| 58 |
+
"""
|
| 59 |
+
Upload a CSV file and get analysis similar to dashboard stats
|
| 60 |
+
"""
|
| 61 |
+
try:
|
| 62 |
+
# Validate file type
|
| 63 |
+
if not file.filename.endswith('.csv'):
|
| 64 |
+
raise HTTPException(status_code=400, detail="Only CSV files are allowed")
|
| 65 |
+
|
| 66 |
+
# Read CSV file
|
| 67 |
+
contents = await file.read()
|
| 68 |
+
df = pd.read_csv(io.StringIO(contents.decode('utf-8')))
|
| 69 |
+
|
| 70 |
+
# Limit to 100,000 rows for performance
|
| 71 |
+
if len(df) > 100000:
|
| 72 |
+
df = df.head(100000)
|
| 73 |
+
|
| 74 |
+
# Validate required columns
|
| 75 |
+
required_cols = ['Protocol', 'Total Fwd Packets', 'Total Backward Packets']
|
| 76 |
+
missing_cols = [col for col in required_cols if col not in df.columns]
|
| 77 |
+
if missing_cols:
|
| 78 |
+
raise HTTPException(
|
| 79 |
+
status_code=400,
|
| 80 |
+
detail=f"CSV is missing required columns: {', '.join(missing_cols)}"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Filter out non-feature columns
|
| 84 |
+
feature_cols = [col for col in df.columns if col not in ['Attack_type', 'Attack_encode']]
|
| 85 |
+
X = df[feature_cols]
|
| 86 |
+
|
| 87 |
+
# Handle NaN values
|
| 88 |
+
X = X.fillna(0)
|
| 89 |
+
|
| 90 |
+
# Predict
|
| 91 |
+
if model:
|
| 92 |
+
preds = predict_with_model(model, X)
|
| 93 |
+
pred_labels = [int(p) if isinstance(p, (int, float, np.number)) else int(p) for p in preds]
|
| 94 |
+
pred_names = [ATTACK_MAP.get(p, 'Unknown') for p in pred_labels]
|
| 95 |
+
else:
|
| 96 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 97 |
+
|
| 98 |
+
# Calculate statistics
|
| 99 |
+
total_flows = len(df)
|
| 100 |
+
|
| 101 |
+
# Attack Distribution
|
| 102 |
+
attack_counts = {}
|
| 103 |
+
for name in pred_names:
|
| 104 |
+
attack_counts[name] = attack_counts.get(name, 0) + 1
|
| 105 |
+
|
| 106 |
+
# Protocol Distribution (All)
|
| 107 |
+
protocol_counts = {}
|
| 108 |
+
if 'Protocol' in df.columns:
|
| 109 |
+
protocol_counts = df['Protocol'].value_counts().head(10).to_dict()
|
| 110 |
+
|
| 111 |
+
# Protocol Distribution (Malicious)
|
| 112 |
+
malicious_protocol_counts = {}
|
| 113 |
+
recent_threats = []
|
| 114 |
+
|
| 115 |
+
# Create temporary dataframe with predictions
|
| 116 |
+
temp_df = df.copy()
|
| 117 |
+
temp_df['Predicted_Attack'] = pred_names
|
| 118 |
+
|
| 119 |
+
malicious_df = temp_df[temp_df['Predicted_Attack'] != 'Benign']
|
| 120 |
+
|
| 121 |
+
if not malicious_df.empty:
|
| 122 |
+
if 'Protocol' in malicious_df.columns:
|
| 123 |
+
malicious_protocol_counts = malicious_df['Protocol'].value_counts().head(10).to_dict()
|
| 124 |
+
|
| 125 |
+
# Recent Threats (last 20)
|
| 126 |
+
threats_df = malicious_df.tail(20).iloc[::-1]
|
| 127 |
+
|
| 128 |
+
for idx, row in threats_df.iterrows():
|
| 129 |
+
recent_threats.append({
|
| 130 |
+
"id": int(idx),
|
| 131 |
+
"attack": row['Predicted_Attack'],
|
| 132 |
+
"protocol": str(row['Protocol']) if 'Protocol' in row else "Unknown",
|
| 133 |
+
"severity": "High",
|
| 134 |
+
"fwd_packets": int(row.get('Total Fwd Packets', 0)),
|
| 135 |
+
"bwd_packets": int(row.get('Total Backward Packets', 0))
|
| 136 |
+
})
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
"success": True,
|
| 140 |
+
"filename": file.filename,
|
| 141 |
+
"total_flows": total_flows,
|
| 142 |
+
"attack_counts": attack_counts,
|
| 143 |
+
"protocol_counts": protocol_counts,
|
| 144 |
+
"malicious_protocol_counts": malicious_protocol_counts,
|
| 145 |
+
"recent_threats": recent_threats
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
except pd.errors.EmptyDataError:
|
| 149 |
+
raise HTTPException(status_code=400, detail="CSV file is empty")
|
| 150 |
+
except pd.errors.ParserError:
|
| 151 |
+
raise HTTPException(status_code=400, detail="Invalid CSV format")
|
| 152 |
+
except Exception as e:
|
| 153 |
+
import traceback
|
| 154 |
+
print(traceback.format_exc())
|
| 155 |
+
raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
|
| 156 |
+
|
| 157 |
+
@router.post("/feature-importance")
|
| 158 |
+
async def calculate_feature_importance(file: UploadFile = File(...)):
|
| 159 |
+
"""
|
| 160 |
+
Calculate feature importance (SHAP values) for uploaded CSV file
|
| 161 |
+
"""
|
| 162 |
+
try:
|
| 163 |
+
# Validate file type
|
| 164 |
+
if not file.filename.endswith('.csv'):
|
| 165 |
+
raise HTTPException(status_code=400, detail="Only CSV files are allowed")
|
| 166 |
+
|
| 167 |
+
# Read CSV file
|
| 168 |
+
contents = await file.read()
|
| 169 |
+
df = pd.read_csv(io.StringIO(contents.decode('utf-8')))
|
| 170 |
+
|
| 171 |
+
# Limit to 10,000 rows for SHAP calculation (performance)
|
| 172 |
+
if len(df) > 10000:
|
| 173 |
+
df = df.head(10000)
|
| 174 |
+
|
| 175 |
+
# Filter out non-feature columns
|
| 176 |
+
feature_cols = [col for col in df.columns if col not in ['Attack_type', 'Attack_encode']]
|
| 177 |
+
X = df[feature_cols]
|
| 178 |
+
|
| 179 |
+
# Handle NaN values
|
| 180 |
+
X = X.fillna(0)
|
| 181 |
+
|
| 182 |
+
# Get feature importance from model
|
| 183 |
+
if model:
|
| 184 |
+
try:
|
| 185 |
+
feature_names = X.columns.tolist()
|
| 186 |
+
print(f"Model type: {type(model)}")
|
| 187 |
+
print(f"Model attributes: {dir(model)}")
|
| 188 |
+
|
| 189 |
+
# Try to get feature importances using different methods
|
| 190 |
+
importances = {}
|
| 191 |
+
|
| 192 |
+
# Method 1: Try feature_importances_ attribute (sklearn-style)
|
| 193 |
+
if hasattr(model, 'feature_importances_'):
|
| 194 |
+
print("Using feature_importances_ attribute")
|
| 195 |
+
importances = dict(zip(feature_names, model.feature_importances_.tolist()))
|
| 196 |
+
print(f"Got {len(importances)} importances, sample: {list(importances.items())[:3]}")
|
| 197 |
+
|
| 198 |
+
# Method 2: Try get_score for XGBoost Booster
|
| 199 |
+
elif hasattr(model, 'get_score'):
|
| 200 |
+
print("Using get_score method")
|
| 201 |
+
# Try different importance types
|
| 202 |
+
for importance_type in ['weight', 'gain', 'cover']:
|
| 203 |
+
try:
|
| 204 |
+
importance_dict = model.get_score(importance_type=importance_type)
|
| 205 |
+
print(f"get_score({importance_type}): {list(importance_dict.items())[:3] if importance_dict else 'empty'}")
|
| 206 |
+
if importance_dict:
|
| 207 |
+
# Create a case-insensitive map of importance keys
|
| 208 |
+
importance_map_lower = {k.lower(): v for k, v in importance_dict.items()}
|
| 209 |
+
|
| 210 |
+
# Map f0, f1, f2... to actual feature names
|
| 211 |
+
# OR use the feature names directly if they exist in the dict (case-insensitive)
|
| 212 |
+
for i, fname in enumerate(feature_names):
|
| 213 |
+
f_key = f'f{i}'
|
| 214 |
+
fname_lower = fname.lower()
|
| 215 |
+
|
| 216 |
+
if fname in importance_dict:
|
| 217 |
+
importances[fname] = float(importance_dict[fname])
|
| 218 |
+
elif fname_lower in importance_map_lower:
|
| 219 |
+
importances[fname] = float(importance_map_lower[fname_lower])
|
| 220 |
+
elif f_key in importance_dict:
|
| 221 |
+
importances[fname] = float(importance_dict[f_key])
|
| 222 |
+
else:
|
| 223 |
+
importances[fname] = 0.0
|
| 224 |
+
|
| 225 |
+
# Debug print
|
| 226 |
+
print(f"Mapped {len(importances)} features. Top 3: {list(importances.items())[:3]}")
|
| 227 |
+
break
|
| 228 |
+
except Exception as e:
|
| 229 |
+
print(f"get_score({importance_type}) failed: {e}")
|
| 230 |
+
continue
|
| 231 |
+
|
| 232 |
+
# If still empty, try without importance_type
|
| 233 |
+
if not importances:
|
| 234 |
+
try:
|
| 235 |
+
importance_dict = model.get_score()
|
| 236 |
+
for i, fname in enumerate(feature_names):
|
| 237 |
+
key = f'f{i}'
|
| 238 |
+
if key in importance_dict:
|
| 239 |
+
importances[fname] = float(importance_dict[key])
|
| 240 |
+
else:
|
| 241 |
+
importances[fname] = 0.0
|
| 242 |
+
except:
|
| 243 |
+
pass
|
| 244 |
+
|
| 245 |
+
# Skip SHAP calculation - just use what we have or return zeros
|
| 246 |
+
# SHAP is too slow for real-time analysis
|
| 247 |
+
if not importances or all(v == 0 for v in importances.values()):
|
| 248 |
+
print("No importances found, returning uniform values")
|
| 249 |
+
# Return uniform importance as fallback (fast)
|
| 250 |
+
importances = {fname: 1.0 for fname in feature_names}
|
| 251 |
+
|
| 252 |
+
if importances:
|
| 253 |
+
return {
|
| 254 |
+
"success": True,
|
| 255 |
+
"importances_dict": importances,
|
| 256 |
+
"feature_count": len(importances)
|
| 257 |
+
}
|
| 258 |
+
else:
|
| 259 |
+
raise HTTPException(status_code=500, detail="Could not extract feature importance")
|
| 260 |
+
|
| 261 |
+
except Exception as e:
|
| 262 |
+
import traceback
|
| 263 |
+
print(traceback.format_exc())
|
| 264 |
+
raise HTTPException(status_code=500, detail=f"Error calculating importance: {str(e)}")
|
| 265 |
+
else:
|
| 266 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 267 |
+
|
| 268 |
+
except pd.errors.EmptyDataError:
|
| 269 |
+
raise HTTPException(status_code=400, detail="CSV file is empty")
|
| 270 |
+
except pd.errors.ParserError:
|
| 271 |
+
raise HTTPException(status_code=400, detail="Invalid CSV format")
|
| 272 |
+
except Exception as e:
|
| 273 |
+
import traceback
|
| 274 |
+
print(traceback.format_exc())
|
| 275 |
+
raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
|
utils/model_loader.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import joblib
|
| 3 |
+
import xgboost as xgb
|
| 4 |
+
import pickle
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
# Default model name
|
| 8 |
+
MODEL_NAME = 'xgb_classifier.pkl'
|
| 9 |
+
|
| 10 |
+
def load_model(model_name=MODEL_NAME):
|
| 11 |
+
# Paths to check (relative to backend directory)
|
| 12 |
+
paths_to_check = [
|
| 13 |
+
os.path.join('data', model_name),
|
| 14 |
+
os.path.join('..', 'data', model_name),
|
| 15 |
+
os.path.join('models', model_name),
|
| 16 |
+
model_name
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
model_path = None
|
| 20 |
+
for p in paths_to_check:
|
| 21 |
+
if os.path.exists(p):
|
| 22 |
+
model_path = p
|
| 23 |
+
break
|
| 24 |
+
|
| 25 |
+
if not model_path:
|
| 26 |
+
# Fallback for development environment
|
| 27 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 28 |
+
parent_dir = os.path.dirname(current_dir)
|
| 29 |
+
model_path = os.path.join(parent_dir, 'data', model_name)
|
| 30 |
+
|
| 31 |
+
if not os.path.exists(model_path):
|
| 32 |
+
raise FileNotFoundError(f"Model file '{model_name}' not found in data/ or other expected locations.")
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
# 1. Try loading with joblib (standard for sklearn)
|
| 36 |
+
return joblib.load(model_path)
|
| 37 |
+
except:
|
| 38 |
+
try:
|
| 39 |
+
# 2. Try loading as XGBoost native
|
| 40 |
+
model = xgb.Booster()
|
| 41 |
+
model.load_model(model_path)
|
| 42 |
+
return model
|
| 43 |
+
except:
|
| 44 |
+
try:
|
| 45 |
+
# 3. Try standard pickle
|
| 46 |
+
with open(model_path, 'rb') as f:
|
| 47 |
+
return pickle.load(f)
|
| 48 |
+
except Exception as e:
|
| 49 |
+
raise RuntimeError(f"Failed to load model '{model_name}': {e}")
|
utils/pcap_converter.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
from scapy.all import rdpcap, IP, TCP, UDP, IPv6
|
| 4 |
+
import os
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
import statistics
|
| 7 |
+
|
| 8 |
+
def safe_div(x, y):
|
| 9 |
+
return x / y if y != 0 else 0
|
| 10 |
+
|
| 11 |
+
def calculate_stats(values):
|
| 12 |
+
if not values:
|
| 13 |
+
return 0, 0, 0, 0
|
| 14 |
+
return min(values), max(values), statistics.mean(values), statistics.stdev(values) if len(values) > 1 else 0
|
| 15 |
+
|
| 16 |
+
def convert_pcap_to_csv(pcap_file_path):
|
| 17 |
+
"""
|
| 18 |
+
Convert a PCAP file to a Pandas DataFrame with CIC-IDS-2017 like features using Scapy.
|
| 19 |
+
"""
|
| 20 |
+
try:
|
| 21 |
+
# Read PCAP file
|
| 22 |
+
packets = rdpcap(pcap_file_path)
|
| 23 |
+
|
| 24 |
+
flows = defaultdict(lambda: {
|
| 25 |
+
"src_ip": None, "dst_ip": None, "src_port": 0, "dst_port": 0, "protocol": 0,
|
| 26 |
+
"timestamps": [], "fwd_timestamps": [], "bwd_timestamps": [],
|
| 27 |
+
"fwd_pkt_lens": [], "bwd_pkt_lens": [], "all_pkt_lens": [],
|
| 28 |
+
"fwd_header_lens": [], "bwd_header_lens": [],
|
| 29 |
+
"flags": {"FIN": 0, "SYN": 0, "RST": 0, "PSH": 0, "ACK": 0, "URG": 0, "CWR": 0, "ECE": 0},
|
| 30 |
+
"fwd_flags": {"PSH": 0, "URG": 0},
|
| 31 |
+
"init_fwd_win": 0, "init_bwd_win": 0,
|
| 32 |
+
"fwd_act_data_pkts": 0, "fwd_seg_size_min": 0
|
| 33 |
+
})
|
| 34 |
+
|
| 35 |
+
for pkt in packets:
|
| 36 |
+
if IP in pkt:
|
| 37 |
+
src_ip = pkt[IP].src
|
| 38 |
+
dst_ip = pkt[IP].dst
|
| 39 |
+
proto = pkt[IP].proto
|
| 40 |
+
header_len = pkt[IP].ihl * 4
|
| 41 |
+
elif IPv6 in pkt:
|
| 42 |
+
src_ip = pkt[IPv6].src
|
| 43 |
+
dst_ip = pkt[IPv6].dst
|
| 44 |
+
proto = pkt[IPv6].nh
|
| 45 |
+
header_len = 40 # Fixed for IPv6
|
| 46 |
+
else:
|
| 47 |
+
continue
|
| 48 |
+
|
| 49 |
+
src_port = 0
|
| 50 |
+
dst_port = 0
|
| 51 |
+
payload_len = len(pkt.payload)
|
| 52 |
+
|
| 53 |
+
if TCP in pkt:
|
| 54 |
+
src_port = pkt[TCP].sport
|
| 55 |
+
dst_port = pkt[TCP].dport
|
| 56 |
+
flags = pkt[TCP].flags
|
| 57 |
+
window = pkt[TCP].window
|
| 58 |
+
elif UDP in pkt:
|
| 59 |
+
src_port = pkt[UDP].sport
|
| 60 |
+
dst_port = pkt[UDP].dport
|
| 61 |
+
flags = None
|
| 62 |
+
window = 0
|
| 63 |
+
else:
|
| 64 |
+
continue
|
| 65 |
+
|
| 66 |
+
# Flow Key (5-tuple)
|
| 67 |
+
key = (src_ip, dst_ip, src_port, dst_port, proto)
|
| 68 |
+
rev_key = (dst_ip, src_ip, dst_port, src_port, proto)
|
| 69 |
+
|
| 70 |
+
if key in flows:
|
| 71 |
+
flow = flows[key]
|
| 72 |
+
direction = "fwd"
|
| 73 |
+
elif rev_key in flows:
|
| 74 |
+
flow = flows[rev_key]
|
| 75 |
+
direction = "bwd"
|
| 76 |
+
else:
|
| 77 |
+
flow = flows[key]
|
| 78 |
+
flow["src_ip"] = src_ip
|
| 79 |
+
flow["dst_ip"] = dst_ip
|
| 80 |
+
flow["src_port"] = src_port
|
| 81 |
+
flow["dst_port"] = dst_port
|
| 82 |
+
flow["protocol"] = proto
|
| 83 |
+
direction = "fwd"
|
| 84 |
+
|
| 85 |
+
timestamp = float(pkt.time)
|
| 86 |
+
flow["timestamps"].append(timestamp)
|
| 87 |
+
flow["all_pkt_lens"].append(payload_len)
|
| 88 |
+
|
| 89 |
+
if direction == "fwd":
|
| 90 |
+
flow["fwd_timestamps"].append(timestamp)
|
| 91 |
+
flow["fwd_pkt_lens"].append(payload_len)
|
| 92 |
+
flow["fwd_header_lens"].append(header_len)
|
| 93 |
+
if TCP in pkt:
|
| 94 |
+
if flow["init_fwd_win"] == 0: flow["init_fwd_win"] = window
|
| 95 |
+
if payload_len > 0: flow["fwd_act_data_pkts"] += 1
|
| 96 |
+
flow["fwd_seg_size_min"] = header_len # Approximation
|
| 97 |
+
else:
|
| 98 |
+
flow["bwd_timestamps"].append(timestamp)
|
| 99 |
+
flow["bwd_pkt_lens"].append(payload_len)
|
| 100 |
+
flow["bwd_header_lens"].append(header_len)
|
| 101 |
+
if TCP in pkt:
|
| 102 |
+
if flow["init_bwd_win"] == 0: flow["init_bwd_win"] = window
|
| 103 |
+
|
| 104 |
+
if TCP in pkt and flags:
|
| 105 |
+
if 'F' in flags: flow["flags"]["FIN"] += 1
|
| 106 |
+
if 'S' in flags: flow["flags"]["SYN"] += 1
|
| 107 |
+
if 'R' in flags: flow["flags"]["RST"] += 1
|
| 108 |
+
if 'P' in flags:
|
| 109 |
+
flow["flags"]["PSH"] += 1
|
| 110 |
+
if direction == "fwd": flow["fwd_flags"]["PSH"] += 1
|
| 111 |
+
if 'A' in flags: flow["flags"]["ACK"] += 1
|
| 112 |
+
if 'U' in flags:
|
| 113 |
+
flow["flags"]["URG"] += 1
|
| 114 |
+
if direction == "fwd": flow["fwd_flags"]["URG"] += 1
|
| 115 |
+
if 'C' in flags: flow["flags"]["CWR"] += 1
|
| 116 |
+
if 'E' in flags: flow["flags"]["ECE"] += 1
|
| 117 |
+
|
| 118 |
+
# Process flows into features
|
| 119 |
+
rows = []
|
| 120 |
+
for flow in flows.values():
|
| 121 |
+
# Basic Stats
|
| 122 |
+
total_fwd_pkts = len(flow["fwd_pkt_lens"])
|
| 123 |
+
total_bwd_pkts = len(flow["bwd_pkt_lens"])
|
| 124 |
+
total_fwd_len = sum(flow["fwd_pkt_lens"])
|
| 125 |
+
total_bwd_len = sum(flow["bwd_pkt_lens"])
|
| 126 |
+
|
| 127 |
+
fwd_min, fwd_max, fwd_mean, fwd_std = calculate_stats(flow["fwd_pkt_lens"])
|
| 128 |
+
bwd_min, bwd_max, bwd_mean, bwd_std = calculate_stats(flow["bwd_pkt_lens"])
|
| 129 |
+
pkt_min, pkt_max, pkt_mean, pkt_std = calculate_stats(flow["all_pkt_lens"])
|
| 130 |
+
|
| 131 |
+
# Time Stats
|
| 132 |
+
duration = max(flow["timestamps"]) - min(flow["timestamps"]) if flow["timestamps"] else 0
|
| 133 |
+
if duration == 0: duration = 1e-6 # Avoid division by zero
|
| 134 |
+
|
| 135 |
+
flow_bytes_s = (total_fwd_len + total_bwd_len) / duration
|
| 136 |
+
flow_pkts_s = (total_fwd_pkts + total_bwd_pkts) / duration
|
| 137 |
+
fwd_pkts_s = total_fwd_pkts / duration
|
| 138 |
+
bwd_pkts_s = total_bwd_pkts / duration
|
| 139 |
+
|
| 140 |
+
# IAT Stats
|
| 141 |
+
flow_iats = [t2 - t1 for t1, t2 in zip(flow["timestamps"][:-1], flow["timestamps"][1:])]
|
| 142 |
+
fwd_iats = [t2 - t1 for t1, t2 in zip(flow["fwd_timestamps"][:-1], flow["fwd_timestamps"][1:])]
|
| 143 |
+
bwd_iats = [t2 - t1 for t1, t2 in zip(flow["bwd_timestamps"][:-1], flow["bwd_timestamps"][1:])]
|
| 144 |
+
|
| 145 |
+
flow_iat_min, flow_iat_max, flow_iat_mean, flow_iat_std = calculate_stats(flow_iats)
|
| 146 |
+
_, fwd_iat_max, _, fwd_iat_std = calculate_stats(fwd_iats)
|
| 147 |
+
_, bwd_iat_max, _, bwd_iat_std = calculate_stats(bwd_iats)
|
| 148 |
+
|
| 149 |
+
# Active/Idle (Simplified)
|
| 150 |
+
active_mean = 0
|
| 151 |
+
active_std = 0
|
| 152 |
+
active_max = 0
|
| 153 |
+
active_min = 0
|
| 154 |
+
idle_mean = 0
|
| 155 |
+
idle_std = 0
|
| 156 |
+
idle_max = 0
|
| 157 |
+
idle_min = 0
|
| 158 |
+
|
| 159 |
+
if flow_iats:
|
| 160 |
+
idle_threshold = 5.0 # seconds
|
| 161 |
+
idles = [iat for iat in flow_iats if iat > idle_threshold]
|
| 162 |
+
actives = [iat for iat in flow_iats if iat <= idle_threshold]
|
| 163 |
+
|
| 164 |
+
if idles:
|
| 165 |
+
idle_min, idle_max, idle_mean, idle_std = calculate_stats(idles)
|
| 166 |
+
if actives:
|
| 167 |
+
active_min, active_max, active_mean, active_std = calculate_stats(actives)
|
| 168 |
+
|
| 169 |
+
row = {
|
| 170 |
+
"Protocol": flow["protocol"],
|
| 171 |
+
"Total Fwd Packets": total_fwd_pkts,
|
| 172 |
+
"Total Backward Packets": total_bwd_pkts,
|
| 173 |
+
"Fwd Packets Length Total": total_fwd_len,
|
| 174 |
+
"Bwd Packets Length Total": total_bwd_len,
|
| 175 |
+
"Fwd Packet Length Max": fwd_max,
|
| 176 |
+
"Fwd Packet Length Min": fwd_min,
|
| 177 |
+
"Fwd Packet Length Std": fwd_std,
|
| 178 |
+
"Bwd Packet Length Max": bwd_max,
|
| 179 |
+
"Bwd Packet Length Min": bwd_min,
|
| 180 |
+
"Bwd Packet Length Std": bwd_std,
|
| 181 |
+
"Flow Bytes/s": flow_bytes_s,
|
| 182 |
+
"Flow Packets/s": flow_pkts_s,
|
| 183 |
+
"Flow IAT Mean": flow_iat_mean,
|
| 184 |
+
"Flow IAT Std": flow_iat_std,
|
| 185 |
+
"Flow IAT Max": flow_iat_max,
|
| 186 |
+
"Fwd IAT Std": fwd_iat_std,
|
| 187 |
+
"Fwd IAT Max": fwd_iat_max,
|
| 188 |
+
"Bwd IAT Std": bwd_iat_std,
|
| 189 |
+
"Bwd IAT Max": bwd_iat_max,
|
| 190 |
+
"Fwd PSH Flags": flow["fwd_flags"]["PSH"],
|
| 191 |
+
"Fwd URG Flags": flow["fwd_flags"]["URG"],
|
| 192 |
+
"Fwd Header Length": sum(flow["fwd_header_lens"]),
|
| 193 |
+
"Bwd Header Length": sum(flow["bwd_header_lens"]),
|
| 194 |
+
"Fwd Packets/s": fwd_pkts_s,
|
| 195 |
+
"Bwd Packets/s": bwd_pkts_s,
|
| 196 |
+
"Packet Length Min": pkt_min,
|
| 197 |
+
"Packet Length Max": pkt_max,
|
| 198 |
+
"Packet Length Mean": pkt_mean,
|
| 199 |
+
"Packet Length Std": pkt_std,
|
| 200 |
+
"FIN Flag Count": flow["flags"]["FIN"],
|
| 201 |
+
"SYN Flag Count": flow["flags"]["SYN"],
|
| 202 |
+
"RST Flag Count": flow["flags"]["RST"],
|
| 203 |
+
"PSH Flag Count": flow["flags"]["PSH"],
|
| 204 |
+
"ACK Flag Count": flow["flags"]["ACK"],
|
| 205 |
+
"URG Flag Count": flow["flags"]["URG"],
|
| 206 |
+
"CWE Flag Count": flow["flags"]["CWR"],
|
| 207 |
+
"ECE Flag Count": flow["flags"]["ECE"],
|
| 208 |
+
"Down/Up Ratio": safe_div(total_bwd_pkts, total_fwd_pkts),
|
| 209 |
+
"Init Fwd Win Bytes": flow["init_fwd_win"],
|
| 210 |
+
"Init Bwd Win Bytes": flow["init_bwd_win"],
|
| 211 |
+
"Fwd Act Data Packets": flow["fwd_act_data_pkts"],
|
| 212 |
+
"Fwd Seg Size Min": flow["fwd_seg_size_min"],
|
| 213 |
+
"Active Mean": active_mean,
|
| 214 |
+
"Active Std": active_std,
|
| 215 |
+
"Active Max": active_max,
|
| 216 |
+
"Active Min": active_min,
|
| 217 |
+
"Idle Mean": idle_mean,
|
| 218 |
+
"Idle Std": idle_std,
|
| 219 |
+
"Idle Max": idle_max,
|
| 220 |
+
"Idle Min": idle_min,
|
| 221 |
+
"Attack_type": "Unknown",
|
| 222 |
+
"Attack_encode": 0,
|
| 223 |
+
"mapped_label": "Unknown",
|
| 224 |
+
"severity_raw": 0,
|
| 225 |
+
"severity": "Unknown"
|
| 226 |
+
}
|
| 227 |
+
rows.append(row)
|
| 228 |
+
|
| 229 |
+
df = pd.DataFrame(rows)
|
| 230 |
+
return df
|
| 231 |
+
|
| 232 |
+
except Exception as e:
|
| 233 |
+
print(f"Error converting PCAP: {e}")
|
| 234 |
+
# Return empty DataFrame with expected columns on error
|
| 235 |
+
return pd.DataFrame()
|
utils/severity.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
LABEL_BOOST = {
|
| 4 |
+
'benign': -2.0, 'bruteforce': 0.9, 'dos': 1.2,
|
| 5 |
+
'malware': 1.3, 'scan': 0.7, 'webattack': 1.0
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
def sigmoid(x):
|
| 9 |
+
return 1 / (1 + np.exp(-np.clip(x, -500, 500)))
|
| 10 |
+
|
| 11 |
+
def get_label_boost(label: str):
|
| 12 |
+
label = label.lower()
|
| 13 |
+
for key, val in LABEL_BOOST.items():
|
| 14 |
+
if key in label:
|
| 15 |
+
return val
|
| 16 |
+
return 0.5
|
| 17 |
+
|
| 18 |
+
def calculate_severity(features, attack_label):
|
| 19 |
+
values = np.array(list(features.values()))
|
| 20 |
+
weights = np.ones(len(values)) / len(values)
|
| 21 |
+
|
| 22 |
+
raw = np.dot(values, weights) + get_label_boost(attack_label) / 2
|
| 23 |
+
return float(sigmoid(raw))
|