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

Delete backend

Browse files
backend/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- .venv
2
- __pycache__/
3
- *.pyc
4
- instance/
5
- data/
 
 
 
 
 
 
backend/blockchain/blockchain.py DELETED
@@ -1,386 +0,0 @@
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}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/blockchain/blockchain_blocks_summary.csv DELETED
@@ -1,30 +0,0 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/blockchain/blockchain_ledger.json DELETED
@@ -1,385 +0,0 @@
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/frontend/dashboard.py DELETED
@@ -1,658 +0,0 @@
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')}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/models/request_models.py DELETED
@@ -1,8 +0,0 @@
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]]
 
 
 
 
 
 
 
 
 
backend/models/response_models.py DELETED
@@ -1,6 +0,0 @@
1
- from pydantic import BaseModel
2
-
3
- class PredictionResponse(BaseModel):
4
- attack: str
5
- severity: float
6
- action: str
 
 
 
 
 
 
 
backend/routers/blockchain.py DELETED
@@ -1,22 +0,0 @@
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))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/routers/monitor.py DELETED
@@ -1,131 +0,0 @@
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)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/routers/predict.py DELETED
@@ -1,136 +0,0 @@
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))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/routers/reports.py DELETED
@@ -1,309 +0,0 @@
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))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/routers/upload.py DELETED
@@ -1,275 +0,0 @@
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)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/utils/model_loader.py DELETED
@@ -1,49 +0,0 @@
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}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/utils/pcap_converter.py DELETED
@@ -1,235 +0,0 @@
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/utils/severity.py DELETED
@@ -1,23 +0,0 @@
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))