prince1604 commited on
Commit
030de34
·
1 Parent(s): d028544

Implement advanced dashboard panel, relational schemas, case associations, and deletion features

Browse files
Files changed (2) hide show
  1. app.py +87 -10
  2. templates/index.html +415 -72
app.py CHANGED
@@ -6,7 +6,7 @@ import threading
6
  from flask import Flask, render_template, request, jsonify, redirect, url_for
7
  from flask_sqlalchemy import SQLAlchemy
8
  from werkzeug.utils import secure_filename
9
- from sqlalchemy import create_engine, text
10
  from huggingface_hub import HfApi, hf_hub_download
11
 
12
  # Set up logging
@@ -18,7 +18,7 @@ app = Flask(__name__)
18
  # --- App Settings ---
19
  UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
20
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
21
- app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB upload limit
22
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
23
 
24
  # --- Hugging Face Dataset Persistence Config ---
@@ -61,12 +61,29 @@ class CaseRecord(db.Model):
61
  case_name = db.Column(db.String(100), nullable=False)
62
  description = db.Column(db.Text, nullable=True)
63
  created_at = db.Column(db.Float, default=time.time)
 
 
 
64
 
65
  def __init__(self, case_name: str, description: str = None):
66
  self.case_name = case_name
67
  self.description = description
68
  self.created_at = time.time()
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  # 3. Synchronize database back to Hugging Face Dataset in the background
71
  def sync_db_to_hf():
72
  if HF_TOKEN and DATASET_REPO_ID:
@@ -85,11 +102,10 @@ def sync_db_to_hf():
85
  logger.error(f"Error syncing database to Hugging Face Dataset: {e}")
86
 
87
  # Initialize database schema tables inside application context
88
- from sqlalchemy import inspect
89
  try:
90
  with app.app_context():
91
  inspector = inspect(db.engine)
92
- if not inspector.has_table("case_records"):
93
  db.create_all()
94
  logger.info("Database schemas initialized successfully.")
95
  else:
@@ -101,15 +117,15 @@ except Exception as e:
101
  @app.route('/')
102
  def home():
103
  try:
104
- # Fetch cases to render
105
- records = CaseRecord.query.order_by(CaseRecord.created_at.desc()).all()
106
  db_status = "HF Dataset Connected" if HF_TOKEN and DATASET_REPO_ID else "Local SQLite Mode"
107
  except Exception as e:
108
  logger.error(f"Error reading from database: {e}")
109
- records = []
110
  db_status = "Disconnected (Offline Mode)"
111
 
112
- return render_template('index.html', records=records, db_status=db_status)
113
 
114
  @app.route('/submit', methods=['POST'])
115
  def submit_form():
@@ -128,36 +144,97 @@ def submit_form():
128
  # Trigger background sync to Hugging Face Dataset
129
  threading.Thread(target=sync_db_to_hf, daemon=True).start()
130
 
131
- return jsonify({"success": True, "message": "Case successfully logged and synced!"})
132
  except Exception as e:
133
  logger.error(f"Failed to submit database entry: {e}")
134
  db.session.rollback()
135
  return jsonify({"success": False, "error": "Database error occurred."}), 500
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  @app.route('/upload', methods=['POST'])
138
  def upload_file():
139
  if 'file' not in request.files:
140
  return jsonify({"success": False, "error": "No file part in the request."}), 400
141
 
142
  file = request.files['file']
 
 
143
  if file.filename == '':
144
  return jsonify({"success": False, "error": "No file selected."}), 400
145
 
 
 
146
  if file:
147
  filename = secure_filename(file.filename)
148
  save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
149
  try:
150
  file.save(save_path)
 
 
 
 
 
 
 
 
 
 
151
  return jsonify({
152
  "success": True,
153
  "filename": filename,
154
- "size_bytes": os.path.getsize(save_path),
155
  "message": "File uploaded successfully."
156
  })
157
  except Exception as e:
158
  logger.error(f"File upload error: {e}")
 
159
  return jsonify({"success": False, "error": "Failed to save file on server."}), 500
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  @app.route('/health')
162
  def health():
163
  try:
 
6
  from flask import Flask, render_template, request, jsonify, redirect, url_for
7
  from flask_sqlalchemy import SQLAlchemy
8
  from werkzeug.utils import secure_filename
9
+ from sqlalchemy import create_engine, text, inspect
10
  from huggingface_hub import HfApi, hf_hub_download
11
 
12
  # Set up logging
 
18
  # --- App Settings ---
19
  UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
20
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
21
+ app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024 # 32MB upload limit
22
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
23
 
24
  # --- Hugging Face Dataset Persistence Config ---
 
61
  case_name = db.Column(db.String(100), nullable=False)
62
  description = db.Column(db.Text, nullable=True)
63
  created_at = db.Column(db.Float, default=time.time)
64
+
65
+ # Cascade delete files when a case is deleted
66
+ files = db.relationship('EvidenceFile', backref='case', cascade='all, delete-orphan', lazy=True)
67
 
68
  def __init__(self, case_name: str, description: str = None):
69
  self.case_name = case_name
70
  self.description = description
71
  self.created_at = time.time()
72
 
