3v324v23 commited on
Commit
8d9926c
Β·
1 Parent(s): d2cbd11

Auto deploy from GitHub Actions

Browse files
.github/workflows/ci.yml ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: SentinelNet CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v3
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v4
18
+ with:
19
+ python-version: "3.10"
20
+
21
+ - name: Install dependencies
22
+ run: |
23
+ pip install --upgrade pip
24
+ pip install -r requirements.txt
25
+
26
+ - name: Check Python syntax
27
+ run: |
28
+ python -m py_compile app.py
29
+
30
+ - name: Test Flask app startup
31
+ run: |
32
+ export SKIP_MODEL=true
33
+ python app.py &
34
+
35
+ for i in {1..10}; do
36
+ if curl -fsS http://127.0.0.1:7860/health; then
37
+ echo "Server is up!"
38
+ exit 0
39
+ fi
40
+ echo "Waiting for server..."
41
+ sleep 2
42
+ done
43
+
44
+ echo "Server failed to start"
45
+ exit 1
46
+
47
+ - name: Build Docker image
48
+ run: |
49
+ docker build -t sentinelnet .
50
+
51
+ # πŸš€ CD STEP (correct placement)
52
+ - name: Deploy to HuggingFace Spaces
53
+ env:
54
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
55
+ run: |
56
+ pip install huggingface_hub
57
+ sudo apt-get update
58
+ sudo apt-get install -y rsync
59
+
60
+ git config --global user.email "you@example.com"
61
+ git config --global user.name "github-actions"
62
+
63
+ git clone https://Hitan2004:$HF_TOKEN@huggingface.co/spaces/Hitan2004/sentinelnet hf_space
64
+
65
+ rsync -av --exclude='.git' ./ hf_space/
66
+
67
+ cd hf_space
68
+ git add .
69
+ git commit -m "Auto deploy from GitHub Actions" || echo "No changes to commit"
70
+ git push
README.md CHANGED
@@ -1,11 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Sentinelnet
3
- emoji: 🐒
4
- colorFrom: blue
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ # πŸ›‘ SentinelNet β€” AI-Powered Network Intrusion Detection System
2
+
3
+ ![Python](https://img.shields.io/badge/Python-3.10-blue?style=flat-square&logo=python)
4
+ ![Flask](https://img.shields.io/badge/Flask-2.x-black?style=flat-square&logo=flask)
5
+ ![scikit-learn](https://img.shields.io/badge/scikit--learn-1.6-orange?style=flat-square&logo=scikit-learn)
6
+ ![HuggingFace](https://img.shields.io/badge/HuggingFace-Spaces-yellow?style=flat-square&logo=huggingface)
7
+ ![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)
8
+
9
+ > A real-time network intrusion detection dashboard powered by a Random Forest classifier trained on the NSL-KDD dataset. Detects 5 categories of network threats with live visualization and batch CSV analysis.
10
+
11
+ πŸ”΄ **Live Demo:** [https://huggingface.co/spaces/Hitan2004/sentinelnet](https://huggingface.co/spaces/Hitan2004/sentinelnet)
12
+
13
+ ---
14
+
15
+ ## πŸ“Œ What It Does
16
+
17
+ SentinelNet analyzes network traffic and classifies each connection as one of 5 categories:
18
+
19
+ | Class | Type | Severity |
20
+ |-------|------|----------|
21
+ | `normal` | Clean traffic | None |
22
+ | `DoS` | Denial of Service attack | Critical |
23
+ | `Probe` | Reconnaissance / Port scanning | Medium |
24
+ | `R2L` | Remote to Local attack | High |
25
+ | `U2R` | User to Root / Privilege escalation | Critical |
26
+
27
+ ---
28
+
29
+ ## ✨ Features
30
+
31
+ ### πŸ“‘ Live Monitor Tab
32
+ - Auto-generates NSL-KDD formatted network packets
33
+ - Sends each packet to the trained Random Forest model in real time
34
+ - Displays live detection feed with class, confidence, and severity
35
+ - Attack distribution bar chart updated in real time
36
+ - Threat timeline chart (last 60 seconds)
37
+ - Activity heatmap of last 60 packets
38
+ - Confidence distribution panel
39
+ - System log terminal
40
+ - Session summary stats
41
+
42
+ ### πŸ“‚ CSV Analysis Tab
43
+ - Upload any NSL-KDD formatted CSV file
44
+ - Auto-detects headers (with or without column names)
45
+ - Processes rows in batches through the model
46
+ - Live progress bar with ETA and processing speed
47
+ - Row-by-row feed showing predictions as they come in
48
+ - On completion generates a full threat report including:
49
+ - Risk score gauge (0–100)
50
+ - Class distribution bar chart
51
+ - Confidence waveform over dataset
52
+ - Threat intensity rolling chart
53
+ - Protocol breakdown
54
+ - Top targeted services
55
+ - Attack pattern clusters
56
+ - Paginated full results table
57
+ - Export results as **Annotated CSV**, **PDF Report**, or **JSON**
58
+
59
+ ---
60
+
61
+ ## 🧠 Model Details
62
+
63
+ | Property | Value |
64
+ |----------|-------|
65
+ | Algorithm | Random Forest Classifier |
66
+ | Dataset | NSL-KDD (improved KDD Cup 1999) |
67
+ | Features | 41 network connection features |
68
+ | Classes | 5 (normal, DoS, Probe, R2L, U2R) |
69
+ | Preprocessing | OHE encoding, frequency encoding, log transforms, standard scaling |
70
+ | Deployment | HuggingFace Spaces (Flask API) |
71
+
72
+ ### Preprocessing Pipeline
73
+ 1. One-hot encode `protocol_type` and `flag`
74
+ 2. Frequency encode `service` column
75
+ 3. Log transform `src_bytes`, `dst_bytes`, `duration`
76
+ 4. Engineer features: `total_bytes`, `src_bytes_ratio`, `is_error_flag`
77
+ 5. Standard scale all selected features
78
+
79
+ ---
80
+
81
+ ## πŸ— Tech Stack
82
+
83
+ **Backend**
84
+ - Python 3.10
85
+ - Flask + Flask-CORS
86
+ - scikit-learn (Random Forest)
87
+ - pandas, numpy, joblib
88
+
89
+ **Frontend**
90
+ - Vanilla HTML/CSS/JavaScript (no frameworks)
91
+ - IBM Plex Mono + Space Grotesk fonts
92
+ - Canvas API for charts
93
+ - Split into 3 files: `index.html`, `style.css`, `app.js`
94
+
95
+ **Deployment**
96
+ - HuggingFace Spaces (Docker)
97
+ - Flask serves both the frontend and the `/predict` API
98
+
99
+ ---
100
+
101
+ ## πŸ“ Project Structure
102
+
103
+ ```
104
+ sentinelnet/
105
+ β”œβ”€β”€ frontend/
106
+ β”‚ β”œβ”€β”€ index.html # Main HTML structure
107
+ β”‚ β”œβ”€β”€ style.css # All styles and CSS variables
108
+ β”‚ └── app.js # All JavaScript logic
109
+ β”œβ”€β”€ models/
110
+ β”‚ β”œβ”€β”€ sentinel_brain.joblib # Trained Random Forest model
111
+ β”‚ β”œβ”€β”€ label_encoder.joblib # Label encoder
112
+ β”‚ β”œβ”€β”€ ohe_encoder.joblib # One-hot encoder
113
+ β”‚ β”œβ”€β”€ freq_map.joblib # Service frequency map
114
+ β”‚ β”œβ”€β”€ scaler.joblib # Standard scaler
115
+ β”‚ └── selected_features.joblib # Selected feature list
116
+ β”œβ”€β”€ app.py # Flask backend + API
117
+ β”œβ”€β”€ requirements.txt # Python dependencies
118
+ └── Dockerfile # HuggingFace deployment config
119
+ ```
120
+
121
+ ---
122
+
123
+ ## πŸš€ Running Locally
124
+
125
+ **1. Clone the repo**
126
+ ```bash
127
+ git clone https://github.com/Hitan547/sentinelnet.git
128
+ cd sentinelnet
129
+ ```
130
+
131
+ **2. Install dependencies**
132
+ ```bash
133
+ pip install -r requirements.txt
134
+ ```
135
+
136
+ **3. Run the Flask server**
137
+ ```bash
138
+ python app.py
139
+ ```
140
+
141
+ **4. Open in browser**
142
+ ```
143
+ http://localhost:7860
144
+ ```
145
+
146
+ ---
147
+
148
+ ## πŸ”Œ API Reference
149
+
150
+ ### `POST /predict`
151
+ Accepts a batch of NSL-KDD rows and returns predictions.
152
+
153
+ **Request:**
154
+ ```json
155
+ {
156
+ "rows": [
157
+ {
158
+ "duration": 0,
159
+ "protocol_type": "tcp",
160
+ "service": "http",
161
+ "src_bytes": 181,
162
+ "dst_bytes": 5450,
163
+ ...
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ **Response:**
170
+ ```json
171
+ {
172
+ "status": "ok",
173
+ "results": [
174
+ {
175
+ "predicted_class": "normal",
176
+ "severity": "None",
177
+ "confidence": 0.9821,
178
+ "is_intrusion": false
179
+ }
180
+ ]
181
+ }
182
+ ```
183
+
184
+ ### `GET /health`
185
+ Returns model status.
186
+ ```json
187
+ {"status": "online", "model": "sentinel_brain"}
188
+ ```
189
+
190
+ ---
191
+
192
+ ## πŸ“Š Dataset
193
+
194
+ This project uses the **NSL-KDD dataset**, an improved version of the KDD Cup 1999 dataset for network intrusion detection research.
195
+
196
+ - Removes duplicate records from KDD Cup 99
197
+ - More balanced class distribution
198
+ - Widely used benchmark for IDS research
199
+ - 41 features per network connection record
200
+
201
+ ---
202
+
203
+ ## 🎯 What I Learned
204
+
205
+ - Training and deploying a multi-class classification model end to end
206
+ - Building a real-time dashboard with vanilla JavaScript
207
+ - Connecting a Flask API to a frontend with CORS handling
208
+ - Deploying on HuggingFace Spaces with Docker
209
+ - Performance optimization for large CSV batch processing
210
+ - Splitting a large frontend file for maintainability
211
+
212
  ---
213
+
214
+ ## πŸ“¬ Contact
215
+
216
+ **Hitan** β€” [GitHub](https://github.com/Hitan547)
217
+
 
 
218
  ---
219
 
220
+ ## πŸ“„ License
221
+
222
+ MIT License β€” feel free to use this project for learning or portfolio purposes.
__pycache__/app.cpython-310.pyc ADDED
Binary file (4.13 kB). View file
 
app.py CHANGED
@@ -4,19 +4,27 @@ import pandas as pd
4
  from flask import Flask, request, jsonify, send_from_directory
5
  from flask_cors import CORS
6
 
 
7
  app = Flask(__name__)
8
  CORS(app, origins="*")
9
 
10
  # ── Load all model artifacts ────────────────────────────────────────────────
11
  MODEL_DIR = os.path.join(os.path.dirname(__file__), 'models')
12
 
13
- sentinel_brain = joblib.load(os.path.join(MODEL_DIR, 'sentinel_brain.joblib'))
14
- le = joblib.load(os.path.join(MODEL_DIR, 'label_encoder.joblib'))
15
- ohe = joblib.load(os.path.join(MODEL_DIR, 'ohe_encoder.joblib'))
16
- freq_map = joblib.load(os.path.join(MODEL_DIR, 'freq_map.joblib'))
17
- scaler = joblib.load(os.path.join(MODEL_DIR, 'scaler.joblib'))
18
- selected_features = joblib.load(os.path.join(MODEL_DIR, 'selected_features.joblib'))
19
-
 
 
 
 
 
 
 
20
  COLUMNS = [
21
  'duration','protocol_type','service','flag','src_bytes','dst_bytes',
22
  'land','wrong_fragment','urgent','hot','num_failed_logins','logged_in',
@@ -72,20 +80,33 @@ def preprocess(df):
72
 
73
  @app.route('/health')
74
  def health():
75
- return jsonify({'status': 'online', 'model': 'sentinel_brain'})
 
 
 
76
 
77
  @app.route('/predict', methods=['POST', 'OPTIONS'])
78
  def predict():
79
  if request.method == 'OPTIONS':
80
  return jsonify({}), 200
 
 
 
 
 
 
 
81
  try:
82
  data = request.get_json(force=True)
83
  rows = data.get('rows', [])
84
  df = pd.DataFrame(rows)
85
- X = preprocess(df)
 
 
86
  preds = sentinel_brain.predict(X)
87
  proba = sentinel_brain.predict_proba(X)
88
  classes = le.inverse_transform(preds)
 
89
  results = [
90
  {
91
  'predicted_class': cls,
@@ -95,11 +116,16 @@ def predict():
95
  }
96
  for cls, conf in zip(classes, proba.max(axis=1))
97
  ]
 
98
  return jsonify({'status': 'ok', 'results': results})
 
99
  except Exception as e:
100
  import traceback
101
- return jsonify({'status': 'error', 'message': str(e),
102
- 'trace': traceback.format_exc()}), 500
 
 
 
103
 
104
  if __name__ == '__main__':
105
- app.run(host='0.0.0.0', port=7860)
 
4
  from flask import Flask, request, jsonify, send_from_directory
5
  from flask_cors import CORS
6
 
7
+ SKIP_MODEL = os.getenv("SKIP_MODEL", "false") == "true"
8
  app = Flask(__name__)
9
  CORS(app, origins="*")
10
 
11
  # ── Load all model artifacts ────────────────────────────────────────────────
12
  MODEL_DIR = os.path.join(os.path.dirname(__file__), 'models')
13
 
14
+ if not SKIP_MODEL:
15
+ sentinel_brain = joblib.load(os.path.join(MODEL_DIR, 'sentinel_brain.joblib'))
16
+ le = joblib.load(os.path.join(MODEL_DIR, 'label_encoder.joblib'))
17
+ ohe = joblib.load(os.path.join(MODEL_DIR, 'ohe_encoder.joblib'))
18
+ freq_map = joblib.load(os.path.join(MODEL_DIR, 'freq_map.joblib'))
19
+ scaler = joblib.load(os.path.join(MODEL_DIR, 'scaler.joblib'))
20
+ selected_features = joblib.load(os.path.join(MODEL_DIR, 'selected_features.joblib'))
21
+ else:
22
+ sentinel_brain = None
23
+ le = None
24
+ ohe = None
25
+ freq_map = {}
26
+ scaler = None
27
+ selected_features = []
28
  COLUMNS = [
29
  'duration','protocol_type','service','flag','src_bytes','dst_bytes',
30
  'land','wrong_fragment','urgent','hot','num_failed_logins','logged_in',
 
80
 
81
  @app.route('/health')
82
  def health():
83
+ return jsonify({
84
+ 'status': 'online',
85
+ 'model_loaded': not SKIP_MODEL
86
+ })
87
 
88
  @app.route('/predict', methods=['POST', 'OPTIONS'])
89
  def predict():
90
  if request.method == 'OPTIONS':
91
  return jsonify({}), 200
92
+
93
+ if SKIP_MODEL:
94
+ return jsonify({
95
+ 'status': 'error',
96
+ 'message': 'Model not loaded (CI mode)'
97
+ }), 503
98
+
99
  try:
100
  data = request.get_json(force=True)
101
  rows = data.get('rows', [])
102
  df = pd.DataFrame(rows)
103
+
104
+ X = preprocess(df)
105
+
106
  preds = sentinel_brain.predict(X)
107
  proba = sentinel_brain.predict_proba(X)
108
  classes = le.inverse_transform(preds)
109
+
110
  results = [
111
  {
112
  'predicted_class': cls,
 
116
  }
117
  for cls, conf in zip(classes, proba.max(axis=1))
118
  ]
119
+
120
  return jsonify({'status': 'ok', 'results': results})
121
+
122
  except Exception as e:
123
  import traceback
124
+ return jsonify({
125
+ 'status': 'error',
126
+ 'message': str(e),
127
+ 'trace': traceback.format_exc()
128
+ }), 500
129
 
130
  if __name__ == '__main__':
131
+ app.run(host='0.0.0.0', port=7860)
data/KDDTest+.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
data/KDDTrain+.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
experiments/01_eda_preprocessing.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
experiments/02_feature_engineering.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
hf_space/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
hf_space/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD gunicorn --bind 0.0.0.0:7860 --timeout 120 app:app
hf_space/README.md ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ›‘ SentinelNet β€” AI-Powered Network Intrusion Detection System
2
+
3
+ ![Python](https://img.shields.io/badge/Python-3.10-blue?style=flat-square&logo=python)
4
+ ![Flask](https://img.shields.io/badge/Flask-2.x-black?style=flat-square&logo=flask)
5
+ ![scikit-learn](https://img.shields.io/badge/scikit--learn-1.6-orange?style=flat-square&logo=scikit-learn)
6
+ ![HuggingFace](https://img.shields.io/badge/HuggingFace-Spaces-yellow?style=flat-square&logo=huggingface)
7
+ ![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)
8
+
9
+ > A real-time network intrusion detection dashboard powered by a Random Forest classifier trained on the NSL-KDD dataset. Detects 5 categories of network threats with live visualization and batch CSV analysis.
10
+
11
+ πŸ”΄ **Live Demo:** [https://huggingface.co/spaces/Hitan2004/sentinelnet](https://huggingface.co/spaces/Hitan2004/sentinelnet)
12
+
13
+ ---
14
+
15
+ ## πŸ“Œ What It Does
16
+
17
+ SentinelNet analyzes network traffic and classifies each connection as one of 5 categories:
18
+
19
+ | Class | Type | Severity |
20
+ |-------|------|----------|
21
+ | `normal` | Clean traffic | None |
22
+ | `DoS` | Denial of Service attack | Critical |
23
+ | `Probe` | Reconnaissance / Port scanning | Medium |
24
+ | `R2L` | Remote to Local attack | High |
25
+ | `U2R` | User to Root / Privilege escalation | Critical |
26
+
27
+ ---
28
+
29
+ ## ✨ Features
30
+
31
+ ### πŸ“‘ Live Monitor Tab
32
+ - Auto-generates NSL-KDD formatted network packets
33
+ - Sends each packet to the trained Random Forest model in real time
34
+ - Displays live detection feed with class, confidence, and severity
35
+ - Attack distribution bar chart updated in real time
36
+ - Threat timeline chart (last 60 seconds)
37
+ - Activity heatmap of last 60 packets
38
+ - Confidence distribution panel
39
+ - System log terminal
40
+ - Session summary stats
41
+
42
+ ### πŸ“‚ CSV Analysis Tab
43
+ - Upload any NSL-KDD formatted CSV file
44
+ - Auto-detects headers (with or without column names)
45
+ - Processes rows in batches through the model
46
+ - Live progress bar with ETA and processing speed
47
+ - Row-by-row feed showing predictions as they come in
48
+ - On completion generates a full threat report including:
49
+ - Risk score gauge (0–100)
50
+ - Class distribution bar chart
51
+ - Confidence waveform over dataset
52
+ - Threat intensity rolling chart
53
+ - Protocol breakdown
54
+ - Top targeted services
55
+ - Attack pattern clusters
56
+ - Paginated full results table
57
+ - Export results as **Annotated CSV**, **PDF Report**, or **JSON**
58
+
59
+ ---
60
+
61
+ ## 🧠 Model Details
62
+
63
+ | Property | Value |
64
+ |----------|-------|
65
+ | Algorithm | Random Forest Classifier |
66
+ | Dataset | NSL-KDD (improved KDD Cup 1999) |
67
+ | Features | 41 network connection features |
68
+ | Classes | 5 (normal, DoS, Probe, R2L, U2R) |
69
+ | Preprocessing | OHE encoding, frequency encoding, log transforms, standard scaling |
70
+ | Deployment | HuggingFace Spaces (Flask API) |
71
+
72
+ ### Preprocessing Pipeline
73
+ 1. One-hot encode `protocol_type` and `flag`
74
+ 2. Frequency encode `service` column
75
+ 3. Log transform `src_bytes`, `dst_bytes`, `duration`
76
+ 4. Engineer features: `total_bytes`, `src_bytes_ratio`, `is_error_flag`
77
+ 5. Standard scale all selected features
78
+
79
+ ---
80
+
81
+ ## πŸ— Tech Stack
82
+
83
+ **Backend**
84
+ - Python 3.10
85
+ - Flask + Flask-CORS
86
+ - scikit-learn (Random Forest)
87
+ - pandas, numpy, joblib
88
+
89
+ **Frontend**
90
+ - Vanilla HTML/CSS/JavaScript (no frameworks)
91
+ - IBM Plex Mono + Space Grotesk fonts
92
+ - Canvas API for charts
93
+ - Split into 3 files: `index.html`, `style.css`, `app.js`
94
+
95
+ **Deployment**
96
+ - HuggingFace Spaces (Docker)
97
+ - Flask serves both the frontend and the `/predict` API
98
+
99
+ ---
100
+
101
+ ## πŸ“ Project Structure
102
+
103
+ ```
104
+ sentinelnet/
105
+ β”œβ”€β”€ frontend/
106
+ β”‚ β”œβ”€β”€ index.html # Main HTML structure
107
+ β”‚ β”œβ”€β”€ style.css # All styles and CSS variables
108
+ β”‚ └── app.js # All JavaScript logic
109
+ β”œβ”€β”€ models/
110
+ β”‚ β”œβ”€β”€ sentinel_brain.joblib # Trained Random Forest model
111
+ β”‚ β”œβ”€β”€ label_encoder.joblib # Label encoder
112
+ β”‚ β”œβ”€β”€ ohe_encoder.joblib # One-hot encoder
113
+ β”‚ β”œβ”€β”€ freq_map.joblib # Service frequency map
114
+ β”‚ β”œβ”€β”€ scaler.joblib # Standard scaler
115
+ β”‚ └── selected_features.joblib # Selected feature list
116
+ β”œβ”€β”€ app.py # Flask backend + API
117
+ β”œβ”€β”€ requirements.txt # Python dependencies
118
+ └── Dockerfile # HuggingFace deployment config
119
+ ```
120
+
121
+ ---
122
+
123
+ ## πŸš€ Running Locally
124
+
125
+ **1. Clone the repo**
126
+ ```bash
127
+ git clone https://github.com/Hitan547/sentinelnet.git
128
+ cd sentinelnet
129
+ ```
130
+
131
+ **2. Install dependencies**
132
+ ```bash
133
+ pip install -r requirements.txt
134
+ ```
135
+
136
+ **3. Run the Flask server**
137
+ ```bash
138
+ python app.py
139
+ ```
140
+
141
+ **4. Open in browser**
142
+ ```
143
+ http://localhost:7860
144
+ ```
145
+
146
+ ---
147
+
148
+ ## πŸ”Œ API Reference
149
+
150
+ ### `POST /predict`
151
+ Accepts a batch of NSL-KDD rows and returns predictions.
152
+
153
+ **Request:**
154
+ ```json
155
+ {
156
+ "rows": [
157
+ {
158
+ "duration": 0,
159
+ "protocol_type": "tcp",
160
+ "service": "http",
161
+ "src_bytes": 181,
162
+ "dst_bytes": 5450,
163
+ ...
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ **Response:**
170
+ ```json
171
+ {
172
+ "status": "ok",
173
+ "results": [
174
+ {
175
+ "predicted_class": "normal",
176
+ "severity": "None",
177
+ "confidence": 0.9821,
178
+ "is_intrusion": false
179
+ }
180
+ ]
181
+ }
182
+ ```
183
+
184
+ ### `GET /health`
185
+ Returns model status.
186
+ ```json
187
+ {"status": "online", "model": "sentinel_brain"}
188
+ ```
189
+
190
+ ---
191
+
192
+ ## πŸ“Š Dataset
193
+
194
+ This project uses the **NSL-KDD dataset**, an improved version of the KDD Cup 1999 dataset for network intrusion detection research.
195
+
196
+ - Removes duplicate records from KDD Cup 99
197
+ - More balanced class distribution
198
+ - Widely used benchmark for IDS research
199
+ - 41 features per network connection record
200
+
201
+ ---
202
+
203
+ ## 🎯 What I Learned
204
+
205
+ - Training and deploying a multi-class classification model end to end
206
+ - Building a real-time dashboard with vanilla JavaScript
207
+ - Connecting a Flask API to a frontend with CORS handling
208
+ - Deploying on HuggingFace Spaces with Docker
209
+ - Performance optimization for large CSV batch processing
210
+ - Splitting a large frontend file for maintainability
211
+
212
+ ---
213
+
214
+ ## πŸ“¬ Contact
215
+
216
+ **Hitan** β€” [GitHub](https://github.com/Hitan547)
217
+
218
+ ---
219
+
220
+ ## πŸ“„ License
221
+
222
+ MIT License β€” feel free to use this project for learning or portfolio purposes.
hf_space/app.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, joblib
2
+ import numpy as np
3
+ import pandas as pd
4
+ from flask import Flask, request, jsonify, send_from_directory
5
+ from flask_cors import CORS
6
+
7
+ SKIP_MODEL = os.getenv("SKIP_MODEL", "false") == "true"
8
+ app = Flask(__name__)
9
+ CORS(app, origins="*")
10
+
11
+ # ── Load all model artifacts ────────────────────────────────────────────────
12
+ MODEL_DIR = os.path.join(os.path.dirname(__file__), 'models')
13
+
14
+ if not SKIP_MODEL:
15
+ sentinel_brain = joblib.load(os.path.join(MODEL_DIR, 'sentinel_brain.joblib'))
16
+ le = joblib.load(os.path.join(MODEL_DIR, 'label_encoder.joblib'))
17
+ ohe = joblib.load(os.path.join(MODEL_DIR, 'ohe_encoder.joblib'))
18
+ freq_map = joblib.load(os.path.join(MODEL_DIR, 'freq_map.joblib'))
19
+ scaler = joblib.load(os.path.join(MODEL_DIR, 'scaler.joblib'))
20
+ selected_features = joblib.load(os.path.join(MODEL_DIR, 'selected_features.joblib'))
21
+ else:
22
+ sentinel_brain = None
23
+ le = None
24
+ ohe = None
25
+ freq_map = {}
26
+ scaler = None
27
+ selected_features = []
28
+ COLUMNS = [
29
+ 'duration','protocol_type','service','flag','src_bytes','dst_bytes',
30
+ 'land','wrong_fragment','urgent','hot','num_failed_logins','logged_in',
31
+ 'num_compromised','root_shell','su_attempted','num_root','num_file_creations',
32
+ 'num_shells','num_access_files','num_outbound_cmds','is_host_login',
33
+ 'is_guest_login','count','srv_count','serror_rate','srv_serror_rate',
34
+ 'rerror_rate','srv_rerror_rate','same_srv_rate','diff_srv_rate',
35
+ 'srv_diff_host_rate','dst_host_count','dst_host_srv_count',
36
+ 'dst_host_same_srv_rate','dst_host_diff_srv_rate','dst_host_same_src_port_rate',
37
+ 'dst_host_srv_diff_host_rate','dst_host_serror_rate','dst_host_srv_serror_rate',
38
+ 'dst_host_rerror_rate','dst_host_srv_rerror_rate','label','difficulty_level'
39
+ ]
40
+ SEVERITY_MAP = {'normal':'None','DoS':'Critical','Probe':'Medium','R2L':'High','U2R':'Critical'}
41
+
42
+ # ── Serve frontend ──────────────────────────────────────────────────────────
43
+ @app.route("/")
44
+ def index():
45
+ return send_from_directory("frontend", "index.html")
46
+
47
+ # ── THIS IS THE KEY FIX: serve style.css, app.js, and any other static files
48
+ @app.route("/<path:filename>")
49
+ def static_files(filename):
50
+ return send_from_directory("frontend", filename)
51
+
52
+ # ── Everything below is UNCHANGED ──────────────────────────────────────────
53
+
54
+ def preprocess(df):
55
+ df = df.copy()
56
+ for col in COLUMNS:
57
+ if col not in df.columns:
58
+ df[col] = 0
59
+ if 'label' not in df.columns:
60
+ df['label'] = 'normal'
61
+ cats = ['protocol_type', 'flag']
62
+ enc_df = pd.DataFrame(
63
+ ohe.transform(df[cats]),
64
+ columns=ohe.get_feature_names_out(cats),
65
+ index=df.index
66
+ )
67
+ df = pd.concat([df, enc_df], axis=1)
68
+ df['service_freq'] = df['service'].map(freq_map).fillna(0)
69
+ for col in ['src_bytes', 'dst_bytes', 'duration']:
70
+ df[f'log_{col}'] = np.log1p(df[col].astype(float))
71
+ df['total_bytes'] = df['src_bytes'].astype(float) + df['dst_bytes'].astype(float)
72
+ df['src_bytes_ratio'] = df['src_bytes'].astype(float) / (df['total_bytes'] + 1e-5)
73
+ df['is_error_flag'] = df['flag'].isin(['S0','S1','S2','S3','REJ']).astype(int)
74
+ for f in selected_features:
75
+ if f not in df.columns:
76
+ df[f] = 0
77
+ feature_matrix = df[selected_features].values
78
+ feature_matrix = scaler.transform(feature_matrix)
79
+ return feature_matrix
80
+
81
+ @app.route('/health')
82
+ def health():
83
+ return jsonify({
84
+ 'status': 'online',
85
+ 'model_loaded': not SKIP_MODEL
86
+ })
87
+
88
+ @app.route('/predict', methods=['POST', 'OPTIONS'])
89
+ def predict():
90
+ if request.method == 'OPTIONS':
91
+ return jsonify({}), 200
92
+
93
+ if SKIP_MODEL:
94
+ return jsonify({
95
+ 'status': 'error',
96
+ 'message': 'Model not loaded (CI mode)'
97
+ }), 503
98
+
99
+ try:
100
+ data = request.get_json(force=True)
101
+ rows = data.get('rows', [])
102
+ df = pd.DataFrame(rows)
103
+
104
+ X = preprocess(df)
105
+
106
+ preds = sentinel_brain.predict(X)
107
+ proba = sentinel_brain.predict_proba(X)
108
+ classes = le.inverse_transform(preds)
109
+
110
+ results = [
111
+ {
112
+ 'predicted_class': cls,
113
+ 'severity': SEVERITY_MAP.get(cls, 'Unknown'),
114
+ 'confidence': round(float(conf), 4),
115
+ 'is_intrusion': cls != 'normal'
116
+ }
117
+ for cls, conf in zip(classes, proba.max(axis=1))
118
+ ]
119
+
120
+ return jsonify({'status': 'ok', 'results': results})
121
+
122
+ except Exception as e:
123
+ import traceback
124
+ return jsonify({
125
+ 'status': 'error',
126
+ 'message': str(e),
127
+ 'trace': traceback.format_exc()
128
+ }), 500
129
+
130
+ if __name__ == '__main__':
131
+ app.run(host='0.0.0.0', port=7860)
hf_space/frontend/app.js ADDED
@@ -0,0 +1,873 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════
2
+ // CONFIG β€” change BACKEND_URL to your HF Space URL
3
+ // e.g. 'https://hitan2004-sentinelnet.hf.space'
4
+ // ════════════════════════════════════════════════
5
+ const BACKEND_URL = 'https://hitan2004-sentinelnet.hf.space';
6
+ const BATCH_SIZE = 50;
7
+ const PAGE_SIZE = 100;
8
+
9
+ // ════════════════════════════════════════════════
10
+ // CONSTANTS
11
+ // ════════════════════════════════════════════════
12
+ const NSL_KDD_COLUMNS = [
13
+ 'duration','protocol_type','service','flag','src_bytes','dst_bytes',
14
+ 'land','wrong_fragment','urgent','hot','num_failed_logins','logged_in',
15
+ 'num_compromised','root_shell','su_attempted','num_root','num_file_creations',
16
+ 'num_shells','num_access_files','num_outbound_cmds','is_host_login','is_guest_login',
17
+ 'count','srv_count','serror_rate','srv_serror_rate','rerror_rate','srv_rerror_rate',
18
+ 'same_srv_rate','diff_srv_rate','srv_diff_host_rate','dst_host_count','dst_host_srv_count',
19
+ 'dst_host_same_srv_rate','dst_host_diff_srv_rate','dst_host_same_src_port_rate',
20
+ 'dst_host_srv_diff_host_rate','dst_host_serror_rate','dst_host_srv_serror_rate',
21
+ 'dst_host_rerror_rate','dst_host_srv_rerror_rate','label','difficulty_level'
22
+ ];
23
+
24
+ const STRING_COLS = new Set(['protocol_type','service','flag','label']);
25
+
26
+ const ATTACK_MAP = {
27
+ normal:'normal',
28
+ back:'DoS',land:'DoS',neptune:'DoS',pod:'DoS',smurf:'DoS',teardrop:'DoS',
29
+ mailbomb:'DoS',apache2:'DoS',processtable:'DoS',udpstorm:'DoS',
30
+ satan:'Probe',ipsweep:'Probe',nmap:'Probe',portsweep:'Probe',mscan:'Probe',saint:'Probe',
31
+ guess_passwd:'R2L',ftp_write:'R2L',imap:'R2L',phf:'R2L',multihop:'R2L',
32
+ warezmaster:'R2L',warezclient:'R2L',spy:'R2L',xlock:'R2L',xsnoop:'R2L',
33
+ snmpguess:'R2L',snmpgetattack:'R2L',httptunnel:'R2L',sendmail:'R2L',named:'R2L',
34
+ buffer_overflow:'U2R',loadmodule:'U2R',perl:'U2R',rootkit:'U2R',
35
+ ps:'U2R',xterm:'U2R',sqlattack:'U2R'
36
+ };
37
+
38
+ const SEV_MAP = { normal:'None', DoS:'Critical', Probe:'Medium', R2L:'High', U2R:'Critical' };
39
+ const SEV_COLOR = { None:'#00e87a', Medium:'#00c8e8', High:'#ffaa00', Critical:'#ff3d5a' };
40
+ const CLASS_COLOR = { normal:'#00e87a', DoS:'#ff3d5a', Probe:'#00c8e8', R2L:'#ffaa00', U2R:'#b06fff' };
41
+ const PROTOCOLS = ['tcp','udp','icmp'];
42
+ const SERVICES = ['http','ftp','smtp','ssh','dns','telnet','pop3','imap4','finger','auth'];
43
+ const LABEL_POOL = [
44
+ ...Array(60).fill('normal'),
45
+ ...Array(12).fill('neptune'), ...Array(6).fill('smurf'), ...Array(4).fill('back'),
46
+ ...Array(5).fill('ipsweep'), ...Array(4).fill('satan'), ...Array(3).fill('portsweep'),
47
+ ...Array(2).fill('guess_passwd'), ...Array(1).fill('buffer_overflow'), ...Array(1).fill('rootkit')
48
+ ];
49
+
50
+ // ════════════════════════════════════════════════
51
+ // LIVE MONITOR STATE
52
+ // ════════════════════════════════════════════════
53
+ let monitorInterval = null, sessionInterval = null;
54
+ let sessionSeconds = 0, isRunning = false, usingRealModel = false, packetId = 0;
55
+ const counts = { normal:0, DoS:0, Probe:0, R2L:0, U2R:0 };
56
+ let totalPackets = 0, totalIntrusions = 0, confSum = 0, peakClass = null;
57
+ let confBuckets = { 90:0, 80:0, 70:0, low:0 };
58
+ let timelineBuckets = Array(60).fill(0), heatmapCells = Array(60).fill(null);
59
+ let tlDirty = false;
60
+
61
+ // ════════════════════════════════════════════════
62
+ // CSV STATE
63
+ // ════════════════════════════════════════════════
64
+ let csvRows = [], csvResults = [], csvIndex = 0, csvRunning = false, csvStartTime = null;
65
+ let csvCounts = { normal:0, DoS:0, Probe:0, R2L:0, U2R:0 };
66
+ let csvSevCounts = { Critical:0, High:0, Medium:0, None:0 };
67
+ let csvConfSum = 0, csvIntrusionCount = 0, csvConfHistory = [];
68
+ let csvUsingReal = false, csvFormatInfo = '';
69
+ let batchNum = 0, totalBatches = 0, reportPage = 0;
70
+
71
+ // ════════════════════════════════════════════════
72
+ // TAB SWITCHER
73
+ // ════════════════════════════════════════════════
74
+ function switchTab(name, btn) {
75
+ document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
76
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
77
+ document.getElementById('tab-' + name).classList.add('active');
78
+ btn.classList.add('active');
79
+ }
80
+
81
+ // ════════════════════════════════════════════════
82
+ // CSV PARSER β€” auto header detection
83
+ // ════════════════════════════════════════════════
84
+ function parseCSV(text) {
85
+ const lines = text.trim().split('\n').filter(l => l.trim());
86
+ if (!lines.length) return { rows:[], hasHeader:false, cols:0 };
87
+
88
+ function splitLine(line) {
89
+ const vals = []; let cur = '', inQ = false;
90
+ for (const c of line) {
91
+ if (c === '"') inQ = !inQ;
92
+ else if (c === ',' && !inQ) { vals.push(cur.trim()); cur = ''; }
93
+ else cur += c;
94
+ }
95
+ vals.push(cur.trim());
96
+ return vals;
97
+ }
98
+
99
+ const firstVals = splitLine(lines[0]);
100
+ const knownCols = new Set(NSL_KDD_COLUMNS);
101
+ const looksLikeHeader = firstVals.some(v => knownCols.has(v.toLowerCase().replace(/^"|"$/g, '')));
102
+ let headers, dataLines;
103
+
104
+ if (looksLikeHeader) {
105
+ headers = firstVals.map(h => h.toLowerCase().replace(/^"|"$/g, '').trim());
106
+ dataLines = lines.slice(1);
107
+ } else {
108
+ headers = NSL_KDD_COLUMNS.slice(0, firstVals.length);
109
+ dataLines = lines;
110
+ }
111
+
112
+ const rows = [];
113
+ for (const line of dataLines) {
114
+ const vals = splitLine(line);
115
+ if (vals.length < 2) continue;
116
+ const obj = {};
117
+ headers.forEach((h, i) => {
118
+ let v = (vals[i] || '').trim().replace(/^"|"$/g, '');
119
+ obj[h] = STRING_COLS.has(h) ? v : (v === '' ? 0 : (isNaN(v) ? v : parseFloat(v)));
120
+ });
121
+ rows.push(obj);
122
+ }
123
+ return { rows, hasHeader: looksLikeHeader, cols: headers.length };
124
+ }
125
+
126
+ // ════════════════════════════════════════════════
127
+ // LOCAL CLASSIFIER β€” uses label col + feature heuristics
128
+ // ════════════════════════════════════════════════
129
+ function classifyLocal(row) {
130
+ const rawLabel = (row.label || '').toString().toLowerCase().trim().replace(/\.$/, '');
131
+ if (rawLabel && rawLabel !== 'unknown') {
132
+ const mc = ATTACK_MAP[rawLabel];
133
+ if (mc) {
134
+ const base = { normal:0.88, DoS:0.91, Probe:0.84, R2L:0.79, U2R:0.82 }[mc] || 0.80;
135
+ const conf = Math.min(0.99, base + (Math.random() * 0.08 - 0.04));
136
+ return { predicted_class:mc, severity:SEV_MAP[mc], confidence:+conf.toFixed(4), is_intrusion:mc !== 'normal' };
137
+ }
138
+ }
139
+ const srcBytes = parseFloat(row.src_bytes) || 0;
140
+ const flag = (row.flag || '').toUpperCase();
141
+ const serrorRate = parseFloat(row.serror_rate) || 0;
142
+ const rerrorRate = parseFloat(row.rerror_rate) || 0;
143
+ const srvCount = parseFloat(row.srv_count) || 0;
144
+ const count = parseFloat(row.count) || 0;
145
+ const loggedIn = parseFloat(row.logged_in) || 0;
146
+ const numRoot = parseFloat(row.num_root) || 0;
147
+ const rootShell = parseFloat(row.root_shell) || 0;
148
+
149
+ let cls = 'normal', conf = 0.75 + Math.random() * 0.15;
150
+ if (['S0','S1','S2','S3','REJ','RSTO','RSTR'].includes(flag) && count > 100) { cls = 'DoS'; conf = 0.85 + Math.random() * 0.1; }
151
+ else if (srcBytes > 50000 && (parseFloat(row.duration) || 0) < 5) { cls = 'DoS'; conf = 0.80 + Math.random() * 0.12; }
152
+ else if (serrorRate > 0.7 || rerrorRate > 0.7) { cls = 'DoS'; conf = 0.78 + Math.random() * 0.1; }
153
+ else if (srvCount > 100 && srcBytes < 500 && loggedIn === 0) { cls = 'Probe'; conf = 0.80 + Math.random() * 0.12; }
154
+ else if (loggedIn === 1 && (row.num_failed_logins || 0) > 0 && srcBytes < 10000) { cls = 'R2L'; conf = 0.75 + Math.random() * 0.12; }
155
+ else if (rootShell > 0 || numRoot > 0) { cls = 'U2R'; conf = 0.82 + Math.random() * 0.12; }
156
+
157
+ return { predicted_class:cls, severity:SEV_MAP[cls], confidence:+Math.min(0.99, conf).toFixed(4), is_intrusion:cls !== 'normal' };
158
+ }
159
+
160
+ // ════════════════════════════════════════════════
161
+ // API CALLS
162
+ // ════════════════════════════════════════════════
163
+ async function predictBatch(rows) {
164
+ try {
165
+ const r = await fetch(BACKEND_URL + '/predict', {
166
+ method: 'POST',
167
+ headers: { 'Content-Type':'application/json', 'ngrok-skip-browser-warning':'true' },
168
+ body: JSON.stringify({ rows }),
169
+ signal: AbortSignal.timeout(30000)
170
+ });
171
+ if (!r.ok) return null;
172
+ const d = await r.json();
173
+ return (d.status === 'ok' && Array.isArray(d.results)) ? d.results : null;
174
+ } catch { return null; }
175
+ }
176
+
177
+ async function predictSingle(row) {
178
+ try {
179
+ const r = await fetch(BACKEND_URL + '/predict', {
180
+ method: 'POST',
181
+ headers: { 'Content-Type':'application/json', 'ngrok-skip-browser-warning':'true' },
182
+ body: JSON.stringify({ rows:[row] }),
183
+ signal: AbortSignal.timeout(15000)
184
+ });
185
+ if (!r.ok) return null;
186
+ const d = await r.json();
187
+ return (d.status === 'ok' && d.results?.[0]) ? d.results[0] : null;
188
+ } catch { return null; }
189
+ }
190
+
191
+ // ════════════════════════════════════════════════
192
+ // FILE UPLOAD
193
+ // ════════════════════════════════════════════════
194
+ const uploadZone = document.getElementById('uploadZone');
195
+ uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
196
+ uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
197
+ uploadZone.addEventListener('drop', e => {
198
+ e.preventDefault(); uploadZone.classList.remove('drag-over');
199
+ const f = e.dataTransfer.files[0];
200
+ if (f && f.name.endsWith('.csv')) processFileUpload(f);
201
+ });
202
+
203
+ function handleFileSelect(e) { const f = e.target.files[0]; if (f) processFileUpload(f); }
204
+
205
+ function processFileUpload(file) {
206
+ const reader = new FileReader();
207
+ reader.onload = e => {
208
+ const { rows, hasHeader, cols } = parseCSV(e.target.result);
209
+ if (!rows.length) { alert('Could not parse CSV.'); return; }
210
+ csvRows = rows;
211
+ csvFormatInfo = hasHeader ? `With headers Β· ${cols} columns` : `Headerless β€” auto-mapped Β· ${cols} columns`;
212
+ totalBatches = Math.ceil(rows.length / BATCH_SIZE);
213
+
214
+ const banner = document.getElementById('formatBanner');
215
+ banner.style.display = 'flex'; banner.className = 'format-banner ok';
216
+ banner.innerHTML = `βœ“ ${hasHeader ? 'Headers detected' : 'Headerless β€” NSL-KDD auto-mapped'} Β· ${cols} columns Β· ${rows.length.toLocaleString()} rows Β· ${totalBatches} batches`;
217
+
218
+ document.getElementById('csvUploadSection').style.display = 'none';
219
+ document.getElementById('csvProcessingArea').classList.add('visible');
220
+ setText('csvFileName', file.name);
221
+ setText('csvFileMeta', `${rows.length.toLocaleString()} rows Β· ${(file.size/1024).toFixed(1)} KB Β· ${csvFormatInfo}`);
222
+
223
+ csvResults = []; csvIndex = 0; csvConfSum = 0; csvIntrusionCount = 0;
224
+ csvConfHistory = []; batchNum = 0;
225
+ Object.keys(csvCounts).forEach(k => csvCounts[k] = 0);
226
+ Object.keys(csvSevCounts).forEach(k => csvSevCounts[k] = 0);
227
+ csvUsingReal = false; csvStartTime = null;
228
+ };
229
+ reader.readAsText(file);
230
+ }
231
+
232
+ // ════════════════════════════════════════════════
233
+ // CSV BATCH ENGINE
234
+ // ════════════════════════════════════════════════
235
+ async function startCsvAnalysis() {
236
+ if (csvRunning || csvIndex >= csvRows.length) return;
237
+ csvRunning = true; csvStartTime = csvStartTime || Date.now();
238
+ document.getElementById('csvStartBtn').disabled = true;
239
+ document.getElementById('csvStopBtn').disabled = false;
240
+ document.getElementById('csvProgressBlock').style.display = 'block';
241
+ document.getElementById('csvLiveGrid').style.display = 'grid';
242
+ document.getElementById('reportSection').classList.remove('visible');
243
+ document.getElementById('liveDot').className = 'dot amber';
244
+ setText('liveStatus', 'SCANNING');
245
+ await processBatches();
246
+ }
247
+
248
+ async function processBatches() {
249
+ while (csvRunning && csvIndex < csvRows.length) {
250
+ const bStart = csvIndex, bEnd = Math.min(csvIndex + BATCH_SIZE, csvRows.length);
251
+ const batch = csvRows.slice(bStart, bEnd);
252
+ batchNum++;
253
+
254
+ if (batchNum === 1 || batchNum % 5 === 0 || batchNum === totalBatches)
255
+ updateBatchChips(batchNum, totalBatches);
256
+ setText('csvCurrentRow', `Batch ${batchNum}/${totalBatches} β€” rows ${(bStart+1).toLocaleString()}–${bEnd.toLocaleString()}`);
257
+
258
+ let results = await predictBatch(batch);
259
+ if (results) {
260
+ if (!csvUsingReal) { csvUsingReal = true; setConnBadge('real'); }
261
+ } else {
262
+ if (csvUsingReal || batchNum === 1) { csvUsingReal = false; setConnBadge('local'); }
263
+ results = batch.map(r => classifyLocal(r));
264
+ }
265
+
266
+ for (let i = 0; i < batch.length; i++) {
267
+ const { predicted_class:cls, confidence:conf, severity:sev, is_intrusion:isI } = results[i];
268
+ csvResults.push({ rowNum:bStart+i+1, row:batch[i], cls, conf, sev, isI });
269
+ if (csvResults.length % 5 === 0) csvConfHistory.push(conf);
270
+ csvCounts[cls] = (csvCounts[cls] || 0) + 1;
271
+ csvSevCounts[sev]= (csvSevCounts[sev]|| 0) + 1;
272
+ csvConfSum += conf;
273
+ if (isI) csvIntrusionCount++;
274
+ }
275
+
276
+ // Show last 5 rows in feed
277
+ const feedSlice = batch.slice(-2);
278
+ feedSlice.forEach((row, i) => {
279
+ const ri = bEnd - feedSlice.length + i;
280
+ addCsvFeedRow(ri+1, row, results[batch.length-feedSlice.length+i].predicted_class,
281
+ results[batch.length-feedSlice.length+i].confidence,
282
+ results[batch.length-feedSlice.length+i].severity);
283
+ });
284
+
285
+ csvIndex = bEnd;
286
+ const pct = (csvIndex / csvRows.length * 100).toFixed(1);
287
+ const elapsed = (Date.now() - csvStartTime) / 1000;
288
+ const rate = csvIndex / Math.max(elapsed, 0.01);
289
+ const remaining= (csvRows.length - csvIndex) / Math.max(rate, 0.1);
290
+
291
+ setText('csvProgressStats', `${csvIndex.toLocaleString()} / ${csvRows.length.toLocaleString()} rows`);
292
+ setText('csvProgressPct', pct + '%');
293
+ setText('csvThreatRate', `Threats: ${csvIntrusionCount.toLocaleString()}`);
294
+ setText('csvProgressEta', csvIndex < csvRows.length ? `ETA: ${formatETA(remaining)}` : 'Done!');
295
+ setText('csvSpeedStat', Math.round(rate) + ' rows/s');
296
+ document.getElementById('csvProgressFill').style.width = pct + '%';
297
+ if (csvIntrusionCount / Math.max(csvIndex,1) > 0.5)
298
+ document.getElementById('csvProgressFill').classList.add('warning');
299
+
300
+ if (batchNum % 5 === 0 || batchNum === totalBatches) updateCsvSidebar(rate);
301
+ setText('csvAlertCount', csvIntrusionCount.toLocaleString() + ' THREATS');
302
+ await new Promise(r => setTimeout(r, 100));
303
+ }
304
+ if (csvIndex >= csvRows.length) finishCsvAnalysis();
305
+ }
306
+
307
+ function setConnBadge(type) {
308
+ const el = document.getElementById('connBadge');
309
+ if (type === 'real') { el.textContent = 'βœ“ REAL MODEL'; el.className = 'real'; }
310
+ else { el.textContent = '⚠ LOCAL SIM'; el.className = 'local'; }
311
+ }
312
+
313
+ function updateBatchChips(current, total) {
314
+ const el = document.getElementById('batchStatus'); el.innerHTML = '';
315
+ const show = Math.min(total, 12);
316
+ for (let i = 1; i <= show; i++) {
317
+ const chip = document.createElement('div');
318
+ chip.className = 'batch-chip' + (i < current ? ' done' : i === current ? ' active' : '');
319
+ chip.textContent = i < current ? `βœ“${i}` : i === current ? `⟳${i}` : `${i}`;
320
+ el.appendChild(chip);
321
+ }
322
+ if (total > show) {
323
+ const chip = document.createElement('div');
324
+ chip.className = 'batch-chip';
325
+ chip.textContent = `+${total-show} more`;
326
+ el.appendChild(chip);
327
+ }
328
+ }
329
+
330
+ function addCsvFeedRow(rowNum, row, cls, conf, sev) {
331
+ const tbody = document.getElementById('csvFeedBody');
332
+ const tr = document.createElement('tr'); tr.className = 'csv-new-row';
333
+ if (cls !== 'normal') tr.style.background = 'rgba(255,61,90,0.025)';
334
+ tr.innerHTML = `
335
+ <td style="color:var(--muted)">${rowNum}</td>
336
+ <td style="color:var(--cyan)">${row.protocol_type||'β€”'}</td>
337
+ <td>${row.service||'β€”'}</td>
338
+ <td>${(row.src_bytes||0).toLocaleString()}</td>
339
+ <td><span class="cls-badge cls-${cls}">${cls}</span></td>
340
+ <td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td>
341
+ <td style="color:${SEV_COLOR[sev]}">● ${sev}</td>`;
342
+ tbody.insertBefore(tr, tbody.firstChild);
343
+ while (tbody.children.length > 50) tbody.removeChild(tbody.lastChild);
344
+ }
345
+
346
+ function updateCsvSidebar(rate) {
347
+ const classes = ['normal','DoS','Probe','R2L','U2R'];
348
+ const mx = Math.max(...classes.map(c => csvCounts[c]||0), 1);
349
+ classes.forEach(c => { setWidth('csvbar-'+c, (csvCounts[c]||0)/mx*100); setText('csvbc-'+c, (csvCounts[c]||0).toLocaleString()); });
350
+ setText('csvAvgConf', csvIndex > 0 ? (csvConfSum/csvIndex*100).toFixed(1)+'%' : 'β€”');
351
+ const sevs = ['Critical','High','Medium','None'];
352
+ const smx = Math.max(...sevs.map(s => csvSevCounts[s]||0), 1);
353
+ sevs.forEach(s => { setWidth('sevbar-'+s, (csvSevCounts[s]||0)/smx*100); setText('sevbc-'+s, (csvSevCounts[s]||0).toLocaleString()); });
354
+ setText('csvProcRate', Math.round(rate).toLocaleString());
355
+ }
356
+
357
+ function stopCsvAnalysis() {
358
+ csvRunning = false;
359
+ document.getElementById('csvStartBtn').disabled = false;
360
+ document.getElementById('csvStopBtn').disabled = true;
361
+ document.getElementById('liveDot').className = 'dot red';
362
+ setText('liveStatus', 'PAUSED');
363
+ }
364
+
365
+ function finishCsvAnalysis() {
366
+ csvRunning = false;
367
+ document.getElementById('csvStartBtn').disabled = true;
368
+ document.getElementById('csvStopBtn').disabled = true;
369
+ document.getElementById('liveDot').className = 'dot green';
370
+ setText('liveStatus', 'DONE');
371
+ setText('csvProgressEta', 'Done!');
372
+ setText('csvCurrentRow', `βœ“ All ${csvRows.length.toLocaleString()} rows processed. Building report…`);
373
+ setTimeout(() => { exportAnnotatedCSV(); buildReport(); }, 300);
374
+ }
375
+
376
+ // ════════════════════════════════════════════════
377
+ // REPORT BUILDER
378
+ // ════════════════════════════════════════════════
379
+ function buildReport() {
380
+ document.getElementById('reportSection').classList.add('visible');
381
+ const elapsed = (Date.now() - csvStartTime) / 1000;
382
+ const fileName = document.getElementById('csvFileName').textContent;
383
+ const total = csvResults.length, threats = csvIntrusionCount;
384
+ const avgConf = (csvConfSum/total*100).toFixed(1) + '%';
385
+ const rate = (threats/total*100).toFixed(1) + '%';
386
+ const riskScore = Math.min(100, Math.round(
387
+ ((csvCounts.DoS||0)*0.4 + (csvCounts.U2R||0)*0.35 + (csvCounts.R2L||0)*0.15 + (csvCounts.Probe||0)*0.1)
388
+ / Math.max(total,1) * 100 * 6
389
+ ));
390
+
391
+ setText('bannerSub', `${total.toLocaleString()} rows Β· ${formatETA(elapsed)} Β· ${threats.toLocaleString()} threats`);
392
+ setText('rmFile', fileName);
393
+ setText('rmRows', total.toLocaleString());
394
+ setText('rmDate', new Date().toLocaleString());
395
+ setText('rmModel', csvUsingReal ? 'Real Random Forest' : 'Local Simulation');
396
+ setText('rmDuration', formatETA(elapsed));
397
+ setText('rmFormat', csvFormatInfo);
398
+ setText('reportSubtitle', `Generated ${new Date().toUTCString()}`);
399
+ setText('rs-total', total.toLocaleString());
400
+ setText('rs-threats', threats.toLocaleString());
401
+ setText('rs-rate', rate);
402
+ setText('rs-conf', avgConf);
403
+ setText('rs-risk', riskScore + '/100');
404
+
405
+ const sevs = ['Critical','High','Medium','None'];
406
+ const smx = Math.max(...sevs.map(s => csvSevCounts[s]||0), 1);
407
+ sevs.forEach(s => { setWidth('rsevbar-'+s, (csvSevCounts[s]||0)/smx*100); setText('rsevbc-'+s, (csvSevCounts[s]||0).toLocaleString()); });
408
+
409
+ requestAnimationFrame(() => {
410
+ drawBarChart(); drawConfWave(); drawIntensity();
411
+ drawProto(); drawServices(); drawGauge(riskScore);
412
+ buildClusters(); reportPage = 0; renderReportPage();
413
+ });
414
+ setTimeout(() => document.getElementById('reportSection').scrollIntoView({ behavior:'smooth', block:'start' }), 300);
415
+ }
416
+
417
+ // ── Paginated table ──
418
+ function renderReportPage() {
419
+ const tbody = document.getElementById('reportTableBody');
420
+ const total = csvResults.length, totalPages = Math.ceil(total / PAGE_SIZE);
421
+ const start = reportPage * PAGE_SIZE, end = Math.min(start + PAGE_SIZE, total);
422
+ const frag = document.createDocumentFragment();
423
+
424
+ for (let i = start; i < end; i++) {
425
+ const { rowNum, row, cls, conf, sev, isI } = csvResults[i];
426
+ const tr = document.createElement('tr');
427
+ if (isI) tr.className = 'row-intrusion';
428
+ tr.innerHTML = `
429
+ <td style="color:var(--muted)">${rowNum}</td>
430
+ <td style="color:var(--cyan)">${row.protocol_type||'β€”'}</td>
431
+ <td>${row.service||'β€”'}</td>
432
+ <td>${(row.src_bytes||0).toLocaleString()}</td>
433
+ <td>${(row.dst_bytes||0).toLocaleString()}</td>
434
+ <td><span class="cls-badge cls-${cls}">${cls}</span></td>
435
+ <td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td>
436
+ <td style="color:${SEV_COLOR[sev]}">● ${sev}</td>
437
+ <td style="color:var(--muted);font-size:10px">${row.label||'β€”'}</td>`;
438
+ frag.appendChild(tr);
439
+ }
440
+ tbody.innerHTML = '';
441
+ tbody.appendChild(frag);
442
+ setText('reportRowCount', `${total.toLocaleString()} rows`);
443
+ setText('pgInfo', `Page ${reportPage+1} of ${totalPages} Β· rows ${start+1}–${end}`);
444
+ document.getElementById('pgPrev').disabled = reportPage === 0;
445
+ document.getElementById('pgNext').disabled = reportPage >= totalPages - 1;
446
+ }
447
+
448
+ function changePage(dir) {
449
+ reportPage += dir; renderReportPage();
450
+ document.getElementById('reportSection').scrollIntoView({ behavior:'smooth', block:'start' });
451
+ }
452
+
453
+ // ── Charts ──
454
+ function drawBarChart() {
455
+ const c = document.getElementById('reportBarCanvas'), ctx = c.getContext('2d');
456
+ const W = c.offsetWidth||300, H = 160;
457
+ c.width = W*devicePixelRatio; c.height = H*devicePixelRatio;
458
+ ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H);
459
+ const classes = ['normal','DoS','Probe','R2L','U2R'];
460
+ const colors = ['#00e87a','#ff3d5a','#00c8e8','#ffaa00','#b06fff'];
461
+ const vals = classes.map(c => csvCounts[c]||0), mx = Math.max(...vals, 1);
462
+ const bw = (W-40)/classes.length, pad = bw*0.18;
463
+ classes.forEach((cls, i) => {
464
+ const x = 20+i*bw+pad, bW = bw-pad*2, bH = (vals[i]/mx)*(H-30), y = H-10-bH;
465
+ const g = ctx.createLinearGradient(0,y,0,H-10);
466
+ g.addColorStop(0, colors[i]); g.addColorStop(1, colors[i]+'33');
467
+ ctx.fillStyle = g; ctx.beginPath(); ctx.roundRect(x,y,bW,Math.max(bH,1),4); ctx.fill();
468
+ ctx.fillStyle = 'rgba(90,122,153,.9)'; ctx.font = '9px IBM Plex Mono'; ctx.textAlign = 'center';
469
+ ctx.fillText(cls, x+bW/2, H-1);
470
+ if (vals[i] > 0) { ctx.fillStyle = colors[i]; ctx.fillText(vals[i].toLocaleString(), x+bW/2, y-4); }
471
+ });
472
+ }
473
+
474
+ function drawConfWave() {
475
+ const c = document.getElementById('reportConfCanvas'), ctx = c.getContext('2d');
476
+ const W = c.offsetWidth||300, H = 160;
477
+ c.width = W*devicePixelRatio; c.height = H*devicePixelRatio;
478
+ ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H);
479
+ const data = csvConfHistory; if (data.length < 2) return;
480
+ const xStep = (W-20)/Math.max(data.length-1,1), mn = 0.5, mx = 1;
481
+ ctx.strokeStyle = 'rgba(0,200,120,.06)'; ctx.lineWidth = 1;
482
+ [.25,.5,.75,1].forEach(f => { const y=10+(1-f)*(H-20); ctx.beginPath(); ctx.moveTo(10,y); ctx.lineTo(W-10,y); ctx.stroke(); });
483
+ ctx.beginPath();
484
+ data.forEach((v,i) => { const x=10+i*xStep, y=10+(1-(v-mn)/(mx-mn))*(H-20); i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
485
+ ctx.lineTo(10+(data.length-1)*xStep, H-10); ctx.lineTo(10, H-10); ctx.closePath();
486
+ const g = ctx.createLinearGradient(0,0,0,H); g.addColorStop(0,'rgba(0,200,232,.15)'); g.addColorStop(1,'rgba(0,200,232,.01)');
487
+ ctx.fillStyle = g; ctx.fill();
488
+ ctx.beginPath(); ctx.strokeStyle = '#00c8e8'; ctx.lineWidth = 1.5;
489
+ data.forEach((v,i) => { const x=10+i*xStep, y=10+(1-(v-mn)/(mx-mn))*(H-20); i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
490
+ ctx.stroke();
491
+ const avg = csvConfSum/csvResults.length, avgY = 10+(1-(avg-mn)/(mx-mn))*(H-20);
492
+ ctx.beginPath(); ctx.setLineDash([4,3]); ctx.strokeStyle='rgba(0,232,122,.6)'; ctx.lineWidth=1;
493
+ ctx.moveTo(10,avgY); ctx.lineTo(W-10,avgY); ctx.stroke(); ctx.setLineDash([]);
494
+ ctx.fillStyle='rgba(0,232,122,.8)'; ctx.font='9px IBM Plex Mono'; ctx.textAlign='left';
495
+ ctx.fillText(`avg ${(avg*100).toFixed(1)}%`, 14, avgY-5);
496
+ }
497
+
498
+ function drawIntensity() {
499
+ const c = document.getElementById('reportIntensityCanvas'), ctx = c.getContext('2d');
500
+ const W = c.offsetWidth||300, H = 160;
501
+ c.width = W*devicePixelRatio; c.height = H*devicePixelRatio;
502
+ ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H);
503
+ const wSize = Math.max(5, Math.floor(csvResults.length/60));
504
+ const windows = [];
505
+ for (let i = 0; i < csvResults.length; i += wSize) {
506
+ const sl = csvResults.slice(i, i+wSize);
507
+ windows.push(sl.filter(r => r.isI).length / sl.length);
508
+ }
509
+ if (windows.length < 2) { ctx.fillStyle='#4a6a88'; ctx.font='11px IBM Plex Mono'; ctx.textAlign='center'; ctx.fillText('Not enough data',W/2,H/2); return; }
510
+ const mx = Math.max(...windows, .01), xStep = (W-20)/Math.max(windows.length-1,1);
511
+ ctx.strokeStyle='rgba(255,61,90,.05)'; ctx.lineWidth=1;
512
+ [.25,.5,.75,1].forEach(f => { const y=10+(1-f)*(H-20); ctx.beginPath(); ctx.moveTo(10,y); ctx.lineTo(W-10,y); ctx.stroke(); });
513
+ ctx.beginPath();
514
+ windows.forEach((v,i) => { const x=10+i*xStep, y=10+(1-v/mx)*(H-20); i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
515
+ ctx.lineTo(10+(windows.length-1)*xStep, H-10); ctx.lineTo(10,H-10); ctx.closePath();
516
+ const g = ctx.createLinearGradient(0,0,0,H); g.addColorStop(0,'rgba(255,61,90,.28)'); g.addColorStop(1,'rgba(255,61,90,.02)');
517
+ ctx.fillStyle=g; ctx.fill();
518
+ ctx.beginPath(); ctx.strokeStyle='#ff3d5a'; ctx.lineWidth=2;
519
+ windows.forEach((v,i) => { const x=10+i*xStep, y=10+(1-v/mx)*(H-20); i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
520
+ ctx.stroke();
521
+ }
522
+
523
+ function drawProto() {
524
+ const el = document.getElementById('protoBreakdown'); el.innerHTML = '';
525
+ const pc = {}, pt = {};
526
+ csvResults.forEach(({ row, isI }) => { const p=row.protocol_type||'unknown'; pc[p]=(pc[p]||0)+1; if(isI)pt[p]=(pt[p]||0)+1; });
527
+ const sorted = Object.entries(pc).sort((a,b) => b[1]-a[1]);
528
+ const mx = Math.max(...sorted.map(([,v]) => v), 1);
529
+ const frag = document.createDocumentFragment();
530
+ sorted.forEach(([proto, cnt]) => {
531
+ const tc=pt[proto]||0, tp=cnt>0?Math.round(tc/cnt*100):0;
532
+ const div = document.createElement('div'); div.className = 'proto-row';
533
+ div.innerHTML = `<div class="proto-lbl">${proto}</div><div class="proto-track"><div class="proto-fill" style="width:${cnt/mx*100}%;background:${tc>0?'var(--red)':'var(--accent)'}"></div></div><div class="proto-cnt">${cnt.toLocaleString()} <span style="color:${tc>0?'var(--red)':'var(--muted)'}">${tp}% threat</span></div>`;
534
+ frag.appendChild(div);
535
+ });
536
+ el.appendChild(frag);
537
+ }
538
+
539
+ function drawServices() {
540
+ const el = document.getElementById('servicesList'); el.innerHTML = '';
541
+ const sc = {}, st = {};
542
+ csvResults.forEach(({ row, isI }) => { const s=row.service||'unknown'; sc[s]=(sc[s]||0)+1; if(isI)st[s]=(st[s]||0)+1; });
543
+ const sorted = Object.entries(sc).sort((a,b) => b[1]-a[1]).slice(0, 8);
544
+ const mx = Math.max(...sorted.map(([,v]) => v), 1);
545
+ const frag = document.createDocumentFragment();
546
+ sorted.forEach(([svc, cnt]) => {
547
+ const hot = (st[svc]||0) > cnt*0.3;
548
+ const div = document.createElement('div'); div.className = 'svc-row';
549
+ div.innerHTML = `<span class="svc-name" style="color:${hot?'var(--red)':'var(--cyan)'}">${svc}</span><span style="flex:1;margin:0 8px;background:var(--surface);border-radius:2px;height:4px;display:block;overflow:hidden"><span style="display:block;height:100%;width:${Math.round(cnt/mx*100)}%;background:${hot?'var(--red)':'var(--cyan)'};border-radius:2px"></span></span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${cnt.toLocaleString()}</span>`;
550
+ frag.appendChild(div);
551
+ });
552
+ el.appendChild(frag);
553
+ }
554
+
555
+ function drawGauge(score) {
556
+ const c = document.getElementById('riskGaugeCanvas'), ctx = c.getContext('2d');
557
+ const W = 160, H = 90;
558
+ c.width = W*devicePixelRatio; c.height = H*devicePixelRatio;
559
+ ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H);
560
+ const cx=W/2, cy=H-8, r=66;
561
+ ctx.beginPath(); ctx.arc(cx,cy,r,Math.PI,2*Math.PI); ctx.strokeStyle='rgba(255,255,255,.05)'; ctx.lineWidth=14; ctx.lineCap='round'; ctx.stroke();
562
+ [[0,.33,'#00e87a'],[.33,.66,'#ffaa00'],[.66,1,'#ff3d5a']].forEach(([from,to,col]) => {
563
+ ctx.beginPath(); ctx.arc(cx,cy,r,Math.PI+from*Math.PI,Math.PI+to*Math.PI); ctx.strokeStyle=col+'55'; ctx.lineWidth=14; ctx.stroke();
564
+ });
565
+ const sc = score<33?'#00e87a':score<66?'#ffaa00':'#ff3d5a';
566
+ ctx.beginPath(); ctx.arc(cx,cy,r,Math.PI,Math.PI+(score/100)*Math.PI); ctx.strokeStyle=sc; ctx.lineWidth=14; ctx.lineCap='round'; ctx.stroke();
567
+ ctx.fillStyle=sc; ctx.font='bold 22px IBM Plex Mono'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(score,cx,cy-16);
568
+ ctx.fillStyle='rgba(90,122,153,.8)'; ctx.font='9px IBM Plex Mono'; ctx.fillText('/100',cx,cy-2);
569
+ const lbl = score<20?'LOW':score<40?'MODERATE':score<60?'ELEVATED':score<80?'HIGH':'CRITICAL';
570
+ setText('riskLabel', `${lbl} RISK Β· Score ${score}/100`);
571
+ }
572
+
573
+ function buildClusters() {
574
+ const grid = document.getElementById('clusterGrid'); grid.innerHTML = '';
575
+ let cnt = 0; const frag = document.createDocumentFragment();
576
+ ['DoS','Probe','R2L','U2R','normal'].forEach(cls => {
577
+ const c = csvCounts[cls]||0; if (!c) return; cnt++;
578
+ const col = CLASS_COLOR[cls], res = csvResults.filter(r => r.cls === cls);
579
+ const avgConf = res.reduce((s,r) => s+r.conf, 0) / c;
580
+ const div = document.createElement('div'); div.className = `cluster-card ${cls}`;
581
+ div.innerHTML = `<div class="cluster-title" style="color:${col}">${cls} Traffic</div><div class="cluster-count" style="color:${col}">${c.toLocaleString()}</div><div class="cluster-sub">Severity: ${SEV_MAP[cls]}<br>Avg confidence: ${(avgConf*100).toFixed(1)}%<br>${(c/csvResults.length*100).toFixed(1)}% of dataset</div>`;
582
+ frag.appendChild(div);
583
+ });
584
+ grid.appendChild(frag);
585
+ if (!cnt) grid.innerHTML = '<div style="padding:20px;font-family:var(--mono);font-size:11px;color:var(--accent);grid-column:span 4">βœ“ No attack clusters β€” clean dataset</div>';
586
+ setText('clusterCount', cnt + ' clusters');
587
+ }
588
+
589
+ // ════════════════════════════════════════════════
590
+ // EXPORTS
591
+ // ════════════════════════════════════════════════
592
+ function exportAnnotatedCSV() {
593
+ if (!csvResults.length) return;
594
+ const headers = Object.keys(csvResults[0].row).concat(['predicted_class','severity','confidence','is_intrusion']);
595
+ const lines = [headers.join(',')];
596
+ csvResults.forEach(({ row, cls, sev, conf, isI }) => {
597
+ const vals = Object.values(row).map(v => typeof v==='string' && v.includes(',') ? `"${v}"` : v);
598
+ vals.push(cls, sev, conf, isI?1:0); lines.push(vals.join(','));
599
+ });
600
+ downloadFile('sentinelnet_annotated.csv', lines.join('\n'), 'text/csv');
601
+ }
602
+
603
+ function exportJSON() {
604
+ if (!csvResults.length) { alert('No results.'); return; }
605
+ const data = {
606
+ meta: { file:document.getElementById('csvFileName').textContent, date:new Date().toISOString(), total:csvResults.length, threats:csvIntrusionCount, threatRate:(csvIntrusionCount/csvResults.length*100).toFixed(1)+'%', model:csvUsingReal?'Real RF':'Local Sim', batchSize:BATCH_SIZE },
607
+ distribution:csvCounts, severity:csvSevCounts,
608
+ avgConf:(csvConfSum/csvResults.length*100).toFixed(2)+'%',
609
+ results:csvResults.map(({ rowNum, cls, conf, sev, isI }) => ({ rowNum, cls, conf, sev, isI }))
610
+ };
611
+ downloadFile('sentinelnet_results.json', JSON.stringify(data,null,2), 'application/json');
612
+ }
613
+
614
+ function exportPDFReport() {
615
+ if (!csvResults.length) { alert('No results.'); return; }
616
+ const total=csvResults.length, threats=csvIntrusionCount;
617
+ const rate=(threats/total*100).toFixed(1), avgConf=(csvConfSum/total*100).toFixed(1);
618
+ const fileName=document.getElementById('csvFileName').textContent;
619
+ const dateStr=new Date().toLocaleString(), elapsed=(Date.now()-csvStartTime)/1000;
620
+ const modelSrc=csvUsingReal?'Real Random Forest':'Local Simulation';
621
+ const riskScore=Math.min(100,Math.round(((csvCounts.DoS||0)*0.4+(csvCounts.U2R||0)*0.35+(csvCounts.R2L||0)*0.15+(csvCounts.Probe||0)*0.1)/Math.max(total,1)*100*6));
622
+ const riskLbl=riskScore<20?'LOW':riskScore<40?'MODERATE':riskScore<60?'ELEVATED':riskScore<80?'HIGH':'CRITICAL';
623
+ const colors={normal:'#00e87a',DoS:'#ff3d5a',Probe:'#00c8e8',R2L:'#ffaa00',U2R:'#b06fff'};
624
+ const sevColors={None:'#00e87a',Medium:'#00c8e8',High:'#ffaa00',Critical:'#ff3d5a'};
625
+ const distRows=['normal','DoS','Probe','R2L','U2R'].map(c=>`<tr><td style="color:${colors[c]}">${c}</td><td>${(csvCounts[c]||0).toLocaleString()}</td><td>${total>0?((csvCounts[c]||0)/total*100).toFixed(1):'0'}%</td><td style="color:${sevColors[SEV_MAP[c]]}">${SEV_MAP[c]}</td></tr>`).join('');
626
+ const pc={}; csvResults.forEach(({row})=>{const p=row.protocol_type||'unknown';pc[p]=(pc[p]||0)+1;});
627
+ const protoRows=Object.entries(pc).sort((a,b)=>b[1]-a[1]).map(([p,c])=>`<tr><td>${p}</td><td>${c.toLocaleString()}</td><td>${(c/total*100).toFixed(1)}%</td></tr>`).join('');
628
+ const topThreats=csvResults.filter(r=>r.isI).slice(0,100);
629
+ const threatRows=topThreats.map(({rowNum,row,cls,conf,sev})=>`<tr><td>${rowNum}</td><td>${row.protocol_type||'β€”'}</td><td>${row.service||'β€”'}</td><td>${(row.src_bytes||0).toLocaleString()}</td><td style="color:${colors[cls]};font-weight:bold">${cls}</td><td>${(conf*100).toFixed(1)}%</td><td style="color:${sevColors[sev]}">${sev}</td></tr>`).join('');
630
+ const html=`<!DOCTYPE html><html><head><meta charset="UTF-8"/><title>SentinelNet Report</title>
631
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&display=swap" rel="stylesheet">
632
+ <style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:'IBM Plex Mono',monospace;background:#04080d;color:#d8eeff;padding:40px;font-size:11px}h1{color:#00e87a;font-size:22px;border-bottom:2px solid #00e87a;padding-bottom:12px;margin-bottom:8px}h2{color:#00c8e8;font-size:13px;margin:28px 0 12px;letter-spacing:2px;text-transform:uppercase}.meta{color:#4a6a88;font-size:10px;margin-bottom:32px;line-height:2.2}.grid{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:32px}.stat{background:#080e16;border:1px solid rgba(0,210,130,0.15);border-radius:10px;padding:16px;text-align:center}.stat-val{font-size:24px;font-weight:700;margin-bottom:5px}.stat-lbl{font-size:8px;color:#4a6a88;letter-spacing:2px;text-transform:uppercase}table{width:100%;border-collapse:collapse;margin-bottom:28px}th{background:#0d1520;padding:9px 12px;text-align:left;color:#4a6a88;font-size:8px;letter-spacing:1.5px;border-bottom:1px solid rgba(0,210,130,0.2)}td{padding:7px 12px;border-top:1px solid rgba(0,210,130,0.06)}.risk-box{background:#0d1520;border:1px solid rgba(0,210,130,0.2);border-radius:12px;padding:20px;text-align:center;margin-bottom:28px}.risk-num{font-size:40px;font-weight:700;color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'}}.footer{margin-top:40px;padding-top:14px;border-top:1px solid rgba(0,210,130,0.12);color:#4a6a88;font-size:9px;text-align:center}@media print{body{background:#04080d!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}}</style></head><body>
633
+ <h1>πŸ›‘ SentinelNet β€” Threat Analysis Report</h1>
634
+ <div class="meta">File: <strong style="color:#d8eeff">${fileName}</strong> | Date: ${dateStr} | Model: ${modelSrc} | Duration: ${formatETA(elapsed)}</div>
635
+ <div class="grid"><div class="stat"><div class="stat-val" style="color:#00c8e8">${total.toLocaleString()}</div><div class="stat-lbl">Total Rows</div></div><div class="stat"><div class="stat-val" style="color:#ff3d5a">${threats.toLocaleString()}</div><div class="stat-lbl">Threats Found</div></div><div class="stat"><div class="stat-val" style="color:#ffaa00">${rate}%</div><div class="stat-lbl">Threat Rate</div></div><div class="stat"><div class="stat-val" style="color:#00e87a">${avgConf}%</div><div class="stat-lbl">Avg Confidence</div></div><div class="stat"><div class="stat-val" style="color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'}">${riskScore}/100</div><div class="stat-lbl">Risk Score</div></div></div>
636
+ <div class="risk-box"><div class="risk-num">${riskScore}</div><div style="font-size:13px;color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'};margin-top:4px">${riskLbl} RISK</div><div style="color:#4a6a88;font-size:10px;margin-top:8px">Weighted: DoSΓ—0.4 Β· U2RΓ—0.35 Β· R2LΓ—0.15 Β· ProbeΓ—0.1</div></div>
637
+ <h2>Attack Class Distribution</h2><table><thead><tr><th>CLASS</th><th>COUNT</th><th>PERCENTAGE</th><th>SEVERITY</th></tr></thead><tbody>${distRows}</tbody></table>
638
+ <h2>Protocol Breakdown</h2><table><thead><tr><th>PROTOCOL</th><th>COUNT</th><th>PERCENTAGE</th></tr></thead><tbody>${protoRows}</tbody></table>
639
+ <h2>Severity Summary</h2><table><thead><tr><th>SEVERITY</th><th>COUNT</th><th>PERCENTAGE</th></tr></thead><tbody>${['Critical','High','Medium','None'].map(s=>`<tr><td style="color:${sevColors[s]}">${s}</td><td>${(csvSevCounts[s]||0).toLocaleString()}</td><td>${((csvSevCounts[s]||0)/total*100).toFixed(1)}%</td></tr>`).join('')}</tbody></table>
640
+ <h2>Detected Threats β€” Top 100</h2><table><thead><tr><th>ROW</th><th>PROTOCOL</th><th>SERVICE</th><th>SRC BYTES</th><th>CLASS</th><th>CONFIDENCE</th><th>SEVERITY</th></tr></thead><tbody>${threatRows||'<tr><td colspan="7" style="color:#00e87a;text-align:center;padding:20px">No threats detected</td></tr>'}</tbody></table>
641
+ <div class="footer">Generated by SentinelNet Β· ${dateStr} Β· NSL-KDD Intrusion Detection</div>
642
+ </body></html>`;
643
+ const win = window.open('','_blank','width=1100,height=900');
644
+ if (!win) { alert('Pop-up blocked.'); return; }
645
+ win.document.write(html); win.document.close(); setTimeout(()=>win.print(), 900);
646
+ }
647
+
648
+ function downloadFile(filename, content, type) {
649
+ const blob = new Blob([content], {type});
650
+ const url = URL.createObjectURL(blob);
651
+ const a = document.createElement('a');
652
+ a.href = url; a.download = filename; a.click();
653
+ URL.revokeObjectURL(url);
654
+ }
655
+
656
+ function resetCsvTab() {
657
+ csvRunning = false; csvRows=[]; csvResults=[]; csvIndex=0; csvConfSum=0;
658
+ csvIntrusionCount=0; csvConfHistory=[]; batchNum=0; reportPage=0;
659
+ Object.keys(csvCounts).forEach(k=>csvCounts[k]=0);
660
+ Object.keys(csvSevCounts).forEach(k=>csvSevCounts[k]=0);
661
+ csvStartTime=null; csvUsingReal=false; csvFormatInfo=''; totalBatches=0;
662
+ document.getElementById('csvUploadSection').style.display='';
663
+ document.getElementById('csvProcessingArea').classList.remove('visible');
664
+ document.getElementById('csvProgressBlock').style.display='none';
665
+ document.getElementById('csvLiveGrid').style.display='none';
666
+ document.getElementById('reportSection').classList.remove('visible');
667
+ document.getElementById('csvFeedBody').innerHTML='';
668
+ document.getElementById('reportTableBody').innerHTML='';
669
+ document.getElementById('csvFileInput').value='';
670
+ document.getElementById('formatBanner').style.display='none';
671
+ document.getElementById('liveDot').className='dot'; setText('liveStatus','IDLE');
672
+ document.getElementById('csvStartBtn').disabled=false;
673
+ document.getElementById('csvStopBtn').disabled=true;
674
+ document.getElementById('batchStatus').innerHTML='';
675
+ document.getElementById('csvProgressFill').style.width='0%';
676
+ document.getElementById('csvProgressFill').classList.remove('warning');
677
+ }
678
+
679
+ // ════════════════════════════════════════════════
680
+ // LIVE MONITOR
681
+ // ════════════════════════════════════════════════
682
+ function generatePacket() {
683
+ const label=LABEL_POOL[Math.floor(Math.random()*LABEL_POOL.length)];
684
+ const isAtk=label!=='normal';
685
+ const protocol=PROTOCOLS[Math.floor(Math.random()*3)];
686
+ const service=SERVICES[Math.floor(Math.random()*SERVICES.length)];
687
+ const flag=isAtk&&Math.random()>0.5?['S0','REJ','RSTO'][Math.floor(Math.random()*3)]:'SF';
688
+ const srcBytes=isAtk?Math.floor(Math.random()*200000):Math.floor(Math.random()*4000);
689
+ return {
690
+ duration:Math.floor(Math.random()*3600),protocol_type:protocol,service,flag,
691
+ src_bytes:srcBytes,dst_bytes:Math.floor(Math.random()*8000),land:0,
692
+ wrong_fragment:Math.floor(Math.random()*2),urgent:0,hot:Math.floor(Math.random()*8),
693
+ num_failed_logins:isAtk?Math.floor(Math.random()*3):0,logged_in:Math.random()>0.4?1:0,
694
+ num_compromised:isAtk?Math.floor(Math.random()*5):0,root_shell:0,su_attempted:0,
695
+ num_root:0,num_file_creations:0,num_shells:0,num_access_files:0,num_outbound_cmds:0,
696
+ is_host_login:0,is_guest_login:Math.random()>0.95?1:0,
697
+ count:Math.floor(Math.random()*511),srv_count:Math.floor(Math.random()*511),
698
+ serror_rate:+(Math.random()).toFixed(2),srv_serror_rate:+(Math.random()).toFixed(2),
699
+ rerror_rate:+(Math.random()).toFixed(2),srv_rerror_rate:+(Math.random()).toFixed(2),
700
+ same_srv_rate:+(Math.random()).toFixed(2),diff_srv_rate:+(Math.random()).toFixed(2),
701
+ srv_diff_host_rate:+(Math.random()).toFixed(2),
702
+ dst_host_count:Math.floor(Math.random()*255),dst_host_srv_count:Math.floor(Math.random()*255),
703
+ dst_host_same_srv_rate:+(Math.random()).toFixed(2),dst_host_diff_srv_rate:+(Math.random()).toFixed(2),
704
+ dst_host_same_src_port_rate:+(Math.random()).toFixed(2),dst_host_srv_diff_host_rate:+(Math.random()).toFixed(2),
705
+ dst_host_serror_rate:+(Math.random()).toFixed(2),dst_host_srv_serror_rate:+(Math.random()).toFixed(2),
706
+ dst_host_rerror_rate:+(Math.random()).toFixed(2),dst_host_srv_rerror_rate:+(Math.random()).toFixed(2),
707
+ label,difficulty_level:Math.floor(Math.random()*21)
708
+ };
709
+ }
710
+
711
+ async function tick() {
712
+ const packet = generatePacket();
713
+ let result;
714
+ const api = await predictSingle(packet);
715
+ if (api) {
716
+ result = api;
717
+ if (!usingRealModel) { usingRealModel=true; document.getElementById('connBadge').textContent='βœ“ REAL MODEL'; document.getElementById('connBadge').className='real'; document.getElementById('sum-model').textContent='REAL RF'; termLog('info','Connected to model at '+BACKEND_URL); }
718
+ } else {
719
+ result = classifyLocal(packet);
720
+ if (usingRealModel) { usingRealModel=false; document.getElementById('connBadge').textContent='⚠ LOCAL SIM'; document.getElementById('connBadge').className='local'; document.getElementById('sum-model').textContent='LOCAL'; }
721
+ }
722
+ packetId++; totalPackets++;
723
+ const cls=result.predicted_class, conf=result.confidence, sev=result.severity, isI=result.is_intrusion;
724
+ counts[cls]=(counts[cls]||0)+1;
725
+ if (isI) { totalIntrusions++; if (!peakClass||counts[cls]>(counts[peakClass]||0)) peakClass=cls; }
726
+ confSum+=conf;
727
+ const cp=conf*100;
728
+ if (cp>=90) confBuckets[90]++; else if (cp>=80) confBuckets[80]++; else if (cp>=70) confBuckets[70]++; else confBuckets.low++;
729
+ timelineBuckets[sessionSeconds%60]+=(isI?1:0);
730
+ heatmapCells.shift(); heatmapCells.push(cls);
731
+ addFeedRow(packet, cls, sev, conf);
732
+ if (cls==='U2R') termLog('crit',`U2R ALERT β€” Privilege escalation! Conf: ${(conf*100).toFixed(1)}%`);
733
+ else if (cls==='DoS'&&Math.random()<0.15) termLog('warn',`DoS β€” ${packet.service} flood`);
734
+ else if (cls==='normal'&&totalPackets%50===0) termLog('ok',`${totalPackets} packets. Rate: ${(totalIntrusions/totalPackets*100).toFixed(1)}%`);
735
+ updateMetrics(); updateBars(); updateConfBars();
736
+ tlDirty=true;
737
+ updateHeatmap(); updateSummary(); flashMetric(cls);
738
+ }
739
+
740
+ // Throttled timeline redraw
741
+ setInterval(() => { if (tlDirty) { updateTimeline(); tlDirty=false; } }, 500);
742
+
743
+ function addFeedRow(packet, cls, sev, conf) {
744
+ const tbody=document.getElementById('feedBody');
745
+ const now=new Date(), ts=`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
746
+ const tr=document.createElement('tr'); tr.className='new-row';
747
+ tr.innerHTML=`<td style="color:var(--muted)">${packetId}</td><td style="color:var(--muted)">${ts}</td><td style="color:var(--cyan)">${packet.protocol_type}</td><td>${packet.src_bytes.toLocaleString()}</td><td><span class="cls-badge cls-${cls}">${cls}</span></td><td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td><td style="color:${SEV_COLOR[sev]}">● ${sev}</td>`;
748
+ tbody.insertBefore(tr,tbody.firstChild);
749
+ while (tbody.children.length>100) tbody.removeChild(tbody.lastChild);
750
+ document.getElementById('emptyState').style.display='none';
751
+ document.getElementById('feedTable').style.display='';
752
+ document.getElementById('alertCount').textContent=totalIntrusions+' ALERTS';
753
+ }
754
+
755
+ function updateMetrics() {
756
+ const pct=n=>totalPackets>0?(n/totalPackets*100).toFixed(1)+'%':'β€”';
757
+ setText('m-total',totalPackets.toLocaleString()); setText('m-normal',counts.normal||0); setText('m-normal-pct',pct(counts.normal||0));
758
+ setText('m-intrusions',totalIntrusions); setText('m-intrusions-pct',pct(totalIntrusions));
759
+ setText('m-dos',counts.DoS||0); setText('m-probe',counts.Probe||0); setText('m-u2r',counts.U2R||0);
760
+ setText('m-rate',(totalPackets/Math.max(sessionSeconds,1)).toFixed(1)+' /sec');
761
+ setWidth('mb-total',Math.min(totalPackets/500*100,100)); setWidth('mb-normal',pctW(counts.normal));
762
+ setWidth('mb-intrusions',pctW(totalIntrusions)); setWidth('mb-dos',pctW(counts.DoS)); setWidth('mb-probe',pctW(counts.Probe)); setWidth('mb-u2r',pctW(counts.U2R));
763
+ }
764
+ function pctW(n) { return totalPackets>0?(n/totalPackets*100):0; }
765
+ function updateBars() {
766
+ const cls=['normal','DoS','Probe','R2L','U2R'], mx=Math.max(...cls.map(c=>counts[c]||0),1);
767
+ cls.forEach(c=>{setWidth('bar-'+c,(counts[c]||0)/mx*100); setText('bc-'+c,counts[c]||0);});
768
+ setText('distTotal',totalPackets+' total');
769
+ }
770
+ function updateConfBars() {
771
+ const mx=Math.max(...Object.values(confBuckets),1);
772
+ setWidth('conf-90',confBuckets[90]/mx*100); setWidth('conf-80',confBuckets[80]/mx*100);
773
+ setWidth('conf-70',confBuckets[70]/mx*100); setWidth('conf-low',confBuckets.low/mx*100);
774
+ setText('cbc-90',confBuckets[90]); setText('cbc-80',confBuckets[80]); setText('cbc-70',confBuckets[70]); setText('cbc-low',confBuckets.low);
775
+ setText('avgConf','avg: '+(totalPackets>0?(confSum/totalPackets*100).toFixed(1)+'%':'β€”'));
776
+ }
777
+ function updateTimeline() {
778
+ const canvas=document.getElementById('tlCanvas'); if (!canvas) return;
779
+ const ctx=canvas.getContext('2d'), W=canvas.offsetWidth, H=80;
780
+ if (!W) return;
781
+ canvas.width=W*devicePixelRatio; canvas.height=H*devicePixelRatio;
782
+ ctx.scale(devicePixelRatio,devicePixelRatio); ctx.clearRect(0,0,W,H);
783
+ const data=timelineBuckets, mx=Math.max(...data,1), step=W/data.length;
784
+ ctx.strokeStyle='rgba(0,200,120,.06)'; ctx.lineWidth=1;
785
+ [.25,.5,.75,1].forEach(f=>{ctx.beginPath();ctx.moveTo(0,H*f);ctx.lineTo(W,H*f);ctx.stroke();});
786
+ ctx.beginPath(); ctx.moveTo(0,H);
787
+ data.forEach((v,i)=>{const x=i*step,y=H-(v/mx*(H-10));ctx.lineTo(x,y);});
788
+ ctx.lineTo(W,H); ctx.closePath(); ctx.fillStyle='rgba(255,61,90,.12)'; ctx.fill();
789
+ ctx.beginPath(); ctx.strokeStyle='#ff3d5a'; ctx.lineWidth=1.5;
790
+ data.forEach((v,i)=>{const x=i*step,y=H-(v/mx*(H-10));i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
791
+ ctx.stroke();
792
+ }
793
+ const HM = { normal:'rgba(0,232,122,0.4)',DoS:'rgba(255,61,90,0.65)',Probe:'rgba(0,200,232,0.5)',R2L:'rgba(255,170,0,0.55)',U2R:'rgba(176,111,255,0.75)',null:'rgba(255,255,255,0.04)' };
794
+ function updateHeatmap() {
795
+ const grid=document.getElementById('heatmap');
796
+ if (!grid.children.length) {
797
+ const f=document.createDocumentFragment();
798
+ for (let i=0;i<60;i++){const d=document.createElement('div');d.className='hm-cell';f.appendChild(d);}
799
+ grid.appendChild(f);
800
+ }
801
+ heatmapCells.forEach((cls,i)=>{grid.children[i].style.background=HM[cls]||HM[null];});
802
+ }
803
+ function updateSummary() {
804
+ setText('sum-rate',totalPackets>0?(totalIntrusions/totalPackets*100).toFixed(1)+'%':'β€”');
805
+ setText('sum-conf',totalPackets>0?(confSum/totalPackets*100).toFixed(1)+'%':'β€”');
806
+ setText('sum-peak',peakClass||'β€”');
807
+ }
808
+ function termLog(type, msg) {
809
+ const wrap=document.getElementById('termWrap'), now=new Date();
810
+ const ts=`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
811
+ const div=document.createElement('div'); div.className='term-line';
812
+ div.innerHTML=`<span class="ts">[${ts}]</span> <span class="${type}">${msg}</span>`;
813
+ wrap.appendChild(div); wrap.scrollTop=wrap.scrollHeight;
814
+ while (wrap.children.length>50) wrap.removeChild(wrap.firstChild);
815
+ }
816
+ const CLS_TO_MC={DoS:'mc-intrusions',Probe:'mc-probe',R2L:'mc-intrusions',U2R:'mc-u2r',normal:'mc-normal'};
817
+ function flashMetric(cls){const el=document.getElementById(CLS_TO_MC[cls]||'mc-total');if(!el)return;el.classList.remove('flash');void el.offsetWidth;el.classList.add('flash');}
818
+
819
+ function startMonitor() {
820
+ if (isRunning) return; isRunning=true;
821
+ const speed=parseInt(document.getElementById('speedSel').value);
822
+ document.getElementById('startBtn').disabled=true; document.getElementById('stopBtn').disabled=false;
823
+ setText('liveStatus','LIVE'); document.getElementById('liveDot').className='dot';
824
+ document.getElementById('connBadge').textContent='⟳ CONNECTING'; document.getElementById('connBadge').className='idle';
825
+ termLog('info','Monitor started');
826
+ monitorInterval=setInterval(tick, speed);
827
+ sessionInterval=setInterval(()=>{
828
+ sessionSeconds++;
829
+ const m=Math.floor(sessionSeconds/60), s=sessionSeconds%60;
830
+ setText('sessionClock',`Session: ${pad(m)}:${pad(s)}`);
831
+ timelineBuckets[sessionSeconds%60]=0;
832
+ },1000);
833
+ }
834
+
835
+ function stopMonitor() {
836
+ if (!isRunning) return; isRunning=false;
837
+ clearInterval(monitorInterval); clearInterval(sessionInterval);
838
+ document.getElementById('startBtn').disabled=false; document.getElementById('stopBtn').disabled=true;
839
+ setText('liveStatus','PAUSED'); document.getElementById('liveDot').className='dot red';
840
+ document.getElementById('connBadge').textContent='β–  STOPPED'; document.getElementById('connBadge').className='idle';
841
+ termLog('warn',`Stopped. ${totalPackets} packets analyzed.`);
842
+ }
843
+
844
+ function clearAll() {
845
+ stopMonitor(); Object.keys(counts).forEach(k=>counts[k]=0);
846
+ totalPackets=0; totalIntrusions=0; confSum=0; packetId=0;
847
+ confBuckets={90:0,80:0,70:0,low:0};
848
+ timelineBuckets=Array(60).fill(0); heatmapCells=Array(60).fill(null);
849
+ peakClass=null; sessionSeconds=0; usingRealModel=false;
850
+ setText('sessionClock','Session: 00:00'); setText('liveStatus','IDLE');
851
+ document.getElementById('liveDot').className='dot';
852
+ document.getElementById('feedBody').innerHTML='';
853
+ document.getElementById('feedTable').style.display='none';
854
+ document.getElementById('emptyState').style.display='';
855
+ document.getElementById('alertCount').textContent='0 ALERTS';
856
+ document.getElementById('connBadge').className='idle';
857
+ document.getElementById('connBadge').textContent='β€” IDLE';
858
+ document.getElementById('termWrap').innerHTML=`<div class="term-line"><span class="ts">[--:--:--]</span> <span class="info">Reset.</span></div>`;
859
+ updateMetrics(); updateBars(); updateConfBars(); updateHeatmap();
860
+ ['sum-rate','sum-conf','sum-peak'].forEach(id=>setText(id,'β€”'));
861
+ setText('sum-model','LOCAL');
862
+ }
863
+
864
+ // ════════════════════════════════════════════════
865
+ // UTILS
866
+ // ════════════════════════════════════════════════
867
+ function setText(id, v) { const el=document.getElementById(id); if(el) el.textContent=v; }
868
+ function setWidth(id, pct){ const el=document.getElementById(id); if(el) el.style.width=pct+'%'; }
869
+ function pad(n) { return String(n).padStart(2,'0'); }
870
+ function formatETA(secs) { if(secs<60) return secs.toFixed(0)+'s'; return `${Math.floor(secs/60)}m ${pad(Math.floor(secs%60))}s`; }
871
+
872
+ // Init
873
+ window.addEventListener('load', () => { updateHeatmap(); updateTimeline(); });
hf_space/frontend/index.html ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>SentinelNet β€” Live Threat Monitor</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="style.css">
10
+ </head>
11
+ <body>
12
+ <div class="app">
13
+
14
+ <header>
15
+ <div class="logo">
16
+ <div class="logo-shield">πŸ›‘</div>
17
+ <div><div class="logo-name">SentinelNet</div><div class="logo-tag">Intrusion Detection System Β· NSL-KDD</div></div>
18
+ </div>
19
+ <div class="header-right">
20
+ <div id="sessionClock">Session: 00:00</div>
21
+ <div class="live-badge"><div class="dot" id="liveDot"></div><span id="liveStatus">IDLE</span></div>
22
+ </div>
23
+ </header>
24
+
25
+ <div class="tab-nav">
26
+ <button class="tab-btn active" onclick="switchTab('live',this)"><span class="tab-icon">πŸ“‘</span>LIVE MONITOR</button>
27
+ <button class="tab-btn" onclick="switchTab('csv',this)"><span class="tab-icon">πŸ“‚</span>CSV ANALYSIS</button>
28
+ </div>
29
+
30
+ <!-- TAB 1: LIVE MONITOR -->
31
+ <div class="tab-pane active" id="tab-live">
32
+ <div class="config-strip">
33
+ <div class="speed-wrap"><span>Speed</span>
34
+ <select id="speedSel">
35
+ <option value="2000">Slow (0.5/s)</option>
36
+ <option value="1000" selected>Normal (1/s)</option>
37
+ <option value="500">Fast (2/s)</option>
38
+ <option value="200">Turbo (5/s)</option>
39
+ </select>
40
+ </div>
41
+ <button class="btn btn-start" id="startBtn" onclick="startMonitor()">β–Ά START MONITOR</button>
42
+ <button class="btn btn-stop" id="stopBtn" onclick="stopMonitor()" disabled>β–  STOP</button>
43
+ <button class="btn btn-clear" onclick="clearAll()">β†Ί RESET</button>
44
+ <div id="connBadge" class="idle">β€” IDLE</div>
45
+ </div>
46
+
47
+ <div class="metrics">
48
+ <div class="mc" id="mc-total"><div class="mc-label">Packets Analyzed</div><div class="mc-val blue" id="m-total">0</div><div class="mc-sub" id="m-rate">0 /sec</div><div class="mc-bar blue" id="mb-total" style="width:0%"></div></div>
49
+ <div class="mc" id="mc-normal"><div class="mc-label">Normal Traffic</div><div class="mc-val green" id="m-normal">0</div><div class="mc-sub" id="m-normal-pct">β€”</div><div class="mc-bar green" id="mb-normal" style="width:0%"></div></div>
50
+ <div class="mc" id="mc-intrusions"><div class="mc-label">Intrusions</div><div class="mc-val red" id="m-intrusions">0</div><div class="mc-sub" id="m-intrusions-pct">β€”</div><div class="mc-bar red" id="mb-intrusions" style="width:0%"></div></div>
51
+ <div class="mc" id="mc-dos"><div class="mc-label">DoS Attacks</div><div class="mc-val red" id="m-dos">0</div><div class="mc-sub">Denial of service</div><div class="mc-bar red" id="mb-dos" style="width:0%"></div></div>
52
+ <div class="mc" id="mc-probe"><div class="mc-label">Probes</div><div class="mc-val blue" id="m-probe">0</div><div class="mc-sub">Reconnaissance</div><div class="mc-bar blue" id="mb-probe" style="width:0%"></div></div>
53
+ <div class="mc" id="mc-u2r"><div class="mc-label">Critical (U2R)</div><div class="mc-val purple" id="m-u2r">0</div><div class="mc-sub">Privilege escalation</div><div class="mc-bar purple" id="mb-u2r" style="width:0%"></div></div>
54
+ </div>
55
+
56
+ <div class="main-grid">
57
+ <div class="panel">
58
+ <div class="panel-head"><span>// LIVE DETECTION FEED</span><span class="red" id="alertCount">0 ALERTS</span></div>
59
+ <div class="feed-wrap">
60
+ <div class="empty-state" id="emptyState"><div class="empty-icon">πŸ“‘</div>Press START MONITOR to begin</div>
61
+ <table class="feed-table" id="feedTable" style="display:none">
62
+ <thead><tr><th>#</th><th>TIME</th><th>PROTOCOL</th><th>SRC BYTES</th><th>PREDICTION</th><th>CONFIDENCE</th><th>SEVERITY</th></tr></thead>
63
+ <tbody id="feedBody"></tbody>
64
+ </table>
65
+ </div>
66
+ </div>
67
+ <div class="side-panels">
68
+ <div class="panel">
69
+ <div class="panel-head"><span>// ATTACK DISTRIBUTION</span><span id="distTotal" class="accent">0 total</span></div>
70
+ <div style="padding:14px 18px">
71
+ <div class="bar-row"><div class="bar-lbl">normal</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--accent)" id="bar-normal"></div></div><div class="bar-cnt" id="bc-normal">0</div></div>
72
+ <div class="bar-row"><div class="bar-lbl">DoS</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--red)" id="bar-DoS"></div></div><div class="bar-cnt" id="bc-DoS">0</div></div>
73
+ <div class="bar-row"><div class="bar-lbl">Probe</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--cyan)" id="bar-Probe"></div></div><div class="bar-cnt" id="bc-Probe">0</div></div>
74
+ <div class="bar-row"><div class="bar-lbl">R2L</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--amber)" id="bar-R2L"></div></div><div class="bar-cnt" id="bc-R2L">0</div></div>
75
+ <div class="bar-row"><div class="bar-lbl">U2R</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--purple)" id="bar-U2R"></div></div><div class="bar-cnt" id="bc-U2R">0</div></div>
76
+ </div>
77
+ </div>
78
+ <div class="panel">
79
+ <div class="panel-head"><span>// THREAT TIMELINE</span><span class="accent" id="tlWindow">last 60s</span></div>
80
+ <div class="timeline-wrap"><canvas class="tl-canvas" id="tlCanvas"></canvas></div>
81
+ </div>
82
+ <div class="panel">
83
+ <div class="panel-head"><span>// SYSTEM LOG</span></div>
84
+ <div class="term-wrap" id="termWrap"><div class="term-line"><span class="ts">[--:--:--]</span> <span class="info">SentinelNet initialized.</span></div></div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="bottom-grid">
90
+ <div class="panel">
91
+ <div class="panel-head"><span>// ACTIVITY HEATMAP</span><span class="accent" style="font-size:9px">last 60 packets</span></div>
92
+ <div class="heatmap-grid" id="heatmap"></div>
93
+ </div>
94
+ <div class="panel">
95
+ <div class="panel-head"><span>// CONFIDENCE DISTRIBUTION</span><span id="avgConf" class="accent">avg: β€”</span></div>
96
+ <div style="padding:14px 18px">
97
+ <div class="bar-row"><div class="bar-lbl">90-100%</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--accent)" id="conf-90"></div></div><div class="bar-cnt" id="cbc-90">0</div></div>
98
+ <div class="bar-row"><div class="bar-lbl">80-90%</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--cyan)" id="conf-80"></div></div><div class="bar-cnt" id="cbc-80">0</div></div>
99
+ <div class="bar-row"><div class="bar-lbl">70-80%</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--amber)" id="conf-70"></div></div><div class="bar-cnt" id="cbc-70">0</div></div>
100
+ <div class="bar-row"><div class="bar-lbl">&lt;70%</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--red)" id="conf-low"></div></div><div class="bar-cnt" id="cbc-low">0</div></div>
101
+ </div>
102
+ </div>
103
+ <div class="panel">
104
+ <div class="panel-head"><span>// SESSION SUMMARY</span></div>
105
+ <div class="summary-grid">
106
+ <div class="sum-item"><div class="sum-label">Detection Rate</div><div class="sum-val" id="sum-rate" style="color:var(--accent)">β€”</div></div>
107
+ <div class="sum-item"><div class="sum-label">Avg Confidence</div><div class="sum-val" id="sum-conf" style="color:var(--cyan)">β€”</div></div>
108
+ <div class="sum-item"><div class="sum-label">Peak Threat</div><div class="sum-val" id="sum-peak" style="color:var(--red)">β€”</div></div>
109
+ <div class="sum-item"><div class="sum-label">Model Source</div><div class="sum-val" id="sum-model" style="color:var(--amber);font-size:13px">LOCAL</div></div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <!-- TAB 2: CSV ANALYSIS -->
116
+ <div class="tab-pane" id="tab-csv">
117
+ <div id="csvUploadSection">
118
+ <div class="upload-zone" id="uploadZone">
119
+ <input type="file" id="csvFileInput" accept=".csv" onchange="handleFileSelect(event)"/>
120
+ <span class="upload-icon">πŸ“</span>
121
+ <div class="upload-title">Drop your NSL-KDD CSV here</div>
122
+ <div class="upload-sub">Auto-detects headers Β· Batch processed Β· Instant report</div>
123
+ <div class="upload-hint">ACCEPTED: .csv Β· NSL-KDD format Β· 42 or 43 columns Β· Any size</div>
124
+ </div>
125
+ <div style="margin-top:22px;display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
126
+ <div class="panel"><div class="panel-head"><span>// SUPPORTED FORMATS</span></div><div style="padding:16px;font-family:var(--mono);font-size:10px;color:var(--muted2);line-height:2.4">βœ“ NSL-KDD <span style="color:var(--accent)">with headers</span><br>βœ“ NSL-KDD <span style="color:var(--accent)">without headers</span><br>βœ“ KDDTrain+.txt / KDDTest+.txt<br>βœ“ 42 or 43 columns</div></div>
127
+ <div class="panel"><div class="panel-head"><span>// BATCH PROCESSING</span></div><div style="padding:16px;font-family:var(--mono);font-size:10px;color:var(--muted2);line-height:2.4">βœ“ <span style="color:var(--cyan)">100 rows/batch</span> β€” fast<br>βœ“ Live feed while processing<br>βœ“ Auto-downloads CSV on finish<br>βœ“ Full report: 6 charts + risk gauge</div></div>
128
+ <div class="panel"><div class="panel-head"><span>// THREAT CLASSES</span></div><div style="padding:16px">
129
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-normal">normal</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Clean traffic</span></div>
130
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-DoS">DoS</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Denial of service</span></div>
131
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-Probe">Probe</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Reconnaissance</span></div>
132
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-R2L">R2L</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Remote to local</span></div>
133
+ <div style="display:flex;align-items:center;gap:8px"><span class="cls-badge cls-U2R">U2R</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Privilege escalation</span></div>
134
+ </div></div>
135
+ </div>
136
+ </div>
137
+
138
+ <div class="processing-area" id="csvProcessingArea">
139
+ <div id="formatBanner" class="format-banner ok" style="display:none"></div>
140
+ <div class="file-card">
141
+ <div class="file-icon">πŸ“Š</div>
142
+ <div class="file-details"><div class="file-name" id="csvFileName">β€”</div><div class="file-meta" id="csvFileMeta">β€”</div></div>
143
+ <div class="file-actions">
144
+ <button class="btn btn-start" id="csvStartBtn" onclick="startCsvAnalysis()">β–Ά ANALYZE</button>
145
+ <button class="btn btn-stop" id="csvStopBtn" onclick="stopCsvAnalysis()" disabled>β–  PAUSE</button>
146
+ <button class="btn btn-clear" onclick="resetCsvTab()">βœ• CLEAR</button>
147
+ </div>
148
+ </div>
149
+
150
+ <div class="progress-block" id="csvProgressBlock" style="display:none">
151
+ <div class="scan-line"></div>
152
+ <div class="progress-header"><span class="progress-title">// BATCH PROCESSING</span><span class="progress-stats" id="csvProgressStats">0 / 0 rows</span></div>
153
+ <div class="progress-track"><div class="progress-fill" id="csvProgressFill"></div></div>
154
+ <div class="progress-row">
155
+ <span id="csvProgressPct">0%</span>
156
+ <span id="csvProgressEta">ETA: β€”</span>
157
+ <span id="csvThreatRate" style="color:var(--red)">Threats: 0</span>
158
+ <span id="csvSpeedStat" style="color:var(--cyan)">β€” rows/s</span>
159
+ </div>
160
+ <div class="current-row-display" id="csvCurrentRow">Initializing…</div>
161
+ <div class="batch-status" id="batchStatus"></div>
162
+ </div>
163
+
164
+ <div class="csv-results-grid" id="csvLiveGrid" style="display:none">
165
+ <div class="panel">
166
+ <div class="panel-head"><span>// ROW ANALYSIS FEED</span><span id="csvAlertCount" class="red">0 THREATS</span></div>
167
+ <div class="csv-feed-wrap"><table class="csv-feed-table">
168
+ <thead><tr><th>ROW</th><th>PROTO</th><th>SERVICE</th><th>SRC BYTES</th><th>CLASS</th><th>CONFIDENCE</th><th>SEVERITY</th></tr></thead>
169
+ <tbody id="csvFeedBody"></tbody>
170
+ </table></div>
171
+ </div>
172
+ <div class="mini-stat-panel">
173
+ <div class="mini-card">
174
+ <div class="mini-card-title">Distribution</div>
175
+ <div class="bar-row"><div class="bar-lbl">normal</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--accent)" id="csvbar-normal"></div></div><div class="bar-cnt" id="csvbc-normal">0</div></div>
176
+ <div class="bar-row"><div class="bar-lbl">DoS</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--red)" id="csvbar-DoS"></div></div><div class="bar-cnt" id="csvbc-DoS">0</div></div>
177
+ <div class="bar-row"><div class="bar-lbl">Probe</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--cyan)" id="csvbar-Probe"></div></div><div class="bar-cnt" id="csvbc-Probe">0</div></div>
178
+ <div class="bar-row"><div class="bar-lbl">R2L</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--amber)" id="csvbar-R2L"></div></div><div class="bar-cnt" id="csvbc-R2L">0</div></div>
179
+ <div class="bar-row"><div class="bar-lbl">U2R</div><div class="bar-track"><div class="bar-fill" style="width:0%;background:var(--purple)" id="csvbar-U2R"></div></div><div class="bar-cnt" id="csvbc-U2R">0</div></div>
180
+ </div>
181
+ <div class="mini-card"><div class="mini-card-title">Avg Confidence</div><div style="font-family:var(--mono);font-size:30px;font-weight:700;color:var(--cyan)" id="csvAvgConf">β€”</div><div style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-top:4px">across all predictions</div></div>
182
+ <div class="mini-card">
183
+ <div class="mini-card-title">Severity Breakdown</div>
184
+ <div class="sev-bar-row"><div class="sev-bar-lbl">Critical</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--red)" id="sevbar-Critical"></div></div><div class="sev-bar-cnt" id="sevbc-Critical" style="color:var(--red)">0</div></div>
185
+ <div class="sev-bar-row"><div class="sev-bar-lbl">High</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--amber)" id="sevbar-High"></div></div><div class="sev-bar-cnt" id="sevbc-High" style="color:var(--amber)">0</div></div>
186
+ <div class="sev-bar-row"><div class="sev-bar-lbl">Medium</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--cyan)" id="sevbar-Medium"></div></div><div class="sev-bar-cnt" id="sevbc-Medium" style="color:var(--cyan)">0</div></div>
187
+ <div class="sev-bar-row"><div class="sev-bar-lbl">None</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--accent)" id="sevbar-None"></div></div><div class="sev-bar-cnt" id="sevbc-None" style="color:var(--accent)">0</div></div>
188
+ </div>
189
+ <div class="mini-card"><div class="mini-card-title">Processing Speed</div><div style="font-family:var(--mono);font-size:24px;font-weight:700;color:var(--accent)" id="csvProcRate">β€”</div><div style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-top:4px">rows / second</div></div>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- REPORT -->
194
+ <div class="report-section" id="reportSection">
195
+ <div class="completion-banner">
196
+ <div style="font-size:32px">βœ…</div>
197
+ <div><div class="banner-title">Analysis Complete β€” Threat Report Ready</div><div class="banner-sub" id="bannerSub">β€”</div></div>
198
+ </div>
199
+ <div class="export-bar">
200
+ <button class="btn-export btn-export-csv" onclick="exportAnnotatedCSV()">⬇ ANNOTATED CSV</button>
201
+ <button class="btn-export btn-export-pdf" onclick="exportPDFReport()">⬇ PDF REPORT</button>
202
+ <button class="btn-export btn-export-json" onclick="exportJSON()">⬇ JSON EXPORT</button>
203
+ <button class="btn-export btn-new-scan" onclick="resetCsvTab()">⟳ NEW SCAN</button>
204
+ </div>
205
+ <div class="report-header">
206
+ <div class="report-title">πŸ›‘ SentinelNet Threat Analysis Report</div>
207
+ <div class="report-subtitle" id="reportSubtitle">β€”</div>
208
+ <div class="report-meta">
209
+ <div class="report-meta-item">File: <span id="rmFile">β€”</span></div>
210
+ <div class="report-meta-item">Rows: <span id="rmRows">β€”</span></div>
211
+ <div class="report-meta-item">Date: <span id="rmDate">β€”</span></div>
212
+ <div class="report-meta-item">Model: <span id="rmModel">β€”</span></div>
213
+ <div class="report-meta-item">Duration: <span id="rmDuration">β€”</span></div>
214
+ <div class="report-meta-item">Format: <span id="rmFormat">β€”</span></div>
215
+ </div>
216
+ </div>
217
+ <div class="report-grid">
218
+ <div class="report-stat cyan"><div class="report-stat-val" id="rs-total" style="color:var(--cyan)">β€”</div><div class="report-stat-lbl">Total Rows</div></div>
219
+ <div class="report-stat red"><div class="report-stat-val" id="rs-threats" style="color:var(--red)">β€”</div><div class="report-stat-lbl">Threats Found</div></div>
220
+ <div class="report-stat amber"><div class="report-stat-val" id="rs-rate" style="color:var(--amber)">β€”</div><div class="report-stat-lbl">Threat Rate</div></div>
221
+ <div class="report-stat green"><div class="report-stat-val" id="rs-conf" style="color:var(--accent)">β€”</div><div class="report-stat-lbl">Avg Confidence</div></div>
222
+ <div class="report-stat purple"><div class="report-stat-val" id="rs-risk" style="color:var(--purple)">β€”</div><div class="report-stat-lbl">Risk Score</div></div>
223
+ </div>
224
+ <div class="report-charts">
225
+ <div class="panel"><div class="panel-head"><span>// CLASS DISTRIBUTION</span></div><div class="chart-canvas-wrap"><canvas class="chart-canvas" id="reportBarCanvas"></canvas></div></div>
226
+ <div class="panel"><div class="panel-head"><span>// CONFIDENCE WAVEFORM</span><span class="cyan">over dataset</span></div><div class="chart-canvas-wrap"><canvas class="chart-canvas" id="reportConfCanvas"></canvas></div></div>
227
+ <div class="panel"><div class="panel-head"><span>// THREAT INTENSITY</span><span class="red">rolling density</span></div><div class="chart-canvas-wrap"><canvas class="chart-canvas" id="reportIntensityCanvas"></canvas></div></div>
228
+ </div>
229
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;margin-bottom:22px">
230
+ <div class="panel"><div class="panel-head"><span>// PROTOCOL BREAKDOWN</span></div><div class="proto-bars" id="protoBreakdown"></div></div>
231
+ <div class="panel"><div class="panel-head"><span>// TOP TARGETED SERVICES</span></div><div class="services-list" id="servicesList"></div></div>
232
+ <div class="panel">
233
+ <div class="panel-head"><span>// RISK GAUGE</span></div>
234
+ <div class="risk-gauge-wrap"><canvas class="risk-gauge-canvas" id="riskGaugeCanvas"></canvas><div class="risk-label" id="riskLabel">β€”</div></div>
235
+ <div style="padding:0 18px 14px">
236
+ <div class="sev-bar-row"><div class="sev-bar-lbl">Critical</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--red)" id="rsevbar-Critical"></div></div><div class="sev-bar-cnt" id="rsevbc-Critical" style="color:var(--red)">0</div></div>
237
+ <div class="sev-bar-row"><div class="sev-bar-lbl">High</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--amber)" id="rsevbar-High"></div></div><div class="sev-bar-cnt" id="rsevbc-High" style="color:var(--amber)">0</div></div>
238
+ <div class="sev-bar-row"><div class="sev-bar-lbl">Medium</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--cyan)" id="rsevbar-Medium"></div></div><div class="sev-bar-cnt" id="rsevbc-Medium" style="color:var(--cyan)">0</div></div>
239
+ <div class="sev-bar-row"><div class="sev-bar-lbl">None</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--accent)" id="rsevbar-None"></div></div><div class="sev-bar-cnt" id="rsevbc-None" style="color:var(--accent)">0</div></div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ <div class="panel" style="margin-bottom:22px">
244
+ <div class="panel-head"><span>// ATTACK PATTERN CLUSTERS</span><span class="accent" id="clusterCount">0 clusters</span></div>
245
+ <div class="cluster-grid" id="clusterGrid"></div>
246
+ </div>
247
+ <div class="panel">
248
+ <div class="panel-head"><span>// COMPLETE ANALYSIS</span><span id="reportRowCount" class="accent">0 rows</span></div>
249
+ <div style="overflow-x:auto">
250
+ <table class="threat-table">
251
+ <thead><tr><th>ROW</th><th>PROTOCOL</th><th>SERVICE</th><th>SRC BYTES</th><th>DST BYTES</th><th>CLASS</th><th>CONFIDENCE</th><th>SEVERITY</th><th>TRUE LABEL</th></tr></thead>
252
+ <tbody id="reportTableBody"></tbody>
253
+ </table>
254
+ </div>
255
+ <div class="pager">
256
+ <button class="pager-btn" id="pgPrev" onclick="changePage(-1)" disabled>β—€ PREV</button>
257
+ <span id="pgInfo" style="flex:1;text-align:center">Page 1 of 1</span>
258
+ <button class="pager-btn" id="pgNext" onclick="changePage(1)" disabled>NEXT β–Ά</button>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ <script src="app.js"></script>
266
+ </body>
267
+ </html>
hf_space/frontend/style.css ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ══════════════════════════════════════════
2
+ SentinelNet β€” style.css
3
+ Edit this file to change colors / layout
4
+ ══════════════════════════════════════════ */
5
+
6
+ *,*::before,*::after { box-sizing:border-box; margin:0; padding:0 }
7
+
8
+ :root {
9
+ --bg:#04080d; --bg2:#080e16; --bg3:#0d1520;
10
+ --surface:#101d2e; --surface2:#162438;
11
+ --border:rgba(0,210,130,0.08);
12
+ --border2:rgba(0,210,130,0.18);
13
+ --border3:rgba(0,210,130,0.35);
14
+ --accent:#00e87a; --cyan:#00c8e8;
15
+ --red:#ff3d5a; --amber:#ffaa00; --purple:#b06fff;
16
+ --text:#d8eeff; --muted:#4a6a88; --muted2:#7a9ab8;
17
+ --mono:'IBM Plex Mono',monospace;
18
+ --sans:'Space Grotesk',sans-serif;
19
+ --glow-green:0 0 20px rgba(0,232,122,0.25);
20
+ }
21
+
22
+ html { scroll-behavior:smooth }
23
+
24
+ body {
25
+ background:var(--bg); color:var(--text);
26
+ font-family:var(--sans); min-height:100vh; overflow-x:hidden;
27
+ }
28
+
29
+ body::before {
30
+ content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
31
+ background:
32
+ radial-gradient(ellipse 60% 40% at 10% 20%,rgba(0,232,122,0.04) 0%,transparent 60%),
33
+ radial-gradient(ellipse 50% 50% at 90% 80%,rgba(0,200,232,0.03) 0%,transparent 60%),
34
+ repeating-linear-gradient(0deg,transparent,transparent 40px,rgba(0,210,130,0.015) 40px,rgba(0,210,130,0.015) 41px),
35
+ repeating-linear-gradient(90deg,transparent,transparent 40px,rgba(0,210,130,0.015) 40px,rgba(0,210,130,0.015) 41px);
36
+ }
37
+
38
+ .app { position:relative; z-index:1; max-width:1500px; margin:0 auto; padding:0 24px 80px }
39
+
40
+ /* ── HEADER ── */
41
+ header {
42
+ display:flex; align-items:center; justify-content:space-between;
43
+ padding:20px 0; border-bottom:1px solid var(--border2);
44
+ margin-bottom:0; position:relative;
45
+ }
46
+ header::after {
47
+ content:''; position:absolute; bottom:-1px; left:0;
48
+ width:200px; height:1px;
49
+ background:linear-gradient(90deg,var(--accent),transparent);
50
+ }
51
+ .logo { display:flex; align-items:center; gap:14px }
52
+ .logo-shield {
53
+ width:44px; height:44px; border-radius:12px;
54
+ background:linear-gradient(135deg,#001a0d,#003020);
55
+ border:1px solid var(--border3); display:flex;
56
+ align-items:center; justify-content:center;
57
+ font-size:20px; box-shadow:var(--glow-green);
58
+ }
59
+ .logo-name { font-family:var(--mono); font-size:20px; font-weight:700; color:var(--accent); letter-spacing:-0.5px }
60
+ .logo-tag { font-size:10px; color:var(--muted); letter-spacing:3px; text-transform:uppercase; margin-top:3px; font-family:var(--mono) }
61
+ .header-right { display:flex; align-items:center; gap:14px }
62
+ .live-badge {
63
+ display:flex; align-items:center; gap:8px; padding:7px 16px;
64
+ border-radius:999px; border:1px solid var(--border2);
65
+ background:rgba(0,232,122,0.05);
66
+ font-family:var(--mono); font-size:11px; color:var(--accent);
67
+ }
68
+ .dot { width:7px; height:7px; border-radius:50%; background:var(--accent); animation:blink 1.4s ease-in-out infinite; flex-shrink:0 }
69
+ .dot.red { background:var(--red); animation:none; box-shadow:0 0 6px var(--red) }
70
+ .dot.amber { background:var(--amber); box-shadow:0 0 6px var(--amber) }
71
+ .dot.green { background:var(--accent); box-shadow:0 0 8px var(--accent) }
72
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
73
+ #sessionClock {
74
+ font-family:var(--mono); font-size:11px; color:var(--muted2);
75
+ background:var(--surface); padding:6px 12px;
76
+ border-radius:6px; border:1px solid var(--border);
77
+ }
78
+
79
+ /* ── TABS ── */
80
+ .tab-nav { display:flex; border-bottom:1px solid var(--border2); margin-bottom:28px }
81
+ .tab-btn {
82
+ padding:16px 32px; font-family:var(--mono); font-size:11px;
83
+ font-weight:600; letter-spacing:2px; text-transform:uppercase;
84
+ color:var(--muted); background:transparent; border:none; cursor:pointer;
85
+ transition:color .2s; border-bottom:2px solid transparent; margin-bottom:-1px;
86
+ }
87
+ .tab-btn:hover { color:var(--muted2) }
88
+ .tab-btn.active { color:var(--accent); border-bottom:2px solid var(--accent) }
89
+ .tab-btn .tab-icon { margin-right:8px }
90
+ .tab-pane { display:none }
91
+ .tab-pane.active { display:block; animation:fadeIn .25s ease }
92
+ @keyframes fadeIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:none} }
93
+
94
+ /* ── PANEL ── */
95
+ .panel { background:var(--bg2); border:1px solid var(--border); border-radius:14px; overflow:hidden }
96
+ .panel-head {
97
+ padding:12px 18px; font-family:var(--mono); font-size:10px; color:var(--muted);
98
+ border-bottom:1px solid var(--border); display:flex; justify-content:space-between;
99
+ align-items:center; letter-spacing:1.5px; text-transform:uppercase; background:rgba(0,0,0,0.2);
100
+ }
101
+ .panel-head .accent{color:var(--accent)} .panel-head .red{color:var(--red)}
102
+ .panel-head .cyan{color:var(--cyan)} .panel-head .amber{color:var(--amber)}
103
+
104
+ /* ── CONFIG STRIP ── */
105
+ .config-strip {
106
+ display:flex; gap:10px; align-items:center;
107
+ background:var(--bg2); border:1px solid var(--border2);
108
+ border-radius:12px; padding:14px 18px; margin-bottom:22px; flex-wrap:wrap;
109
+ }
110
+ .speed-wrap { display:flex; align-items:center; gap:8px; font-family:var(--mono); font-size:11px; color:var(--muted) }
111
+ .speed-wrap select {
112
+ background:var(--surface); border:1px solid var(--border2);
113
+ border-radius:7px; color:var(--cyan); font-family:var(--mono);
114
+ font-size:11px; padding:7px 12px; outline:none; cursor:pointer;
115
+ }
116
+
117
+ /* ── BUTTONS ── */
118
+ .btn { padding:8px 18px; border-radius:8px; font-family:var(--mono); font-size:11px; font-weight:700; cursor:pointer; border:none; transition:all .2s; white-space:nowrap; letter-spacing:.5px }
119
+ .btn-start { background:rgba(0,232,122,0.12); color:var(--accent); border:1px solid rgba(0,232,122,0.3) }
120
+ .btn-start:hover:not(:disabled) { background:rgba(0,232,122,0.22); box-shadow:var(--glow-green) }
121
+ .btn-start:disabled { opacity:.4; cursor:not-allowed }
122
+ .btn-stop { background:rgba(255,61,90,0.1); color:var(--red); border:1px solid rgba(255,61,90,0.25) }
123
+ .btn-stop:hover:not(:disabled) { background:rgba(255,61,90,0.2) }
124
+ .btn-stop:disabled { opacity:.4; cursor:not-allowed }
125
+ .btn-clear { background:var(--surface); color:var(--muted2); border:1px solid var(--border) }
126
+ .btn-clear:hover { color:var(--text); border-color:var(--border2) }
127
+
128
+ /* ── CONNECTION BADGE ── */
129
+ #connBadge { font-family:var(--mono); font-size:11px; padding:6px 14px; border-radius:7px; white-space:nowrap; transition:all .3s }
130
+ #connBadge.real { background:rgba(0,232,122,0.1); color:var(--accent); border:1px solid rgba(0,232,122,0.2) }
131
+ #connBadge.local { background:rgba(255,170,0,0.1); color:var(--amber); border:1px solid rgba(255,170,0,0.2) }
132
+ #connBadge.idle { background:var(--surface); color:var(--muted); border:1px solid var(--border) }
133
+
134
+ /* ── METRICS ── */
135
+ .metrics { display:grid; grid-template-columns:repeat(6,1fr); gap:12px; margin-bottom:22px }
136
+ @media(max-width:1000px) { .metrics { grid-template-columns:repeat(3,1fr) } }
137
+ .mc { background:var(--bg2); border:1px solid var(--border); border-radius:12px; padding:18px 16px; position:relative; overflow:hidden; transition:border-color .3s }
138
+ .mc.flash { border-color:var(--accent); animation:mcflash .6s ease-out }
139
+ @keyframes mcflash { 0%{box-shadow:0 0 16px rgba(0,232,122,0.5)} 100%{box-shadow:none} }
140
+ .mc-label { font-family:var(--mono); font-size:9px; letter-spacing:2px; text-transform:uppercase; color:var(--muted); margin-bottom:10px }
141
+ .mc-val { font-family:var(--mono); font-size:28px; font-weight:700; line-height:1 }
142
+ .mc-val.green{color:var(--accent)} .mc-val.red{color:var(--red)} .mc-val.amber{color:var(--amber)} .mc-val.blue{color:var(--cyan)} .mc-val.purple{color:var(--purple)}
143
+ .mc-sub { font-size:10px; color:var(--muted); margin-top:6px; font-family:var(--mono) }
144
+ .mc-bar { position:absolute; bottom:0; left:0; height:2px; transition:width .6s ease }
145
+ .mc-bar.green{background:linear-gradient(90deg,var(--accent),transparent)} .mc-bar.red{background:linear-gradient(90deg,var(--red),transparent)} .mc-bar.blue{background:linear-gradient(90deg,var(--cyan),transparent)} .mc-bar.purple{background:linear-gradient(90deg,var(--purple),transparent)}
146
+
147
+ /* ── MAIN GRID ── */
148
+ .main-grid { display:grid; grid-template-columns:1fr 360px; gap:18px; margin-bottom:18px }
149
+ @media(max-width:1100px) { .main-grid { grid-template-columns:1fr } }
150
+ .feed-wrap { max-height:480px; overflow-y:auto }
151
+ .feed-table { width:100%; border-collapse:collapse; font-size:11px }
152
+ .feed-table th { padding:9px 14px; text-align:left; font-family:var(--mono); font-size:9px; color:var(--muted); background:var(--surface); position:sticky; top:0; letter-spacing:1.5px; z-index:2 }
153
+ .feed-table td { padding:8px 14px; border-top:1px solid var(--border); font-family:var(--mono) }
154
+ .feed-table tr.new-row { animation:rowIn .4s ease-out }
155
+ @keyframes rowIn { from{opacity:0;background:rgba(0,232,122,0.07)} to{opacity:1;background:transparent} }
156
+
157
+ /* ── BADGES ── */
158
+ .cls-badge { display:inline-flex; padding:3px 9px; border-radius:5px; font-size:9px; font-weight:700; letter-spacing:.5px; font-family:var(--mono) }
159
+ .cls-normal { background:rgba(0,232,122,0.12); color:var(--accent); border:1px solid rgba(0,232,122,0.2) }
160
+ .cls-DoS { background:rgba(255,61,90,0.14); color:var(--red); border:1px solid rgba(255,61,90,0.2) }
161
+ .cls-Probe { background:rgba(0,200,232,0.12); color:var(--cyan); border:1px solid rgba(0,200,232,0.2) }
162
+ .cls-R2L { background:rgba(255,170,0,0.12); color:var(--amber); border:1px solid rgba(255,170,0,0.2) }
163
+ .cls-U2R { background:rgba(176,111,255,0.13);color:var(--purple); border:1px solid rgba(176,111,255,0.2) }
164
+
165
+ /* ── SIDE PANELS ── */
166
+ .side-panels { display:flex; flex-direction:column; gap:18px }
167
+ .bar-row { display:flex; align-items:center; gap:10px; margin-bottom:10px }
168
+ .bar-lbl { font-size:10px; font-family:var(--mono); color:var(--muted2); width:58px; flex-shrink:0 }
169
+ .bar-track{ flex:1; background:var(--surface); border-radius:3px; height:5px; overflow:hidden }
170
+ .bar-fill { height:100%; border-radius:3px; transition:width .8s cubic-bezier(.4,0,.2,1) }
171
+ .bar-cnt { font-size:10px; font-family:var(--mono); min-width:34px; text-align:right; color:var(--text) }
172
+ .timeline-wrap { padding:14px 16px }
173
+ .tl-canvas { width:100%; height:80px; display:block }
174
+ .term-wrap { max-height:180px; overflow-y:auto; padding:12px 14px; background:rgba(0,0,0,0.4) }
175
+ .term-line { font-family:var(--mono); font-size:10px; line-height:1.9; white-space:nowrap }
176
+ .term-line .ts{color:var(--muted)} .term-line .ok{color:var(--accent)} .term-line .warn{color:var(--amber)} .term-line .crit{color:var(--red)} .term-line .info{color:var(--cyan)}
177
+
178
+ /* ── BOTTOM GRID ── */
179
+ .empty-state { padding:70px 20px; text-align:center; color:var(--muted); font-family:var(--mono); font-size:12px }
180
+ .empty-icon { font-size:40px; margin-bottom:14px; opacity:.3; display:block }
181
+ .bottom-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:18px }
182
+ @media(max-width:800px) { .bottom-grid { grid-template-columns:1fr } }
183
+ .heatmap-grid { display:grid; grid-template-columns:repeat(12,1fr); gap:3px; padding:14px }
184
+ .hm-cell { aspect-ratio:1; border-radius:3px; background:var(--surface) }
185
+ .summary-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:10px; padding:14px }
186
+ .sum-item { background:var(--surface); border-radius:10px; padding:14px; border:1px solid var(--border) }
187
+ .sum-label { font-family:var(--mono); font-size:9px; color:var(--muted); letter-spacing:1.5px; text-transform:uppercase; margin-bottom:6px }
188
+ .sum-val { font-family:var(--mono); font-size:20px; font-weight:700 }
189
+
190
+ /* ══════════════════════════════════════════
191
+ CSV TAB
192
+ ══════════════════════════════════════════ */
193
+ .upload-zone {
194
+ border:2px dashed var(--border2); border-radius:20px; padding:70px 40px;
195
+ text-align:center; cursor:pointer; transition:all .3s;
196
+ background:var(--bg2); position:relative; overflow:hidden;
197
+ }
198
+ .upload-zone:hover,.upload-zone.drag-over { border-color:var(--accent); background:rgba(0,232,122,0.03) }
199
+ .upload-zone input { position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100% }
200
+ .upload-icon { font-size:56px; margin-bottom:18px; display:block; opacity:.5 }
201
+ .upload-title { font-family:var(--mono); font-size:18px; color:var(--accent); margin-bottom:10px; font-weight:700 }
202
+ .upload-sub { font-size:13px; color:var(--muted2) }
203
+ .upload-hint { display:inline-block; margin-top:18px; padding:8px 20px; border-radius:8px; border:1px solid var(--border2); font-family:var(--mono); font-size:10px; color:var(--muted); letter-spacing:1px; background:var(--surface) }
204
+
205
+ .format-banner { display:flex; align-items:center; gap:12px; padding:12px 18px; border-radius:10px; font-family:var(--mono); font-size:11px; margin-bottom:16px; border:1px solid }
206
+ .format-banner.ok { background:rgba(0,232,122,0.06); border-color:rgba(0,232,122,0.2); color:var(--accent) }
207
+
208
+ .file-card { background:var(--bg2); border:1px solid var(--border2); border-radius:14px; padding:18px 22px; margin-bottom:18px; display:flex; align-items:center; gap:18px }
209
+ .file-icon { font-size:32px; flex-shrink:0 }
210
+ .file-details { flex:1 }
211
+ .file-name { font-family:var(--mono); font-size:14px; color:var(--accent); font-weight:700 }
212
+ .file-meta { font-family:var(--mono); font-size:10px; color:var(--muted); margin-top:5px; line-height:1.8 }
213
+ .file-actions { display:flex; gap:10px }
214
+
215
+ .progress-block { background:var(--bg2); border:1px solid var(--border2); border-radius:14px; padding:24px; margin-bottom:22px }
216
+ .scan-line { height:1px; background:linear-gradient(90deg,transparent,var(--accent),transparent); animation:scanAnim 1.5s ease-in-out infinite; margin:0 0 16px }
217
+ @keyframes scanAnim { 0%,100%{opacity:0;transform:scaleX(0.2)} 50%{opacity:1;transform:scaleX(1)} }
218
+ .progress-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px }
219
+ .progress-title { font-family:var(--mono); font-size:11px; color:var(--muted); letter-spacing:2px; text-transform:uppercase }
220
+ .progress-stats { font-family:var(--mono); font-size:13px; color:var(--cyan); font-weight:600 }
221
+ .progress-track { background:var(--surface); border-radius:6px; height:10px; overflow:hidden; margin-bottom:14px }
222
+ .progress-fill { height:100%; background:linear-gradient(90deg,var(--accent),var(--cyan)); border-radius:6px; transition:width .4s ease; width:0% }
223
+ .progress-fill.warning { background:linear-gradient(90deg,var(--amber),var(--red)) }
224
+ .progress-row { display:flex; justify-content:space-between; font-family:var(--mono); font-size:10px; color:var(--muted); margin-bottom:12px; flex-wrap:wrap; gap:6px }
225
+ .current-row-display { font-family:var(--mono); font-size:10px; background:rgba(0,0,0,0.4); border-radius:8px; padding:10px 14px; color:var(--cyan); word-break:break-all; border-left:2px solid var(--accent); min-height:36px; line-height:1.6 }
226
+ .batch-status { display:flex; gap:8px; margin-top:12px; flex-wrap:wrap }
227
+ .batch-chip { font-family:var(--mono); font-size:10px; padding:4px 10px; border-radius:5px; background:var(--surface); color:var(--muted); border:1px solid var(--border) }
228
+ .batch-chip.done { color:var(--accent); border-color:rgba(0,232,122,0.3); background:rgba(0,232,122,0.07) }
229
+ .batch-chip.active { color:var(--cyan); border-color:rgba(0,200,232,0.3); background:rgba(0,200,232,0.07); animation:blink 1s infinite }
230
+
231
+ .csv-results-grid { display:grid; grid-template-columns:1fr 340px; gap:18px; margin-bottom:20px }
232
+ @media(max-width:1100px) { .csv-results-grid { grid-template-columns:1fr } }
233
+ .csv-feed-wrap { max-height:520px; overflow-y:auto }
234
+ .csv-feed-table { width:100%; border-collapse:collapse; font-size:11px }
235
+ .csv-feed-table th { padding:9px 14px; text-align:left; font-family:var(--mono); font-size:9px; color:var(--muted); background:var(--surface); position:sticky; top:0; letter-spacing:1.5px; z-index:2 }
236
+ .csv-feed-table td { padding:8px 14px; border-top:1px solid var(--border); font-family:var(--mono) }
237
+ .csv-feed-table tr.csv-new-row { animation:rowIn .3s ease-out }
238
+ .mini-stat-panel { display:flex; flex-direction:column; gap:14px }
239
+ .mini-card { background:var(--bg2); border:1px solid var(--border); border-radius:12px; padding:16px }
240
+ .mini-card-title { font-family:var(--mono); font-size:9px; color:var(--muted); letter-spacing:2px; text-transform:uppercase; margin-bottom:12px }
241
+ .sev-bar-row { display:flex; align-items:center; gap:10px; margin-bottom:9px }
242
+ .sev-bar-lbl { font-family:var(--mono); font-size:10px; width:62px; color:var(--muted2) }
243
+ .sev-bar-track{ flex:1; background:var(--surface); border-radius:3px; height:7px; overflow:hidden }
244
+ .sev-bar-fill { height:100%; border-radius:3px; transition:width .8s ease }
245
+ .sev-bar-cnt { font-family:var(--mono); font-size:10px; min-width:36px; text-align:right }
246
+
247
+ /* ══════════════════════════════════════════
248
+ REPORT
249
+ ══════════════════════════════════════════ */
250
+ .processing-area { display:none }
251
+ .processing-area.visible { display:block }
252
+ .report-section { display:none }
253
+ .report-section.visible { display:block; animation:fadeIn .4s ease }
254
+
255
+ .completion-banner {
256
+ background:linear-gradient(135deg,rgba(0,232,122,0.08),rgba(0,200,232,0.05));
257
+ border:1px solid rgba(0,232,122,0.3); border-radius:14px;
258
+ padding:20px 24px; display:flex; align-items:center; gap:18px; margin-bottom:22px;
259
+ }
260
+ .banner-title { font-family:var(--mono); font-size:15px; font-weight:700; color:var(--accent) }
261
+ .banner-sub { font-family:var(--mono); font-size:10px; color:var(--muted2); margin-top:4px; line-height:1.8 }
262
+
263
+ .export-bar { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:24px }
264
+ .btn-export { padding:10px 22px; border-radius:9px; font-family:var(--mono); font-size:11px; font-weight:700; cursor:pointer; border:none; transition:all .2s; letter-spacing:.5px }
265
+ .btn-export-csv { background:rgba(0,232,122,0.1); color:var(--accent); border:1px solid rgba(0,232,122,0.25) }
266
+ .btn-export-csv:hover { background:rgba(0,232,122,0.2) }
267
+ .btn-export-pdf { background:rgba(0,200,232,0.1); color:var(--cyan); border:1px solid rgba(0,200,232,0.25) }
268
+ .btn-export-pdf:hover { background:rgba(0,200,232,0.2) }
269
+ .btn-export-json { background:rgba(176,111,255,0.1);color:var(--purple); border:1px solid rgba(176,111,255,0.25) }
270
+ .btn-export-json:hover { background:rgba(176,111,255,0.2) }
271
+ .btn-new-scan { background:rgba(255,170,0,0.1); color:var(--amber); border:1px solid rgba(255,170,0,0.25) }
272
+ .btn-new-scan:hover { background:rgba(255,170,0,0.2) }
273
+
274
+ .report-header {
275
+ background:linear-gradient(135deg,rgba(0,232,122,0.05),rgba(0,200,232,0.03));
276
+ border:1px solid var(--border2); border-radius:18px; padding:32px 36px; margin-bottom:22px;
277
+ }
278
+ .report-title { font-family:var(--mono); font-size:24px; font-weight:700; color:var(--accent); margin-bottom:6px }
279
+ .report-subtitle { font-family:var(--mono); font-size:10px; color:var(--muted); letter-spacing:2px; text-transform:uppercase }
280
+ .report-meta { display:flex; gap:28px; margin-top:22px; flex-wrap:wrap }
281
+ .report-meta-item{ font-family:var(--mono); font-size:11px; color:var(--muted) }
282
+ .report-meta-item span { color:var(--text) }
283
+
284
+ .report-grid { display:grid; grid-template-columns:repeat(5,1fr); gap:14px; margin-bottom:22px }
285
+ @media(max-width:1000px) { .report-grid { grid-template-columns:repeat(3,1fr) } }
286
+ .report-stat { background:var(--bg2); border:1px solid var(--border); border-radius:12px; padding:20px 16px; text-align:center; position:relative; overflow:hidden }
287
+ .report-stat::before { content:''; position:absolute; top:0; left:0; right:0; height:2px }
288
+ .report-stat.green::before{background:var(--accent)} .report-stat.red::before{background:var(--red)} .report-stat.amber::before{background:var(--amber)} .report-stat.cyan::before{background:var(--cyan)} .report-stat.purple::before{background:var(--purple)}
289
+ .report-stat-val { font-family:var(--mono); font-size:30px; font-weight:700; margin-bottom:6px; line-height:1 }
290
+ .report-stat-lbl { font-family:var(--mono); font-size:9px; color:var(--muted); letter-spacing:2px; text-transform:uppercase }
291
+
292
+ .report-charts { display:grid; grid-template-columns:1fr 1fr 1fr; gap:18px; margin-bottom:22px }
293
+ @media(max-width:1000px) { .report-charts { grid-template-columns:1fr 1fr } }
294
+ .chart-canvas-wrap { padding:16px }
295
+ .chart-canvas { width:100%; height:160px; display:block }
296
+
297
+ .cluster-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:14px; padding:16px }
298
+ @media(max-width:900px) { .cluster-grid { grid-template-columns:repeat(2,1fr) } }
299
+ .cluster-card { background:var(--surface); border-radius:10px; padding:16px; border:1px solid var(--border); border-left:3px solid }
300
+ .cluster-card.DoS{border-left-color:var(--red)} .cluster-card.Probe{border-left-color:var(--cyan)} .cluster-card.R2L{border-left-color:var(--amber)} .cluster-card.U2R{border-left-color:var(--purple)} .cluster-card.normal{border-left-color:var(--accent)}
301
+ .cluster-title { font-family:var(--mono); font-size:11px; font-weight:700; margin-bottom:6px }
302
+ .cluster-count { font-family:var(--mono); font-size:24px; font-weight:700; margin-bottom:4px }
303
+ .cluster-sub { font-family:var(--mono); font-size:9px; color:var(--muted); line-height:1.8 }
304
+
305
+ .threat-table { width:100%; border-collapse:collapse; font-size:11px }
306
+ .threat-table th { padding:10px 16px; text-align:left; font-family:var(--mono); font-size:9px; color:var(--muted); background:var(--surface); position:sticky; top:0; letter-spacing:1.5px; z-index:2; border-bottom:1px solid var(--border2) }
307
+ .threat-table td { padding:9px 16px; border-top:1px solid var(--border); font-family:var(--mono); font-size:11px }
308
+ .threat-table tr.row-intrusion td:first-child { border-left:2px solid var(--red) }
309
+
310
+ .pager { display:flex; align-items:center; gap:8px; padding:12px 16px; font-family:var(--mono); font-size:11px; color:var(--muted); border-top:1px solid var(--border) }
311
+ .pager-btn { padding:5px 14px; border-radius:6px; background:var(--surface); border:1px solid var(--border2); color:var(--cyan); font-family:var(--mono); font-size:11px; cursor:pointer }
312
+ .pager-btn:disabled { opacity:.4; cursor:not-allowed }
313
+
314
+ .proto-bars { padding:14px 18px }
315
+ .proto-row { display:flex; align-items:center; gap:10px; margin-bottom:10px }
316
+ .proto-lbl { font-family:var(--mono); font-size:10px; color:var(--muted2); width:50px; flex-shrink:0 }
317
+ .proto-track{ flex:1; background:var(--surface); border-radius:3px; height:6px; overflow:hidden }
318
+ .proto-fill { height:100%; border-radius:3px; transition:width .9s ease }
319
+ .proto-cnt { font-family:var(--mono); font-size:10px; min-width:90px; text-align:right; color:var(--muted2) }
320
+ .services-list { padding:14px 18px }
321
+ .svc-row { display:flex; align-items:center; padding:6px 0; border-top:1px solid var(--border); gap:8px }
322
+ .svc-name{ font-family:var(--mono); font-size:11px; min-width:70px }
323
+ .risk-gauge-wrap { padding:16px; display:flex; flex-direction:column; align-items:center; gap:8px }
324
+ .risk-gauge-canvas { width:160px; height:90px; display:block }
325
+ .risk-label { font-family:var(--mono); font-size:11px; color:var(--muted2); text-align:center }
326
+
327
+ /* ── SCROLLBAR ── */
328
+ ::-webkit-scrollbar { width:4px; height:4px }
329
+ ::-webkit-scrollbar-track { background:transparent }
330
+ ::-webkit-scrollbar-thumb { background:var(--surface2); border-radius:2px }
hf_space/models/freq_map.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dd16ba2e3d778bc6c4cba6efba8b5ce73bb1b1d3e7a196569354f535aca1a06a
3
+ size 1256
hf_space/models/label_encoder.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a09c4b425b3b090b893296bee7a9e18722e2682e8662b1bf42f3f43c9bc21979
3
+ size 516
hf_space/models/ohe_encoder.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed4b0210959a71a85c11691248c2ed3a0443755564b64331837a10100a18e497
3
+ size 1262
hf_space/models/scaler.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d6a03b202b2039254f35835c722bec7eb444aded62b949a7ba619ed3e4102fed
3
+ size 2639
hf_space/models/selected_features.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:92f8774d7edf7bb772e468aaecda0bc665ab4fa8034be54647606d6fdd8cf163
3
+ size 640
hf_space/models/sentinel_brain.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ed03b0dc01b32e53aec54762456fabc985d39526f024696c8d8536ae5299511
3
+ size 13691057
hf_space/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ numpy
4
+ pandas
5
+ scikit-learn
6
+ joblib
7
+ gunicorn