wafaHell 1.0.0__py3-none-any.whl → 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wafaHell/__init__.py +2 -2
- wafaHell/app.py +3 -3
- wafaHell/logger.py +6 -5
- wafaHell/middleware.py +276 -159
- wafaHell/model.py +11 -0
- wafaHell/panel.py +254 -40
- wafaHell/utils.py +303 -199
- {wafahell-1.0.0.dist-info → wafahell-1.1.1.dist-info}/METADATA +1 -1
- wafahell-1.1.1.dist-info/RECORD +15 -0
- wafahell-1.0.0.dist-info/RECORD +0 -15
- {wafahell-1.0.0.dist-info → wafahell-1.1.1.dist-info}/WHEEL +0 -0
- {wafahell-1.0.0.dist-info → wafahell-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {wafahell-1.0.0.dist-info → wafahell-1.1.1.dist-info}/top_level.txt +0 -0
wafaHell/panel.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
from .model import AdminUser, WafLog, Whitelist, get_session, Blocked
|
|
2
|
+
from .globals import waf_cache
|
|
3
|
+
from .utils import Dashboard, admin
|
|
1
4
|
from datetime import datetime, timedelta, timezone
|
|
2
|
-
|
|
3
|
-
from
|
|
5
|
+
import re
|
|
6
|
+
from flask import request, jsonify, make_response, flash, session, redirect, render_template
|
|
4
7
|
from sqlalchemy import func
|
|
5
8
|
import csv
|
|
6
9
|
import io
|
|
7
|
-
from utils import Admin, Dashboard, admin
|
|
8
10
|
from werkzeug.security import check_password_hash
|
|
11
|
+
|
|
9
12
|
dashboard = Dashboard()
|
|
10
13
|
|
|
11
14
|
def get_logs_and_stats(ip_filter=None, type_filter=None, limit=100):
|
|
@@ -67,9 +70,9 @@ def setup_dashboard(app, custom_path=None):
|
|
|
67
70
|
if request.method == "POST":
|
|
68
71
|
username = request.form.get("user")
|
|
69
72
|
password = request.form.get("password")
|
|
70
|
-
|
|
73
|
+
db_session = get_session()
|
|
71
74
|
try:
|
|
72
|
-
admin =
|
|
75
|
+
admin = db_session.query(AdminUser).filter(AdminUser.login == username).first()
|
|
73
76
|
|
|
74
77
|
# 3. Valida (Aqui você deveria usar hash, mas vamos focar na lógica)
|
|
75
78
|
if admin and check_password_hash(admin.password, password):
|
|
@@ -80,7 +83,7 @@ def setup_dashboard(app, custom_path=None):
|
|
|
80
83
|
flash("Acesso Negado: Credenciais Inválidas", "error")
|
|
81
84
|
return redirect(request.url)
|
|
82
85
|
finally:
|
|
83
|
-
|
|
86
|
+
db_session.close()
|
|
84
87
|
|
|
85
88
|
return render_template("login.html")
|
|
86
89
|
|
|
@@ -104,43 +107,48 @@ def setup_dashboard(app, custom_path=None):
|
|
|
104
107
|
@app.route(target_path + '/export/csv')
|
|
105
108
|
@admin
|
|
106
109
|
def export_csv():
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
session = get_session()
|
|
112
|
-
query = session.query(WafLog)
|
|
113
|
-
|
|
114
|
-
if ip_filter:
|
|
115
|
-
query = query.filter(WafLog.ip == ip_filter)
|
|
116
|
-
if type_filter:
|
|
117
|
-
query = query.filter(WafLog.attack_type == type_filter)
|
|
110
|
+
try:
|
|
111
|
+
ip_filter = request.args.get('ip')
|
|
112
|
+
type_filter = request.args.get('type')
|
|
118
113
|
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
session = get_session()
|
|
115
|
+
query = session.query(WafLog)
|
|
116
|
+
|
|
117
|
+
if ip_filter:
|
|
118
|
+
query = query.filter(WafLog.ip == ip_filter)
|
|
119
|
+
if type_filter:
|
|
120
|
+
query = query.filter(WafLog.attack_type == type_filter)
|
|
121
|
+
|
|
122
|
+
logs = query.order_by(WafLog.timestamp.desc()).all()
|
|
123
|
+
session.close()
|
|
121
124
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
response = make_response(output.getvalue())
|
|
140
|
-
response.headers["Content-Disposition"] = f"attachment; filename=waf_logs_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
|
|
141
|
-
response.headers["Content-type"] = "text/csv"
|
|
142
|
-
return response
|
|
125
|
+
# Criamos o CSV em memória
|
|
126
|
+
output = io.StringIO()
|
|
127
|
+
writer = csv.writer(output)
|
|
128
|
+
|
|
129
|
+
# Cabeçalho
|
|
130
|
+
writer.writerow(['Data/Hora', 'Tipo', 'IP', 'Endpoint', 'Metodo', 'Payload'])
|
|
131
|
+
|
|
132
|
+
for log in logs:
|
|
133
|
+
writer.writerow([
|
|
134
|
+
log.timestamp,
|
|
135
|
+
log.attack_type,
|
|
136
|
+
log.ip,
|
|
137
|
+
log.path,
|
|
138
|
+
log.method,
|
|
139
|
+
log.payload
|
|
140
|
+
])
|
|
143
141
|
|
|
142
|
+
response = make_response(output.getvalue())
|
|
143
|
+
response.headers["Content-Disposition"] = f"attachment; filename=waf_logs_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
|
|
144
|
+
response.headers["Content-type"] = "text/csv"
|
|
145
|
+
return response
|
|
146
|
+
except Exception as e:
|
|
147
|
+
session.rollback()
|
|
148
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
149
|
+
finally:
|
|
150
|
+
session.close()
|
|
151
|
+
|
|
144
152
|
@app.route(target_path + '/block_ip', methods=['POST'])
|
|
145
153
|
@admin
|
|
146
154
|
def block_ip() -> bool:
|
|
@@ -178,6 +186,47 @@ def setup_dashboard(app, custom_path=None):
|
|
|
178
186
|
finally:
|
|
179
187
|
session.close()
|
|
180
188
|
|
|
189
|
+
@app.route(target_path + '/blocked_list', methods=['GET'])
|
|
190
|
+
@admin
|
|
191
|
+
def get_blocked_list():
|
|
192
|
+
session = get_session()
|
|
193
|
+
try:
|
|
194
|
+
# Pega todos os IPs bloqueados que ainda não expiraram
|
|
195
|
+
now = datetime.now(timezone.utc)
|
|
196
|
+
blocks = session.query(Blocked).filter(Blocked.blocked_until > now).all()
|
|
197
|
+
|
|
198
|
+
data = []
|
|
199
|
+
for b in blocks:
|
|
200
|
+
# Calcula tempo restante
|
|
201
|
+
remaining = b.blocked_until.replace(tzinfo=timezone.utc) - now
|
|
202
|
+
mins, secs = divmod(remaining.total_seconds(), 60)
|
|
203
|
+
|
|
204
|
+
data.append({
|
|
205
|
+
"ip": b.ip,
|
|
206
|
+
"user_agent": b.user_agent[:30] + '...' if len(b.user_agent) > 30 else b.user_agent,
|
|
207
|
+
"expires_in": f"{int(mins)}m {int(secs)}s"
|
|
208
|
+
})
|
|
209
|
+
return jsonify(data)
|
|
210
|
+
finally:
|
|
211
|
+
session.close()
|
|
212
|
+
|
|
213
|
+
@app.route(target_path + '/unblock_ip', methods=['POST'])
|
|
214
|
+
@admin
|
|
215
|
+
def manual_unblock():
|
|
216
|
+
data = request.get_json()
|
|
217
|
+
ip = data.get('ip')
|
|
218
|
+
session = get_session()
|
|
219
|
+
try:
|
|
220
|
+
# Remove da tabela
|
|
221
|
+
session.query(Blocked).filter(Blocked.ip == ip).delete()
|
|
222
|
+
session.commit()
|
|
223
|
+
|
|
224
|
+
return jsonify({"status": "success"})
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return jsonify({"error": str(e)}), 500
|
|
227
|
+
finally:
|
|
228
|
+
session.close()
|
|
229
|
+
|
|
181
230
|
@app.route(target_path + '/graphs', methods=['GET'])
|
|
182
231
|
@admin
|
|
183
232
|
def graphs():
|
|
@@ -187,6 +236,171 @@ def setup_dashboard(app, custom_path=None):
|
|
|
187
236
|
@admin
|
|
188
237
|
def api_stats():
|
|
189
238
|
return jsonify(dashboard.dashboard_setup())
|
|
239
|
+
|
|
240
|
+
@app.route(target_path + '/vars', methods=['GET'])
|
|
241
|
+
@admin
|
|
242
|
+
def get_vars():
|
|
243
|
+
from middleware import Wafahell
|
|
244
|
+
waf = Wafahell()
|
|
245
|
+
|
|
246
|
+
output = {}
|
|
247
|
+
allowed_types = (str, int, float, bool, list, dict)
|
|
248
|
+
blacklisted_keys = {'app', 'log', 'recent_blocks_cache', '_instance', 'initialized', 'dashboard_path', 'rules_sqli', 'rules_xss'}
|
|
249
|
+
|
|
250
|
+
for key, value in vars(waf).items():
|
|
251
|
+
if key not in blacklisted_keys and isinstance(value, allowed_types):
|
|
252
|
+
output[key] = value
|
|
253
|
+
|
|
254
|
+
return output
|
|
255
|
+
|
|
256
|
+
@app.route(target_path + '/vars/change', methods=['POST'])
|
|
257
|
+
@admin
|
|
258
|
+
def vars_change():
|
|
259
|
+
data = request.json
|
|
260
|
+
key = data.get('key')
|
|
261
|
+
value = data.get('value')
|
|
262
|
+
|
|
263
|
+
from middleware import Wafahell
|
|
264
|
+
waf = Wafahell()
|
|
265
|
+
|
|
266
|
+
if hasattr(waf, key):
|
|
267
|
+
|
|
268
|
+
setattr(waf, key, value)
|
|
269
|
+
|
|
270
|
+
print(f" * [WafaHell] Variable '{key}' changed to: {value}")
|
|
271
|
+
|
|
272
|
+
return {"status": "success", "message": f"{key} updated", "newValue": value}
|
|
273
|
+
|
|
274
|
+
return {"status": "error", "message": "Invalid variable"}, 400
|
|
275
|
+
|
|
276
|
+
@app.route(target_path + '/import_blacklist', methods=['POST'])
|
|
277
|
+
@admin
|
|
278
|
+
def import_blacklist():
|
|
279
|
+
data = request.get_json()
|
|
280
|
+
raw_ips = data.get('ips', [])
|
|
281
|
+
|
|
282
|
+
if not raw_ips:
|
|
283
|
+
return jsonify({"status": "error", "message": "Nenhum IP fornecido."}), 400
|
|
284
|
+
|
|
285
|
+
# 1. Validação e Limpeza (Regex IPv4)
|
|
286
|
+
# Usamos set() para remover duplicatas enviadas no mesmo arquivo
|
|
287
|
+
ip_regex = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$")
|
|
288
|
+
valid_ips = set(ip for ip in raw_ips if ip_regex.match(ip))
|
|
289
|
+
|
|
290
|
+
if not valid_ips:
|
|
291
|
+
return jsonify({"status": "error", "message": "Nenhum IP válido encontrado."}), 400
|
|
292
|
+
|
|
293
|
+
session = get_session()
|
|
294
|
+
try:
|
|
295
|
+
# 2. Filtragem de Duplicatas no Banco
|
|
296
|
+
# Descobre quais desses IPs já estão bloqueados para não dar erro de Unique Key
|
|
297
|
+
existing_query = session.query(Blocked.ip).filter(Blocked.ip.in_(valid_ips)).all()
|
|
298
|
+
existing_ips = {row.ip for row in existing_query}
|
|
299
|
+
|
|
300
|
+
# Apenas os novos IPs
|
|
301
|
+
ips_to_insert = valid_ips - existing_ips
|
|
302
|
+
|
|
303
|
+
if not ips_to_insert:
|
|
304
|
+
return jsonify({"status": "success", "message": "Todos os IPs já estavam na blacklist."})
|
|
305
|
+
|
|
306
|
+
# 3. Preparação dos Dados (Bulk)
|
|
307
|
+
# Definimos um tempo padrão de 30 dias para importações manuais de lista
|
|
308
|
+
now = datetime.now(timezone.utc)
|
|
309
|
+
until = now + timedelta(days=3600)
|
|
310
|
+
|
|
311
|
+
bulk_data = []
|
|
312
|
+
for ip in ips_to_insert:
|
|
313
|
+
bulk_data.append({
|
|
314
|
+
"ip": ip,
|
|
315
|
+
"user_agent": "MANUAL_IMPORT_LIST",
|
|
316
|
+
# Mantendo o formato string que você usa no Model
|
|
317
|
+
"blocked_at": now.strftime("%H:%M:%S"),
|
|
318
|
+
"blocked_until": until
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
# 4. Atualização IMEDIATA do Cache (Fast Path)
|
|
322
|
+
# Isso garante que o bloqueio funcione no milissegundo seguinte,
|
|
323
|
+
# sem precisar esperar a próxima query no banco.
|
|
324
|
+
waf_cache.set(f"blocked_{ip}", True, expire=60)
|
|
325
|
+
|
|
326
|
+
# 5. Inserção em Massa (Muito Rápido)
|
|
327
|
+
session.bulk_insert_mappings(Blocked, bulk_data)
|
|
328
|
+
session.commit()
|
|
329
|
+
|
|
330
|
+
count = len(ips_to_insert)
|
|
331
|
+
print(f" * [WafaHell] Blacklist importada: {count} novos IPs.")
|
|
332
|
+
|
|
333
|
+
return jsonify({
|
|
334
|
+
"status": "success",
|
|
335
|
+
"message": f"Sucesso! {count} novos IPs adicionados à Blacklist."
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
session.rollback()
|
|
340
|
+
print(f"Erro na importação: {e}")
|
|
341
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
342
|
+
finally:
|
|
343
|
+
session.close()
|
|
344
|
+
|
|
345
|
+
@app.route(target_path + '/import_whitelist', methods=['POST'])
|
|
346
|
+
@admin
|
|
347
|
+
def import_whitelist():
|
|
348
|
+
data = request.get_json()
|
|
349
|
+
raw_ips = data.get('ips', [])
|
|
350
|
+
|
|
351
|
+
if not raw_ips:
|
|
352
|
+
return jsonify({"status": "error", "message": "Nenhum IP fornecido."}), 400
|
|
353
|
+
|
|
354
|
+
# 1. Validação (Regex)
|
|
355
|
+
ip_regex = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$")
|
|
356
|
+
valid_ips = set(ip for ip in raw_ips if ip_regex.match(ip))
|
|
357
|
+
|
|
358
|
+
if not valid_ips:
|
|
359
|
+
return jsonify({"status": "error", "message": "Nenhum IP válido encontrado."}), 400
|
|
360
|
+
|
|
361
|
+
session = get_session()
|
|
362
|
+
try:
|
|
363
|
+
# 2. Verifica Duplicatas no Banco
|
|
364
|
+
# Como sua coluna 'ip' é unique=True, precisamos filtrar o que já existe
|
|
365
|
+
existing_query = session.query(Whitelist.ip).filter(Whitelist.ip.in_(valid_ips)).all()
|
|
366
|
+
existing_ips = {row.ip for row in existing_query}
|
|
367
|
+
|
|
368
|
+
ips_to_insert = valid_ips - existing_ips
|
|
369
|
+
|
|
370
|
+
if not ips_to_insert:
|
|
371
|
+
return jsonify({"status": "success", "message": "Todos os IPs já estavam na whitelist."})
|
|
372
|
+
|
|
373
|
+
# 3. Prepara Bulk Insert e Cache
|
|
374
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
375
|
+
bulk_data = []
|
|
376
|
+
|
|
377
|
+
for ip in ips_to_insert:
|
|
378
|
+
bulk_data.append({
|
|
379
|
+
"ip": ip,
|
|
380
|
+
"added_at": now_str
|
|
381
|
+
})
|
|
382
|
+
# Cache Warm-up: Marca o IP como "VIP" no cache por 1 hora
|
|
383
|
+
# Isso evita consulta ao banco quando este IP acessar
|
|
384
|
+
waf_cache.set(f"whitelist_{ip}", True, expire=3600)
|
|
385
|
+
|
|
386
|
+
# 4. Grava no Banco
|
|
387
|
+
session.bulk_insert_mappings(Whitelist, bulk_data)
|
|
388
|
+
session.commit()
|
|
389
|
+
|
|
390
|
+
count = len(ips_to_insert)
|
|
391
|
+
print(f" * [WafaHell] Whitelist importada: {count} novos IPs.")
|
|
392
|
+
|
|
393
|
+
return jsonify({
|
|
394
|
+
"status": "success",
|
|
395
|
+
"message": f"Sucesso! {count} IPs adicionados à Whitelist."
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
session.rollback()
|
|
400
|
+
print(f"Erro whitelist: {e}")
|
|
401
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
402
|
+
finally:
|
|
403
|
+
session.close()
|
|
190
404
|
|
|
191
405
|
print(f" * [WafaHell] Dashboard e API de dados prontos em: {target_path}")
|
|
192
406
|
|