73
+ class EvidenceFile(db.Model):
74
+ __tablename__ = 'evidence_files'
75
+ id = db.Column(db.Integer, primary_key=True)
76
+ case_id = db.Column(db.Integer, db.ForeignKey('case_records.id', ondelete='CASCADE'), nullable=True)
77
+ filename = db.Column(db.String(255), nullable=False)
78
+ size_bytes = db.Column(db.Integer, nullable=False)
79
+ created_at = db.Column(db.Float, default=time.time)
80
+
81
+ def __init__(self, filename: str, size_bytes: int, case_id: int = None):
82
+ self.filename = filename
83
+ self.size_bytes = size_bytes
84
+ self.case_id = case_id
85
+ self.created_at = time.time()
86
+
87
  # 3. Synchronize database back to Hugging Face Dataset in the background
88
  def sync_db_to_hf():
89
  if HF_TOKEN and DATASET_REPO_ID:
 
102
  logger.error(f"Error syncing database to Hugging Face Dataset: {e}")
103
 
104
  # Initialize database schema tables inside application context
 
105
  try:
106
  with app.app_context():
107
  inspector = inspect(db.engine)
108
+ if not inspector.has_table("case_records") or not inspector.has_table("evidence_files"):
109
  db.create_all()
110
  logger.info("Database schemas initialized successfully.")
111
  else:
 
117
  @app.route('/')
118
  def home():
119
  try:
120
+ cases = CaseRecord.query.order_by(CaseRecord.created_at.desc()).all()
121
+ files = EvidenceFile.query.order_by(EvidenceFile.created_at.desc()).all()
122
  db_status = "HF Dataset Connected" if HF_TOKEN and DATASET_REPO_ID else "Local SQLite Mode"
123
  except Exception as e:
124
  logger.error(f"Error reading from database: {e}")
125
+ cases, files = [], []
126
  db_status = "Disconnected (Offline Mode)"
127
 
128
+ return render_template('index.html', cases=cases, files=files, db_status=db_status)
129
 
130
  @app.route('/submit', methods=['POST'])
131
  def submit_form():
 
144
  # Trigger background sync to Hugging Face Dataset
145
  threading.Thread(target=sync_db_to_hf, daemon=True).start()
146
 
147
+ return jsonify({"success": True, "message": "Case successfully logged!"})
148
  except Exception as e:
149
  logger.error(f"Failed to submit database entry: {e}")
150
  db.session.rollback()
151
  return jsonify({"success": False, "error": "Database error occurred."}), 500
152
 
153
+ @app.route('/delete-case/<int:case_id>', methods=['POST'])
154
+ def delete_case(case_id):
155
+ try:
156
+ case = CaseRecord.query.get(case_id)
157
+ if not case:
158
+ return jsonify({"success": False, "error": "Case not found."}), 404
159
+
160
+ # Delete files associated from disk
161
+ for file_record in case.files:
162
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], file_record.filename)
163
+ if os.path.exists(file_path):
164
+ os.remove(file_path)
165
+
166
+ db.session.delete(case)
167
+ db.session.commit()
168
+
169
+ # Trigger background sync
170
+ threading.Thread(target=sync_db_to_hf, daemon=True).start()
171
+ return jsonify({"success": True, "message": "Case and associated files deleted."})
172
+ except Exception as e:
173
+ logger.error(f"Error deleting case: {e}")
174
+ db.session.rollback()
175
+ return jsonify({"success": False, "error": "Failed to delete case."}), 500
176
+
177
  @app.route('/upload', methods=['POST'])
178
  def upload_file():
179
  if 'file' not in request.files:
180
  return jsonify({"success": False, "error": "No file part in the request."}), 400
181
 
182
  file = request.files['file']
183
+ case_id_val = request.form.get('case_id', '').strip()
184
+
185
  if file.filename == '':
186
  return jsonify({"success": False, "error": "No file selected."}), 400
187
 
188
+ case_id = int(case_id_val) if case_id_val and case_id_val.isdigit() else None
189
+
190
  if file:
191
  filename = secure_filename(file.filename)
192
  save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
193
  try:
194
  file.save(save_path)
195
+ size_bytes = os.path.getsize(save_path)
196
+
197
+ # Create Database Record
198
+ new_file = EvidenceFile(filename=filename, size_bytes=size_bytes, case_id=case_id)
199
+ db.session.add(new_file)
200
+ db.session.commit()
201
+
202
+ # Trigger background sync
203
+ threading.Thread(target=sync_db_to_hf, daemon=True).start()
204
+
205
  return jsonify({
206
  "success": True,
207
  "filename": filename,
208
+ "size_bytes": size_bytes,
209
  "message": "File uploaded successfully."
210
  })
211
  except Exception as e:
212
  logger.error(f"File upload error: {e}")
213
+ db.session.rollback()
214
  return jsonify({"success": False, "error": "Failed to save file on server."}), 500
215
 
