saidimn commited on
Commit
fb761f3
Β·
1 Parent(s): e72527a

Deploy IDS backend

Browse files
Files changed (4) hide show
  1. Dockerfile +36 -0
  2. README.md +27 -4
  3. app.py +296 -0
  4. requirements.txt +8 -0
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingFace Spaces requires port 7860 β€” do not change
2
+ FROM python:3.10-slim
3
+
4
+ # Create non-root user (required by HF Spaces)
5
+ RUN useradd -m -u 1000 user
6
+ USER user
7
+ ENV PATH="/home/user/.local/bin:$PATH"
8
+
9
+ WORKDIR /app
10
+
11
+ # Install dependencies first (layer caching)
12
+ COPY --chown=user requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy the rest of the backend code
16
+ COPY --chown=user . .
17
+
18
+ # Download model from HuggingFace Hub at build time
19
+ RUN python - <<'EOF'
20
+ from huggingface_hub import snapshot_download
21
+ import os
22
+ os.makedirs("models", exist_ok=True)
23
+ snapshot_download(
24
+ repo_id="saidimn/ids-cnn-cicids2017",
25
+ local_dir="models",
26
+ ignore_patterns=["*.git*", ".gitattributes"]
27
+ )
28
+ print("Models downloaded successfully.")
29
+ EOF
30
+
31
+ # HF Spaces only allows port 7860
32
+ EXPOSE 7860
33
+
34
+ # Start Flask via gunicorn on port 7860
35
+ # Change "app:app" if your Flask instance is named differently
36
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--timeout", "120", "app:app"]
README.md CHANGED
@@ -1,10 +1,33 @@
1
  ---
2
- title: Ids Backend
3
- emoji: 🌍
4
- colorFrom: purple
5
  colorTo: blue
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: IDS Backend
3
+ emoji: πŸ›‘οΈ
4
+ colorFrom: red
5
  colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # IDS Backend β€” Flask + PyTorch
