wafaHell 0.2.0__py3-none-any.whl → 1.1.0__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/model.py CHANGED
@@ -1,23 +1,111 @@
1
1
  from datetime import datetime
2
- from sqlalchemy import create_engine, Column, Integer, String
2
+ from sqlalchemy import (
3
+ DateTime, Text, create_engine,
4
+ Column, Integer, String, UniqueConstraint
5
+ )
3
6
  from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
4
7
 
8
+ # =========================
9
+ # CONFIGURAÇÃO GLOBAL
10
+ # =========================
11
+
12
+ DATABASE_URL = "sqlite:///wafaHell.db"
13
+
14
+ engine = create_engine(
15
+ DATABASE_URL,
16
+ echo=False,
17
+ future=True
18
+ )
19
+
20
+ SessionLocal = scoped_session(
21
+ sessionmaker(
22
+ bind=engine,
23
+ autocommit=False,
24
+ autoflush=False
25
+ )
26
+ )
27
+
5
28
  Base = declarative_base()
6
29
 
30
+ # =========================
31
+ # MODELS
32
+ # =========================
33
+
7
34
  class Blocked(Base):
8
35
  __tablename__ = "blocks"
9
36
 
10
37
  id = Column(Integer, primary_key=True, autoincrement=True)
11
38
  ip = Column(String, nullable=True)
12
39
  user_agent = Column(String, nullable=True)
13
- blocked_at = Column(String, nullable=False, default=lambda: datetime.now().strftime("%H:%M:%S"))
40
+ blocked_at = Column(
41
+ String,
42
+ nullable=False,
43
+ default=lambda: datetime.now().strftime("%H:%M:%S")
44
+ )
45
+ blocked_until = Column(DateTime, nullable=False)
14
46
 
15
47
  def __repr__(self):