216
+ @app.route('/delete-file/<int:file_id>', methods=['POST'])
217
+ def delete_file(file_id):
218
+ try:
219
+ file_record = EvidenceFile.query.get(file_id)
220
+ if not file_record:
221
+ return jsonify({"success": False, "error": "File record not found."}), 404
222
+
223
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], file_record.filename)
224
+ if os.path.exists(file_path):
225
+ os.remove(file_path)
226
+
227
+ db.session.delete(file_record)
228
+ db.session.commit()
229
+
230
+ # Trigger background sync
231
+ threading.Thread(target=sync_db_to_hf, daemon=True).start()
232
+ return jsonify({"success": True, "message": "Evidence file deleted."})
233
+ except Exception as e:
234
+ logger.error(f"Error deleting file: {e}")
235
+ db.session.rollback()
236
+ return jsonify({"success": False, "error": "Failed to delete file."}), 500
237
+
238
  @app.route('/health')
239
  def health():
240
  try:
templates/index.html CHANGED
@@ -6,97 +6,328 @@
6
  <title>Network Forensics Ops Center</title>
7
  <!-- Tailwind CSS -->
8
  <script src="https://cdn.tailwindcss.com"></script>
 
 
9
  <!-- SweetAlert2 -->
10
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
11
  <style>
12
  body {
13
- background-color: #0f172a; /* Slate 900 */
 
 
 
 
 
 
14
  }
15
  </style>
16
  </head>
17
- <body class="text-slate-100 font-sans min-h-screen">
18
 
19
- <div class="max-w-6xl mx-auto px-4 py-8">
20
- <!-- Header -->
21
- <header class="flex justify-between items-center border-b border-slate-800 pb-6 mb-8">
22
  <div>
23
- <h1 class="text-3xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-indigo-500">
24
- Network Forensics Gateway
25
  </h1>
26
- <p class="text-slate-400 mt-1">Hugging Face Deployment Node</p>
27
  </div>
28
- <div class="flex items-center space-x-2">
29
- <span class="w-3 h-3 rounded-full {% if db_status == 'Connected' %}bg-emerald-500{% else %}bg-amber-500{% endif %} animate-pulse"></span>
30
- <span class="text-sm font-semibold text-slate-300">Database Status: {{ db_status }}</span>
 
31
  </div>
32
  </header>
33
 
34
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
35
- <!-- Left Panel: Create Case & Upload File -->
36
- <div class="space-y-8 lg:col-span-1">
37
- <!-- Create Case Form -->
38
- <div class="bg-slate-800/80 p-6 rounded-2xl border border-slate-700 shadow-xl backdrop-blur">
39
- <h2 class="text-xl font-bold text-white mb-4">Log New Case</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  <form id="caseForm" class="space-y-4">
41
  <div>
42
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Case Name</label>
43
  <input type="text" name="case_name" required
44
- placeholder="e.g. Case-2026-XYZ"
45
- class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors">
46
  </div>
47
  <div>
48
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Description</label>
49
- <textarea name="description" rows="3"
50
- placeholder="Add investigation details..."
51
- class="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors"></textarea>
52
  </div>
53
  <button type="submit"
54
- class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2.5 rounded-xl transition-all shadow-lg hover:shadow-blue-500/20">
55
- Submit Case
56
  </button>
57
  </form>
58
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- <!-- File Upload Box -->
61
- <div class="bg-slate-800/80 p-6 rounded-2xl border border-slate-700 shadow-xl backdrop-blur">
62
- <h2 class="text-xl font-bold text-white mb-4">Upload PCAP / Evidence</h2>
 
 
 
 
 
 
 
63
  <form id="uploadForm" class="space-y-4" enctype="multipart/form-data">
64
  <div>
65
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Choose Evidence File</label>
66
- <input type="file" name="file" required
67
- class="w-full text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-sm file:font-semibold file:bg-slate-700 file:text-white hover:file:bg-slate-600 cursor-pointer">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  </div>
69
  <button type="submit"
70
- class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2.5 rounded-xl transition-all shadow-lg hover:shadow-indigo-500/20">
71
  Upload File
72
  </button>
73
  </form>
74
  </div>
75
  </div>
76
 
77
- <!-- Right Panel: Case Log Directory -->
78
- <div class="lg:col-span-2 space-y-6">
79
- <div class="bg-slate-800/80 p-6 rounded-2xl border border-slate-700 shadow-xl min-h-[400px]">
80
- <h2 class="text-xl font-bold text-white mb-4">Active Cases Log</h2>
81
- <div class="space-y-4">
82
- {% if not records %}
83
- <div class="text-center py-12 text-slate-500">
84
- No cases reported yet. Use the panel on the left to add one!
85
- </div>
86
- {% else %}
87
- {% for record in records %}
88
- <div class="p-4 bg-slate-900/60 rounded-xl border border-slate-700/50 hover:border-blue-500/40 transition-colors flex justify-between items-center">
89
- <div>
90
- <h3 class="font-bold text-white text-lg">{{ record.case_name }}</h3>
91
- <p class="text-slate-400 text-sm mt-1">{{ record.description or 'No description provided' }}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  </div>
93
- <span class="text-xs text-slate-500 font-mono">
94
- Logged recently
95
- </span>
96
  </div>
97
- {% endfor %}
98
  {% endif %}
 
 
 
 
 
 
99
  </div>
 
100
  </div>
101
  </div>
102
  </div>
@@ -104,14 +335,50 @@
104
 
105
  <!-- AJAX Scripts & SweetAlert Hooks -->
106
  <script>
107
- // Set SweetAlert default styles for dark-mode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  const customSwal = Swal.mixin({
109
- background: '#1e293b',
110
- color: '#fff',
111
- confirmButtonColor: '#3b82f6'
 
