cloud450 commited on
Commit
6f3b14e
·
verified ·
1 Parent(s): c3fa769

Upload 20 files

Browse files
.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
- fastapi
2
- uvicorn
3
- pandas
4
- numpy
5
- scikit-learn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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))