12
+
13
+ Network Intrusion Detection System API deployed on Hugging Face Spaces.
14
+
15
+ - **Model:** CNN trained on CICIDS2017 (`saidimn/ids-cnn-cicids2017`)
16
+ - **Framework:** Flask + PyTorch
17
+ - **Port:** 7860
18
+
19
+ ## Endpoints
20
+
21
+ | Method | Route | Description |
22
+ |--------|-------|-------------|
23
+ | GET | `/` | Health check |
24
+ | GET | `/health` | Server status |
25
+ | POST | `/predict` | Run inference |
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ curl -X POST https://saidimn-ids-backend.hf.space/predict \
31
+ -H "Content-Type: application/json" \
32
+ -d '{"features": [0.1, 0.2, ...]}'
33
+ ```
app.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from flask_cors import CORS
3
+ import pandas as pd
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn as nn
7
+ import joblib
8
+ from collections import Counter
9
+ import os
10
+ from pathlib import Path
11
+ from huggingface_hub import hf_hub_download
12
+
13
+ app = Flask(__name__)
14
+ CORS(app, origins="*")
15
+
16
+ @app.after_request
17
+ def after_request(response):
18
+ response.headers['Access-Control-Allow-Origin'] = '*'
19
+ response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
20
+ response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
21
+ return response
22
+ # ══════════════════════════════════════════════════════════════════
23
+ # CONFIGURATION
24
+ # ══════════════════════════════════════════════════════════════════
25
+
26
+ # Ton repo Hugging Face
27
+ HF_REPO_ID = "saidimn/ids-cnn-cicids2017"
28
+
29
+ # Dossier local pour stocker les modèles téléchargés
30
+ CACHE_DIR = Path(__file__).parent / "model_cache"
31
+ CACHE_DIR.mkdir(exist_ok=True)
32
+
33
+ # ══════════════════════════════════════════════════════════════════
34
+ # ARCHITECTURES CNN-1D
35
+ # ══════════════════════════════════════════════════════════════════
36
+
37
+ class CNN1D_Binary(nn.Module):
38
+ def __init__(self, num_features):
39
+ super().__init__()
40
+ self.features = nn.Sequential(
41
+ nn.Conv1d(1, 64, kernel_size=3, padding=1),
42
+ nn.BatchNorm1d(64), nn.ReLU(),
43
+ nn.Conv1d(64, 64, kernel_size=3, padding=1),
44
+ nn.BatchNorm1d(64), nn.ReLU(),
45
+ nn.MaxPool1d(2), nn.Dropout(0.2),
46
+ nn.Conv1d(64, 128, kernel_size=3, padding=1),
47
+ nn.BatchNorm1d(128), nn.ReLU(),
48
+ nn.Conv1d(128, 128, kernel_size=3, padding=1),
49
+ nn.BatchNorm1d(128), nn.ReLU(),
50
+ nn.MaxPool1d(2), nn.Dropout(0.3),
51
+ nn.Conv1d(128, 256, kernel_size=3, padding=1),
52
+ nn.BatchNorm1d(256), nn.ReLU(),
53
+ nn.AdaptiveAvgPool1d(1), nn.Dropout(0.3),
54
+ )
55
+ self.classifier = nn.Sequential(
56
+ nn.Flatten(),
57
+ nn.Linear(256, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.3),
58
+ nn.Linear(128, 2)
59
+ )
60
+ def forward(self, x):
61
+ return self.classifier(self.features(x.unsqueeze(1)))
62
+
63
+ class CNN1D_Attack(nn.Module):
64
+ def __init__(self, num_features, num_classes):
65
+ super().__init__()
66
+ self.features = nn.Sequential(
67
+ nn.Conv1d(1, 64, kernel_size=3, padding=1),
68
+ nn.BatchNorm1d(64), nn.ReLU(),
69
+ nn.Conv1d(64, 64, kernel_size=3, padding=1),
70
+ nn.BatchNorm1d(64), nn.ReLU(),
71
+ nn.MaxPool1d(2), nn.Dropout(0.2),
72
+ nn.Conv1d(64, 128, kernel_size=3, padding=1),
73
+ nn.BatchNorm1d(128), nn.ReLU(),
74
+ nn.Conv1d(128, 128, kernel_size=3, padding=1),
75
+ nn.BatchNorm1d(128), nn.ReLU(),
76
+ nn.MaxPool1d(2), nn.Dropout(0.3),
77
+ nn.Conv1d(128, 256, kernel_size=3, padding=1),
78
+ nn.BatchNorm1d(256), nn.ReLU(),
79
+ nn.Conv1d(256, 256, kernel_size=3, padding=1),
80
+ nn.BatchNorm1d(256), nn.ReLU(),
81
+ nn.AdaptiveAvgPool1d(1), nn.Dropout(0.3),
82
+ )
83
+ self.classifier = nn.Sequential(
84
+ nn.Flatten(),
85
+ nn.Linear(256, 256), nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(0.4),
86
+ nn.Linear(256, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.3),
87
+ nn.Linear(128, num_classes)
88
+ )
89
+ def forward(self, x):
90
+ return self.classifier(self.features(x.unsqueeze(1)))
91
+
92
+ # ══════════════════════════════════════════════════════════════════
93
+ # TΓ‰LΓ‰CHARGEMENT DES MODÈLES DEPUIS HUGGING FACE
94
+ # ══════════════════════════════════════════════════════════════════
95
+
96
+ def download_models():
97
+ """Télécharge les modèles depuis Hugging Face Hub"""
98
+ files = {
99
+ "binary": "cnn1d_binary.pth",
100
+ "attack": "cnn1d_attacks_only.pth",
101
+ "scaler": "scaler.pkl",
102
+ "encoder": "label_encoder_attacks.pkl"
103
+ }
104
+
105
+ paths = {}
106
+ print("=" * 50)
107
+ print("Téléchargement des modèles depuis Hugging Face...")
108
+ print("=" * 50)
109
+
110
+ for key, filename in files.items():
111
+ print(" ↓ " + filename)
112
+ paths[key] = hf_hub_download(
113
+ repo_id=HF_REPO_ID,
114
+ filename=filename,
115
+ cache_dir=CACHE_DIR,
116
+ local_dir=CACHE_DIR,
117
+ local_dir_use_symlinks=False
118
+ )
119
+ print(" βœ“ " + paths[key])
120
+
121
+ return paths
122
+
123
+ # TΓ©lΓ©charge au dΓ©marrage du serveur
124
+ paths = download_models()
125
+ print("=" * 50)
126
+
127
+ # ══════════════════════════════════════════════════════════════════
128
+ # CHARGEMENT DES MODÈLES EN MΓ‰MOIRE
129
+ # ══════════════════════════════════════════════════════════════════
130
+
131
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
132
+ print("Device: " + str(device))
133
+
134
+ scaler = joblib.load(paths["scaler"])
135
+ le = joblib.load(paths["encoder"])
136
+
137
+ num_features = scaler.n_features_in_
138
+ num_attack_classes = len(le.classes_)
139
+
140
+ print("Features: " + str(num_features))
141
+ print("Classes: " + str(list(le.classes_)))
142
+
143
+ # Modèle binaire
144
+ binary_model = CNN1D_Binary(num_features).to(device)
145
+ binary_model.load_state_dict(torch.load(paths["binary"], map_location=device, weights_only=True))
146
+ binary_model.eval()
147
+
148
+ # Modèle d'attaque
149
+ attack_model = CNN1D_Attack(num_features, num_attack_classes).to(device)
150
+ attack_model.load_state_dict(torch.load(paths["attack"], map_location=device, weights_only=True))
151
+ attack_model.eval()
152
+
153
+ print("Tous les modΓ¨les sont chargΓ©s βœ“\n")
154
+
155
+ # ══════════════════════════════════════════════════════════════════
156
+ # PRÉTRAITEMENT
157
+ # ══════════════════════════════════════════════════════════════════
158
+
159
+ def preprocess(df):
160
+ df.columns = df.columns.str.strip()
161
+
162
+ cols_to_drop = ['Flow ID', 'Src IP', 'Src Port', 'Dst IP',
163
+ 'Dst Port', 'Protocol', 'Timestamp', 'Label']
164
+ for col in cols_to_drop:
165
+ if col in df.columns:
166
+ df = df.drop(columns=[col])
167
+
168
+ rename_dict = {
169
+ 'Tot Fwd Pkts': 'Total Fwd Packets',
170
+ 'Tot Bwd Pkts': 'Total Backward Packets',
171
+ 'TotLen Fwd Pkts': 'Total Length of Fwd Packets',
172
+ 'TotLen Bwd Pkts': 'Total Length of Bwd Packets',
173
+ 'Fwd Pkt Len Max': 'Fwd Packet Length Max',
174
+ 'Fwd Pkt Len Min': 'Fwd Packet Length Min',
175
+ 'Fwd Pkt Len Mean': 'Fwd Packet Length Mean',
176
+ 'Fwd Pkt Len Std': 'Fwd Packet Length Std',
177
+ 'Bwd Pkt Len Max': 'Bwd Packet Length Max',
178
+ 'Fwd Header Len': 'Fwd Header Length',
179
+ 'Bwd Header Len': 'Bwd Header Length',
180
+ 'Fwd Pkts/s': 'Fwd Packets/s',
181
+ 'Bwd Pkts/s': 'Bwd Packets/s',
182
+ 'Pkt Len Min': 'Min Packet Length',
183
+ 'Pkt Len Max': 'Max Packet Length',
184
+ 'Pkt Len Mean': 'Packet Length Mean',
185
+ 'Pkt Len Std': 'Packet Length Std',
186
+ 'Pkt Len Var': 'Packet Length Variance',
187
+ 'FIN Flag Cnt': 'FIN Flag Count',
188
+ 'SYN Flag Cnt': 'SYN Flag Count',
189
+ 'RST Flag Cnt': 'RST Flag Count',
190
+ 'PSH Flag Cnt': 'PSH Flag Count',
191
+ 'ACK Flag Cnt': 'ACK Flag Count',
192
+ 'URG Flag Cnt': 'URG Flag Count',
193
+ 'Pkt Size Avg': 'Average Packet Size',
194
+ 'Fwd Seg Size Avg': 'Avg Fwd Segment Size',
195
+ 'Bwd Seg Size Avg': 'Avg Bwd Segment Size',
196
+ 'Fwd Byts/b Avg': 'Fwd Avg Bytes/Bulk',
197
+ 'Fwd Pkts/b Avg': 'Fwd Avg Packets/Bulk',
198
+ 'Fwd Blk Rate Avg': 'Fwd Avg Bulk Rate',
199
+ 'Bwd Byts/b Avg': 'Bwd Avg Bytes/Bulk',
200
+ 'Bwd Pkts/b Avg': 'Bwd Avg Packets/Bulk',
201
+ 'Bwd Blk Rate Avg': 'Bwd Avg Bulk Rate',
202
+ 'Subflow Fwd Pkts': 'Subflow Fwd Packets',
203
+ 'Subflow Bwd Pkts': 'Subflow Bwd Packets',
204
+ 'Init Fwd Win Byts': 'Init_Win_bytes_forward',
205
+ 'Init Bwd Win Byts': 'Init_Win_bytes_backward',
206
+ 'Fwd Act Data Pkts': 'act_data_pkt_fwd',
207
+ 'Fwd Seg Size Min': 'min_seg_size_forward',
208
+ }
209
+ df = df.rename(columns=rename_dict)
210
+ df = df.select_dtypes(include=[np.number])
211
+ df.replace([np.inf, -np.inf], np.nan, inplace=True)
212
+ df.fillna(0, inplace=True)
213
+
214
+ if hasattr(scaler, 'feature_names_in_'):
215
+ for col in scaler.feature_names_in_:
216
+ if col not in df.columns:
217
+ df[col] = 0
218
+ df = df[scaler.feature_names_in_]
219
+ else:
220
+ while df.shape[1] < 78:
221
+ df['missing_' + str(df.shape[1])] = 0
222
+ df = df.iloc[:, :78]
223
+
224
+ return scaler.transform(df.values)
225
+
226
+ # ══════════════════════════════════════════════════════════════════
227
+ # ROUTES API
228
+ # ════════════════════════════════��═════════════════════════════════
229
+
230
+ @app.route('/analyze', methods=['POST'])
231
+ def analyze():
232
+ if 'file' not in request.files:
233
+ return jsonify({'error': 'No file uploaded'}), 400
234
+
235
+ try:
236
+ file = request.files['file']
237
+ df = pd.read_csv(file)
238
+
239
+ if df.empty:
240
+ return jsonify({'error': 'CSV file is empty'}), 400
241
+
242
+ total_flows = len(df)
243
+ X_scaled = preprocess(df)
244
+ X = torch.tensor(X_scaled, dtype=torch.float32).to(device)
245
+
246
+ results = []
247
+ with torch.no_grad():
248
+ binary_out = binary_model(X)
249
+ binary_pred = torch.argmax(binary_out, dim=1)
250
+
251
+ for i in range(len(X)):
252
+ if binary_pred[i] == 0:
253
+ results.append('BENIGN')
254
+ else:
255
+ single = X[i].unsqueeze(0)
256
+ attack_out = attack_model(single)
257
+ attack_pred = torch.argmax(attack_out, dim=1).item()
258
+ results.append(le.classes_[attack_pred])
259
+
260
+ counts = Counter(results)
261
+ total = len(results)
262
+
263
+ labels = list(counts.keys())
264
+ values = list(counts.values())
265
+ percentages = [round(v/total*100, 2) for v in values]
266
+
267
+ attacks = {k: v for k, v in counts.items() if k != 'BENIGN'}
268
+ benign = counts.get('BENIGN', 0)
269
+
270
+ return jsonify({
271
+ 'total_flows': total,
272
+ 'benign_count': benign,
273
+ 'attack_count': total - benign,
274
+ 'labels': labels,
275
+ 'values': values,
276
+ 'percentages': percentages,
277
+ 'attack_types': attacks,
278
+ 'results': results[:100]
279
+ })
280
+
281
+ except Exception as e:
282
+ return jsonify({'error': str(e)}), 500
283
+
284
+ @app.route('/health', methods=['GET'])
285
+ def health():
286
+ return jsonify({
287
+ 'status': 'ok',
288
+ 'device': str(device),
289
+ 'repo': HF_REPO_ID,
290
+ 'attack_classes': le.classes_.tolist()
291
+ })
292
+
293
+ if __name__ == '__main__':
294
+ import os
295
+ port = int(os.environ.get('PORT', 5000))
296
+ app.run(debug=False, port=port, host='0.0.0.0')
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ gunicorn
3
+ flask-cors
4
+ torch
5
+ numpy
6
+ pandas
7
+ scikit-learn
8
+ huggingface_hub