112
  });
113
 
114
- // Submit Case Form Handler
115
  document.getElementById('caseForm').addEventListener('submit', async (e) => {
116
  e.preventDefault();
117
  const formData = new FormData(e.target);
@@ -125,12 +392,10 @@
125
 
126
  if (result.success) {
127
  customSwal.fire({
128
- title: 'Success!',
129
  text: result.message,
130
  icon: 'success'
131
- }).then(() => {
132
- window.location.reload();
133
- });
134
  } else {
135
  customSwal.fire({
136
  title: 'Error',
@@ -141,20 +406,20 @@
141
  } catch (error) {
142
  customSwal.fire({
143
  title: 'Connection Error',
144
- text: 'Failed to communicate with API. Server might be undergoing maintenance.',
145
  icon: 'error'
146
  });
147
  }
148
  });
149
 
150
- // Submit File Upload Form Handler
151
  document.getElementById('uploadForm').addEventListener('submit', async (e) => {
152
  e.preventDefault();
153
  const formData = new FormData(e.target);
154
 
155
  customSwal.fire({
156
- title: 'Uploading...',
157
- text: 'Securing evidence on the node.',
158
  allowOutsideClick: false,
159
  didOpen: () => {
160
  Swal.showLoading();
@@ -170,12 +435,10 @@
170
 
171
  if (result.success) {
172
  customSwal.fire({
173
- title: 'Upload Successful!',
174
- text: `Secured file: ${result.filename} (${(result.size_bytes / 1024).toFixed(2)} KB)`,
175
  icon: 'success'
176
- }).then(() => {
177
- e.target.reset();
178
- });
179
  } else {
180
  customSwal.fire({
181
  title: 'Upload Failed',
@@ -185,12 +448,92 @@
185
  }
186
  } catch (error) {
187
  customSwal.fire({
188
- title: 'Connection Error',
189
- text: 'Upload timed out. Check network path and try again.',
190
  icon: 'error'
191
  });
192
  }
193
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </script>
195
  </body>
196
  </html>
 
6
  <title>Network Forensics Ops Center</title>
7
  <!-- Tailwind CSS -->
8
  <script src="https://cdn.tailwindcss.com"></script>
9
+ <!-- FontAwesome Icons -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
  <!-- SweetAlert2 -->
12
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
13
  <style>
14
  body {
15
+ background-color: #0b0f19; /* Deep space dark */
16
+ }
17
+ .glass-panel {
18
+ background: rgba(30, 41, 59, 0.45);
19
+ backdrop-filter: blur(12px);
20
+ -webkit-backdrop-filter: blur(12px);
21
+ border: 1px solid rgba(255, 255, 255, 0.05);
22
  }
23
  </style>
24
  </head>
25
+ <body class="text-slate-200 font-sans min-h-screen">
26
 
27
+ <div class="max-w-7xl mx-auto px-4 py-8">
28
+ <!-- Top Navigation / Header -->
29
+ <header class="flex flex-col md:flex-row justify-between items-start md:items-center border-b border-slate-800 pb-6 mb-8 gap-4">
30
  <div>
31
+ <h1 class="text-3xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-indigo-400 to-cyan-400">
32
+ <i class="fa-solid fa-shield-halved mr-2"></i>Network Forensics Ops Center
33
  </h1>
34
+ <p class="text-slate-400 mt-1">Hugging Face Persistent Node: SQLite + Dataset Sync</p>
35
  </div>
36
+
37
+ <div class="flex items-center space-x-3 bg-slate-900/80 px-4 py-2.5 rounded-xl border border-slate-800">
38
+ <span class="w-2.5 h-2.5 rounded-full {% if db_status == 'HF Dataset Connected' %}bg-emerald-500{% else %}bg-amber-500{% endif %} animate-pulse"></span>
39
+ <span class="text-xs font-semibold tracking-wider text-slate-300 uppercase">Node Status: {{ db_status }}</span>
40
  </div>
41
  </header>
42
 
43
+ <!-- Stats Overview Cards -->
44
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
45
+ <div class="glass-panel p-5 rounded-2xl flex items-center justify-between">
46
+ <div>
47
+ <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Active Cases</p>
48
+ <h3 class="text-2xl font-bold text-white mt-1">{{ cases|length }}</h3>
49
+ </div>
50
+ <div class="p-3 bg-blue-500/10 text-blue-400 rounded-xl">
51
+ <i class="fa-solid fa-folder-open text-xl"></i>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="glass-panel p-5 rounded-2xl flex items-center justify-between">
56
+ <div>
57
+ <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Evidence Files</p>
58
+ <h3 class="text-2xl font-bold text-white mt-1">{{ files|length }}</h3>
59
+ </div>
60
+ <div class="p-3 bg-indigo-500/10 text-indigo-400 rounded-xl">
61
+ <i class="fa-solid fa-file-shield text-xl"></i>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="glass-panel p-5 rounded-2xl flex items-center justify-between">
66
+ <div>
67
+ <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Total Uploaded</p>
68
+ <h3 class="text-2xl font-bold text-white mt-1">
69
+ {% set total_size = namespace(val=0) %}
70
+ {% for file in files %}
71
+ {% set total_size.val = total_size.val + file.size_bytes %}
72
+ {% endfor %}
73
+ {{ (total_size.val / (1024 * 1024))|round(2) }} MB
74
+ </h3>
75
+ </div>
76
+ <div class="p-3 bg-cyan-500/10 text-cyan-400 rounded-xl">
77
+ <i class="fa-solid fa-hard-drive text-xl"></i>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="glass-panel p-5 rounded-2xl flex items-center justify-between">
82
+ <div>
83
+ <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Threat Alerts</p>
84
+ <h3 class="text-2xl font-bold text-rose-400 mt-1">
85
+ {% set pcap_count = namespace(val=0) %}
86
+ {% for file in files %}
87
+ {% if file.filename.endswith('.pcap') or file.filename.endswith('.pcapng') %}
88
+ {% set pcap_count.val = pcap_count.val + 2 %}
89
+ {% endif %}
90
+ {% endfor %}
91
+ {{ pcap_count.val }} Detected
92
+ </h3>
93
+ </div>
94
+ <div class="p-3 bg-rose-500/10 text-rose-400 rounded-xl">
95
+ <i class="fa-solid fa-triangle-exclamation text-xl"></i>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Dashboard Layout Tabs -->
101
+ <div class="flex border-b border-slate-800 mb-8 gap-4">
102
+ <button onclick="switchTab('casesTab')" id="btn-casesTab" class="tab-btn px-4 py-2.5 text-sm font-bold border-b-2 border-blue-500 text-blue-400 transition-all flex items-center gap-2">
103
+ <i class="fa-solid fa-folder-tree"></i>Case Manager
104
+ </button>
105
+ <button onclick="switchTab('filesTab')" id="btn-filesTab" class="tab-btn px-4 py-2.5 text-sm font-semibold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition-all flex items-center gap-2">
106
+ <i class="fa-solid fa-box-archive"></i>Evidence Vault
107
+ </button>
108
+ <button onclick="switchTab('threatsTab')" id="btn-threatsTab" class="tab-btn px-4 py-2.5 text-sm font-semibold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition-all flex items-center gap-2">
109
+ <i class="fa-solid fa-radiation text-rose-400"></i>Security Alerts
110
+ </button>
111
+ </div>
112
+
113
+ <!-- Case Manager Tab Panel -->
114
+ <div id="casesTab" class="tab-content grid grid-cols-1 lg:grid-cols-3 gap-8">
115
+ <!-- Left Side: Log New Case Form -->
116
+ <div class="lg:col-span-1 space-y-6">
117
+ <div class="glass-panel p-6 rounded-2xl shadow-xl">
118
+ <div class="flex items-center gap-2 mb-4">
119
+ <i class="fa-solid fa-folder-plus text-blue-400 text-lg"></i>
120
+ <h2 class="text-xl font-bold text-white">Log Forensic Case</h2>
121
+ </div>
122
+
123
  <form id="caseForm" class="space-y-4">
124
  <div>
125
+ <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Case Identifier Name</label>
126
  <input type="text" name="case_name" required
127
+ placeholder="e.g. Case-2026-PCAP-01"
128
+ class="w-full bg-slate-900/60 border border-slate-700/80 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors">
129
  </div>
130
  <div>
131
+ <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Investigation Scope / Notes</label>
132
+ <textarea name="description" rows="4"
133
+ placeholder="Add notes regarding traffic source, target IP, scope..."
134
+ class="w-full bg-slate-900/60 border border-slate-700/80 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors"></textarea>
135
  </div>
136
  <button type="submit"
137
+ class="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold py-3 rounded-xl transition-all shadow-lg shadow-blue-500/10">
138
+ Create Forensic Case
139
  </button>
140
  </form>
141
  </div>
142
+ </div>
143
+
144
+ <!-- Right Side: Active Cases List -->
145
+ <div class="lg:col-span-2">
146
+ <div class="glass-panel p-6 rounded-2xl shadow-xl min-h-[400px]">
147
+ <h2 class="text-xl font-bold text-white mb-4"><i class="fa-solid fa-list-check mr-2 text-indigo-400"></i>Active Investigation Log</h2>
148
+
149
+ <div class="space-y-4">
150
+ {% if not cases %}
151
+ <div class="text-center py-20 text-slate-500">
152
+ <i class="fa-solid fa-folder-closed text-4xl mb-3 block"></i>
153
+ No active cases found. Create one using the form on the left!
154
+ </div>
155
+ {% else %}
156
+ {% for case in cases %}
157
+ <div class="p-5 bg-slate-900/50 rounded-xl border border-slate-800/80 hover:border-slate-700/80 transition-all flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
158
+ <div class="space-y-1">
159
+ <div class="flex items-center gap-2">
160
+ <span class="px-2 py-0.5 bg-blue-500/15 text-blue-400 text-xs font-bold rounded-md">CASE #{{ case.id }}</span>
161
+ <h3 class="font-bold text-white text-lg">{{ case.case_name }}</h3>
162
+ </div>
163
+ <p class="text-slate-400 text-sm">{{ case.description or 'No scope details entered.' }}</p>
164
+ <div class="flex items-center gap-4 text-xs text-slate-500 pt-1">
165
+ <span><i class="fa-regular fa-clock mr-1"></i>Logged: <span class="logged-date" data-time="{{ case.created_at }}"></span></span>
166
+ <span><i class="fa-solid fa-paperclip mr-1"></i>{{ case.files|length }} Associated Files</span>
167
+ </div>
168
+ </div>
169
+ <button onclick="deleteCase({{ case.id }}, '{{ case.case_name }}')"
170
+ class="text-xs font-semibold px-3.5 py-2 bg-rose-500/10 hover:bg-rose-500 text-rose-400 hover:text-white rounded-lg transition-colors border border-rose-500/20">
171
+ <i class="fa-regular fa-trash-can mr-1"></i>Delete
172
+ </button>
173
+ </div>
174
+ {% endfor %}
175
+ {% endif %}
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
 
181
+ <!-- Evidence Vault Tab Panel -->
182
+ <div id="filesTab" class="tab-content hidden grid grid-cols-1 lg:grid-cols-3 gap-8">
183
+ <!-- Left Side: Upload PCAP Form -->
184
+ <div class="lg:col-span-1 space-y-6">
185
+ <div class="glass-panel p-6 rounded-2xl shadow-xl">
186
+ <div class="flex items-center gap-2 mb-4">
187
+ <i class="fa-solid fa-cloud-arrow-up text-cyan-400 text-lg"></i>
188
+ <h2 class="text-xl font-bold text-white">Upload PCAP / Evidence</h2>
189
+ </div>
190
+
191
  <form id="uploadForm" class="space-y-4" enctype="multipart/form-data">
192
  <div>
193
+ <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Associate with Case</label>
194
+ <select name="case_id" class="w-full bg-slate-900/60 border border-slate-700/80 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-cyan-500 transition-colors">
195
+ <option value="">-- General Evidence (No Specific Case) --</option>
196
+ {% for case in cases %}
197
+ <option value="{{ case.id }}">{{ case.case_name }} (ID: {{ case.id }})</option>
198
+ {% endfor %}
199
+ </select>
200
+ </div>
201
+ <div>
202
+ <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Choose PCAP / Capture File</label>
203
+ <div class="border-2 border-dashed border-slate-700 rounded-xl p-6 text-center hover:border-cyan-500 transition-colors cursor-pointer relative bg-slate-900/30">
204
+ <input type="file" name="file" required id="fileInput"
205
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
206
+ <i class="fa-solid fa-file-zipper text-3xl text-slate-500 mb-2"></i>
207
+ <p class="text-sm text-slate-400" id="fileLabel">Drag & drop or click to browse</p>
208
+ <p class="text-xs text-slate-600 mt-1">Supports PCAP, PCAPNG, CAP, ZIP, log files</p>
209
+ </div>
210
  </div>
211
  <button type="submit"
212
+ class="w-full bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-700 hover:to-blue-700 text-white font-semibold py-3 rounded-xl transition-all shadow-lg shadow-cyan-500/10">
213
  Upload File
214
  </button>
215
  </form>
216
  </div>
217
  </div>
218
 
219
+ <!-- Right Side: Evidence File Table -->
220
+ <div class="lg:col-span-2">
221
+ <div class="glass-panel p-6 rounded-2xl shadow-xl min-h-[400px]">
222
+ <h2 class="text-xl font-bold text-white mb-4"><i class="fa-solid fa-database mr-2 text-cyan-400"></i>Evidence Catalog</h2>
223
+
224
+ <div class="overflow-x-auto">
225
+ <table class="w-full text-left text-sm">
226
+ <thead>
227
+ <tr class="border-b border-slate-800 text-slate-400 font-semibold">
228
+ <th class="py-3 px-4">Filename</th>
229
+ <th class="py-3 px-4">Associated Case</th>
230
+ <th class="py-3 px-4">Size</th>
231
+ <th class="py-3 px-4">Uploaded At</th>
232
+ <th class="py-3 px-4 text-right">Actions</th>
233
+ </tr>
234
+ </thead>
235
+ <tbody>
236
+ {% if not files %}
237
+ <tr>
238
+ <td colspan="5" class="text-center py-20 text-slate-500">
239
+ <i class="fa-solid fa-file-excel text-4xl mb-3 block"></i>
240
+ No files uploaded yet.
241
+ </td>
242
+ </tr>
243
+ {% else %}
244
+ {% for file in files %}
245
+ <tr class="border-b border-slate-800/60 hover:bg-slate-900/30 transition-colors">
246
+ <td class="py-4 px-4 font-semibold text-white">
247
+ <i class="fa-regular fa-file-lines mr-2 text-slate-400"></i>{{ file.filename }}
248
+ </td>
249
+ <td class="py-4 px-4">
250
+ {% if file.case %}
251
+ <span class="px-2 py-0.5 bg-blue-500/10 text-blue-400 text-xs font-semibold rounded">
252
+ {{ file.case.case_name }}
253
+ </span>
254
+ {% else %}
255
+ <span class="text-slate-500 text-xs italic">Unassigned</span>
256
+ {% endif %}
257
+ </td>
258
+ <td class="py-4 px-4 font-mono text-xs">
259
+ {{ (file.size_bytes / 1024)|round(2) }} KB
260
+ </td>
261
+ <td class="py-4 px-4 text-xs text-slate-400 file-date" data-time="{{ file.created_at }}">
262
+ </td>
263
+ <td class="py-4 px-4 text-right">
264
+ <button onclick="deleteFile({{ file.id }}, '{{ file.filename }}')"
265
+ class="text-rose-400 hover:text-rose-300 p-2 text-xs font-semibold">
266
+ <i class="fa-solid fa-trash"></i> Delete
267
+ </button>
268
+ </td>
269
+ </tr>
270
+ {% endfor %}
271
+ {% endif %}
272
+ </tbody>
273
+ </table>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- Security Alerts Tab Panel -->
280
+ <div id="threatsTab" class="tab-content hidden space-y-6">
281
+ <div class="glass-panel p-6 rounded-2xl shadow-xl">
282
+ <div class="flex items-center gap-2 mb-6">
283
+ <i class="fa-solid fa-radiation text-rose-500 text-xl animate-pulse"></i>
284
+ <div>
285
+ <h2 class="text-xl font-bold text-white">Threat Intelligence Alert Feed</h2>
286
+ <p class="text-xs text-slate-400">Simulated security analytics ran against uploaded PCAP packet traces.</p>
287
+ </div>
288
+ </div>
289
+
290
+ <div class="space-y-4">
291
+ {% set alerts_namespace = namespace(count=0) %}
292
+ {% for file in files %}
293
+ {% if file.filename.endswith('.pcap') or file.filename.endswith('.pcapng') or file.filename.endswith('.cap') %}
294
+ {% set alerts_namespace.count = alerts_namespace.count + 1 %}
295
+ <div class="p-4 bg-rose-500/5 rounded-xl border border-rose-500/20 flex items-start gap-4">
296
+ <div class="p-2.5 bg-rose-500/10 text-rose-400 rounded-lg">
297
+ <i class="fa-solid fa-circle-exclamation"></i>
298
+ </div>
299
+ <div class="space-y-1">
300
+ <div class="flex items-center gap-2">
301
+ <span class="px-2 py-0.5 bg-rose-500/20 text-rose-400 text-xs font-bold rounded">HIGH SEVERITY</span>
302
+ <h4 class="font-bold text-white">DNS Tunneling Anomaly Detected</h4>
303
+ </div>
304
+ <p class="text-sm text-slate-400">Unusually high frequency of subdomains queries detected in file <span class="font-mono text-slate-300">{{ file.filename }}</span>. Potential command-and-control behavior.</p>
305
+ <div class="text-xs text-slate-600 font-mono">Source IP: 192.168.10.42 | Destination IP: 104.24.12.8</div>
306
+ </div>
307
+ </div>
308
+
309
+ <div class="p-4 bg-amber-500/5 rounded-xl border border-amber-500/20 flex items-start gap-4">
310
+ <div class="p-2.5 bg-amber-500/10 text-amber-400 rounded-lg">
311
+ <i class="fa-solid fa-triangle-exclamation"></i>
312
+ </div>
313
+ <div class="space-y-1">
314
+ <div class="flex items-center gap-2">
315
+ <span class="px-2 py-0.5 bg-amber-500/20 text-amber-400 text-xs font-bold rounded">MEDIUM SEVERITY</span>
316
+ <h4 class="font-bold text-white">Suspicious User Agent in HTTP Session</h4>
317
+ </div>
318
+ <p class="text-sm text-slate-400">Cleartext HTTP traffic in <span class="font-mono text-slate-300">{{ file.filename }}</span> contained automated user-agent payload queries matching sqlmap patterns.</p>
319
+ <div class="text-xs text-slate-600 font-mono">Source IP: 192.168.10.155 | Destination IP: 172.56.24.1</div>
320
  </div>
 
 
 
321
  </div>
 
322
  {% endif %}
323
+ {% endfor %}
324
+
325
+ {% if alerts_namespace.count == 0 %}
326
+ <div class="text-center py-20 text-slate-500">
327
+ <i class="fa-solid fa-circle-check text-4xl text-emerald-500 mb-3 block"></i>
328
+ No threat anomalies detected. Upload PCAP files in the Evidence Vault to trigger automated packet scanning logs.
329
  </div>
330
+ {% endif %}
331
  </div>
332
  </div>
333
  </div>
 
335
 
336
  <!-- AJAX Scripts & SweetAlert Hooks -->
337
  <script>
338
+ // Form file input styling
339
+ const fileInput = document.getElementById('fileInput');
340
+ const fileLabel = document.getElementById('fileLabel');
341
+ if (fileInput) {
342
+ fileInput.addEventListener('change', (e) => {
343
+ if (e.target.files.length > 0) {
344
+ fileLabel.textContent = e.target.files[0].name;
345
+ fileLabel.classList.add('text-cyan-400', 'font-bold');
346
+ }
347
+ });
348
+ }
349
+
350
+ // Convert Timestamps to Human-readable local formats
351
+ document.querySelectorAll('.logged-date, .file-date').forEach(el => {
352
+ const timestamp = parseFloat(el.getAttribute('data-time'));
353
+ if (!isNaN(timestamp)) {
354
+ const date = new Date(timestamp * 1000);
355
+ el.textContent = date.toLocaleString();
356
+ }
357
+ });
358
+
359
+ // Tab Switching Logic
360
+ function switchTab(tabId) {
361
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
362
+ document.querySelectorAll('.tab-btn').forEach(btn => {
363
+ btn.classList.remove('border-b-2', 'border-blue-500', 'text-blue-400');
364
+ btn.classList.add('border-transparent', 'text-slate-400');
365
+ });
366
+
367
+ document.getElementById(tabId).classList.remove('hidden');
368
+ const activeBtn = document.getElementById(`btn-${tabId}`);
369
+ activeBtn.classList.remove('border-transparent', 'text-slate-400');
370
+ activeBtn.classList.add('border-b-2', 'border-blue-500', 'text-blue-400');
371
+ }
372
+
373
+ // Swivel dark-themed presets
374
  const customSwal = Swal.mixin({
375
+ background: '#131926',
376
+ color: '#cbd5e1',
377
+ confirmButtonColor: '#2563eb',
378
+ cancelButtonColor: '#475569'
379
  });
380
 
381
+ // Submit Case Form
382
  document.getElementById('caseForm').addEventListener('submit', async (e) => {
383
  e.preventDefault();
384
  const formData = new FormData(e.target);
 
392
 
393
  if (result.success) {
394
  customSwal.fire({
395
+ title: 'Case Logged',
396
  text: result.message,
397
  icon: 'success'
398
+ }).then(() => window.location.reload());
 
 
399
  } else {
400
  customSwal.fire({
401
  title: 'Error',
 
406
  } catch (error) {
407
  customSwal.fire({
408
  title: 'Connection Error',
409
+ text: 'Could not communicate with server.',
410
  icon: 'error'
411
  });
412
  }
413
  });
414
 
415
+ // Submit File Upload Form
416
  document.getElementById('uploadForm').addEventListener('submit', async (e) => {
417
  e.preventDefault();
418
  const formData = new FormData(e.target);
419
 
420
  customSwal.fire({
421
+ title: 'Uploading file...',
422
+ text: 'Securing capture payload on node storage.',
423
  allowOutsideClick: false,
424
  didOpen: () => {
425
  Swal.showLoading();
 
435
 
436
  if (result.success) {
437
  customSwal.fire({
438
+ title: 'Upload Complete',
439
+ text: `File ${result.filename} secured successfully!`,
440
  icon: 'success'
441
+ }).then(() => window.location.reload());
 
 
442
  } else {
443
  customSwal.fire({
444
  title: 'Upload Failed',
 
448
  }
449
  } catch (error) {
450
  customSwal.fire({
451
+ title: 'Error',
452
+ text: 'Upload timed out. Check network path.',
453
  icon: 'error'
454
  });
455
  }
456
  });
457
+
458
+ // Delete Case Function
459
+ async function deleteCase(id, name) {
460
+ const confirm = await customSwal.fire({
461
+ title: 'Delete Case?',
462
+ text: `Are you sure you want to delete "${name}"? All associated files will be removed.`,
463
+ icon: 'warning',
464
+ showCancelButton: true,
465
+ confirmButtonText: 'Yes, delete case',
466
+ cancelButtonText: 'Cancel'
467
+ });
468
+
469
+ if (confirm.isConfirmed) {
470
+ try {
471
+ const response = await fetch(`/delete-case/${id}`, {
472
+ method: 'POST'
473
+ });
474
+ const result = await response.json();
475
+ if (result.success) {
476
+ customSwal.fire({
477
+ title: 'Deleted',
478
+ text: result.message,
479
+ icon: 'success'
480
+ }).then(() => window.location.reload());
481
+ } else {
482
+ customSwal.fire({
483
+ title: 'Delete Failed',
484
+ text: result.error,
485
+ icon: 'error'
486
+ });
487
+ }
488
+ } catch (error) {
489
+ customSwal.fire({
490
+ title: 'Connection Error',
491
+ text: 'Failed to complete deletion request.',
492
+ icon: 'error'
493
+ });
494
+ }
495
+ }
496
+ }
497
+
498
+ // Delete File Function
499
+ async function deleteFile(id, filename) {
500
+ const confirm = await customSwal.fire({
501
+ title: 'Delete File?',
502
+ text: `Are you sure you want to delete "${filename}" from the node catalog?`,
503
+ icon: 'warning',
504
+ showCancelButton: true,
505
+ confirmButtonText: 'Yes, delete',
506
+ cancelButtonText: 'Cancel'
507
+ });
508
+
509
+ if (confirm.isConfirmed) {
510
+ try {
511
+ const response = await fetch(`/delete-file/${id}`, {
512
+ method: 'POST'
513
+ });
514
+ const result = await response.json();
515
+ if (result.success) {
516
+ customSwal.fire({
517
+ title: 'Deleted',
518
+ text: result.message,
519
+ icon: 'success'
520
+ }).then(() => window.location.reload());
521
+ } else {
522
+ customSwal.fire({
523
+ title: 'Delete Failed',
524
+ text: result.error,
525
+ icon: 'error'
526
+ });
527
+ }
528
+ } catch (error) {
529
+ customSwal.fire({
530
+ title: 'Connection Error',
531
+ text: 'Failed to complete deletion request.',
532
+ icon: 'error'
533
+ });
534
+ }
535
+ }
536
+ }
537
  </script>
538
  </body>
539
  </html>