16
- return f"<Blocked(ip='{self.ip}', user_agent='{self.user_agent}', blocked_at='{self.blocked_at}')>"
17
-
18
- def get_session(db_url="sqlite:///wafaHell.db"):
19
- engine = create_engine(db_url, echo=False)
20
- Base.metadata.create_all(engine)
21
- session_factory = sessionmaker(bind=engine)
22
- Session = scoped_session(session_factory)
23
- return Session
48
+ return (
49
+ f"<Blocked(ip='{self.ip}', "
50
+ f"user_agent='{self.user_agent}', "
51
+ f"blocked_at='{self.blocked_at}', "
52
+ f"blocked_until='{self.blocked_until}')>"
53
+ )
54
+
55
+ class Whitelist(Base):
56
+ __tablename__ = "whitelist"
57
+ id = Column(Integer, primary_key=True, autoincrement=True)
58
+ ip = Column(String, nullable=False, unique=True)
59
+ added_at = Column(
60
+ String,
61
+ nullable=False,
62
+ default=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")
63
+ )
64
+ def __repr__(self):
65
+ return f"<Whitelist(ip='{self.ip}', added_at='{self.added_at}')>"
66
+
67
+ class WafLog(Base):
68
+ __tablename__ = "waf_logs"
69
+
70
+ id = Column(Integer, primary_key=True)
71
+ timestamp = Column(DateTime, default=datetime.utcnow)
72
+
73
+ time_bucket = Column(String(20), index=True)
74
+
75
+ attack_type = Column(String(50))
76
+ ip = Column(String(50))
77
+ path = Column(String(200))
78
+ method = Column(String(10))
79
+ payload = Column(Text)
80
+ attack_local = Column(String(50))
81
+ level = Column(String(20))
82
+
83
+ __table_args__ = (
84
+ UniqueConstraint('ip', 'attack_type', 'time_bucket', name='uix_ip_attack_time'),
85
+ )
86
+
87
+
88
+ class AdminUser(Base):
89
+ __tablename__ = "admin_user"
90
+
91
+ id = Column(Integer, primary_key=True, autoincrement=True)
92
+ login = Column(String(50), nullable=False, unique=True)
93
+ password = Column(String(255), nullable=False)
94
+
95
+ def __repr__(self):
96
+ return f"<AdminUser(login='{self.login}')>"
97
+
98
+
99
+ # =========================
100
+ # DB INIT / SESSION
101
+ # =========================
102
+
103
+ def init_db():
104
+ """Cria as tabelas uma única vez"""
105
+ Base.metadata.create_all(bind=engine)
106
+
107
+
108
+ def get_session():
109
+ """Retorna uma sessão reutilizável"""
110
+ return SessionLocal()
111
+
wafaHell/panel.py ADDED
@@ -0,0 +1,406 @@
1
+ from .model import AdminUser, WafLog, Whitelist, get_session, Blocked
2
+ from .globals import waf_cache
3
+ from .utils import Dashboard, admin
4
+ from datetime import datetime, timedelta, timezone
5
+ import re
6
+ from flask import request, jsonify, make_response, flash, session, redirect, render_template
7
+ from sqlalchemy import func
8
+ import csv
9
+ import io
10
+ from werkzeug.security import check_password_hash
11
+
12
+ dashboard = Dashboard()
13
+
14
+ def get_logs_and_stats(ip_filter=None, type_filter=None, limit=100):
15
+ session = get_session()
16
+ try:
17
+ log_query = session.query(WafLog).order_by(WafLog.id.desc())
18
+ stat_query = session.query(func.count(WafLog.id))
19
+
20
+ if ip_filter:
21
+ log_query = log_query.filter(WafLog.ip == ip_filter)
22
+ stat_query = stat_query.filter(WafLog.ip == ip_filter)
23
+ if type_filter:
24
+ log_query = log_query.filter(WafLog.attack_type == type_filter)
25
+ stat_query = stat_query.filter(WafLog.attack_type == type_filter)
26
+
27
+ db_logs = log_query.limit(limit).all()
28
+
29
+ # Estatísticas Dinâmicas
30
+ stats = {
31
+ "total": stat_query.scalar() or 0,
32
+ # Agora attacks conta tudo que não é 'Info' ou 'System'
33
+ "attacks": stat_query.filter(WafLog.attack_type.in_(['SQLI', 'XSS', 'RATE LIMIT'])).scalar() or 0,
34
+ "sqli": stat_query.filter(WafLog.attack_type == 'SQLI').scalar() or 0,
35
+ "xss": stat_query.filter(WafLog.attack_type == 'XSS').scalar() or 0,
36
+ "rate_limit": stat_query.filter(WafLog.attack_type == 'RATE LIMIT').scalar() or 0,
37
+ "blocks": stat_query.filter(WafLog.attack_type == 'IP BLOCK').scalar() or 0
38
+ }
39
+
40
+ formatted_logs = []
41
+ for log in db_logs:
42
+ # Determinamos o "Tipo" visual para o HTML
43
+ display_type = "INFO"
44
+ if log.attack_type in ['SQLI', 'XSS', 'RATE LIMIT']:
45
+ display_type = "ATTACK"
46
+ elif log.attack_type == 'IP BLOCK':
47
+ display_type = "BLOCKED" # Mudança aqui para o dashboard
48
+
49
+ formatted_logs.append({
50
+ "timestamp": log.timestamp.strftime("%H:%M:%S - %d/%m/%Y"),
51
+ "type": display_type,
52
+ "details": {
53
+ "attack_type": log.attack_type,
54
+ "ip": log.ip or "---",
55
+ "path": log.path or "---",
56
+ "method": log.method or "---",
57
+ "payload": log.payload or "---",
58
+ "attack_local": log.attack_local or "---"
59
+ }
60
+ })
61
+ return formatted_logs, stats
62
+ finally:
63
+ session.close()
64
+
65
+ def setup_dashboard(app, custom_path=None):
66
+ target_path = custom_path or '/admin/dashboard'
67
+
68
+ @app.route(target_path + "/login", methods=["GET", "POST"])
69
+ def login():
70
+ if request.method == "POST":
71
+ username = request.form.get("user")
72
+ password = request.form.get("password")
73
+ db_session = get_session()
74
+ try:
75
+ admin = db_session.query(AdminUser).filter(AdminUser.login == username).first()
76
+
77
+ # 3. Valida (Aqui você deveria usar hash, mas vamos focar na lógica)
78
+ if admin and check_password_hash(admin.password, password):
79
+ session["logged_in"] = True
80
+ next_page = request.args.get("next", target_path)
81
+ return redirect(next_page)
82
+ else:
83
+ flash("Acesso Negado: Credenciais Inválidas", "error")
84
+ return redirect(request.url)
85
+ finally:
86
+ db_session.close()
87
+
88
+ return render_template("login.html")
89
+
90
+ @app.route(target_path + '/data')
91
+ @admin
92
+ def wafahell_data():
93
+ ip_f = request.args.get('ip')
94
+ type_f = request.args.get('type')
95
+ logs, stats = get_logs_and_stats(ip_filter=ip_f, type_filter=type_f)
96
+ return jsonify({"logs": logs, "stats": stats})
97
+
98
+ # Rota 2: Retorna o HTML inicial
99
+ @app.route(target_path)
100
+ @admin
101
+ def wafahell_dashboard():
102
+ ip_f = request.args.get('ip')
103
+ type_f = request.args.get('type')
104
+ logs, stats = get_logs_and_stats(ip_filter=ip_f, type_filter=type_f)
105
+ return render_template('dashboard.html', logs=logs, stats=stats, filters={'ip': ip_f, 'type': type_f})
106
+
107
+ @app.route(target_path + '/export/csv')
108
+ @admin
109
+ def export_csv():
110
+ try:
111
+ ip_filter = request.args.get('ip')
112
+ type_filter = request.args.get('type')
113
+
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()
124
+
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
+ ])
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
+
152
+ @app.route(target_path + '/block_ip', methods=['POST'])
153
+ @admin
154
+ def block_ip() -> bool:
155
+ data = request.get_json()
156
+ ip = data.get('ip')
157
+ block_time = data.get('block_time_minutes', 5) # Pega do JSON ou assume 5
158
+
159
+ if not ip:
160
+ return jsonify({"status": "error", "message": "IP ausente"}), 400
161
+
162
+ session = get_session()
163
+
164
+ try:
165
+ exists = session.query(Blocked).filter_by(ip=ip, user_agent="MANUAL_BLOCK").first()
166
+ if exists:
167
+ exists.blocked_until = datetime.now(timezone.utc) + timedelta(minutes=int(block_time))
168
+ session.commit()
169
+ return jsonify({"status": "success", "message": "Tempo de bloqueio atualizado"})
170
+
171
+ now = datetime.now(timezone.utc)
172
+ until = now + timedelta(minutes=int(block_time))
173
+
174
+ new_block = Blocked(
175
+ ip=ip,
176
+ user_agent="MANUAL_BLOCK",
177
+ blocked_until=until
178
+ )
179
+
180
+ session.add(new_block)
181
+ session.commit()
182
+ return jsonify({"status": "success", "message": "IP bloqueado com sucesso"})
183
+ except Exception as e:
184
+ session.rollback()
185
+ return jsonify({"status": "error", "message": str(e)}), 500
186
+ finally:
187
+ session.close()
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
+
230
+ @app.route(target_path + '/graphs', methods=['GET'])
231
+ @admin
232
+ def graphs():
233
+ return render_template('graphs.html')
234
+
235
+ @app.route(target_path + '/stats', methods=['GET'])
236
+ @admin
237
+ def api_stats():
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()
404
+
405
+ print(f" * [WafaHell] Dashboard e API de dados prontos em: {target_path}")
406
+
wafaHell/rateLimiter.py CHANGED
@@ -17,5 +17,4 @@ class RateLimiter:
17
17
  self.requests_log[key].popleft()
18
18
 
19
19
  self.requests_log[key].append(now)
20
-
21
20
  return len(self.requests_log[key]) >= self.limit