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/utils.py CHANGED
@@ -1,10 +1,416 @@
1
- from datetime import datetime, timedelta
1
+ from .model import WafLog, Blocked, Whitelist, AdminUser, get_session
2
+ from .globals import waf_cache
3
+ from datetime import datetime, timedelta, timezone
4
+ import secrets
5
+ import socket
6
+ import string
7
+ import time
8
+ import tomllib
9
+ from werkzeug.security import generate_password_hash
10
+ from sqlalchemy.orm import Session
11
+ from sqlalchemy import text, func, case
12
+ from functools import wraps
13
+ from flask import session, redirect, url_for, request
14
+ import geoip2.database
15
+ import os
2
16
 
3
- def is_block_expired(blocked_time_str: str) -> bool:
4
- blocked_time = datetime.strptime(blocked_time_str, "%H:%M:%S")
5
- now = datetime.now()
6
- blocked_time = blocked_time.replace(year=now.year, month=now.month, day=now.day)
17
+ class Admin:
18
+ @staticmethod
19
+ def generate_secure_password(length=64):
20
+ alphabet = string.ascii_letters + string.digits + string.punctuation
21
+ return ''.join(secrets.choice(alphabet) for _ in range(length))
7
22
 
8
- diff = now - blocked_time
9
- return diff >= timedelta(minutes=1)
23
+ @staticmethod
24
+ def create_admin_user(session: Session):
25
+ admin = session.query(AdminUser).filter_by(login="admin").first()
26
+ if admin:
27
+ return
28
+ raw_password = "admin" #Admin.generate_secure_password(64)
29
+ hashed_password = generate_password_hash(raw_password)
10
30
 
31
+ admin = AdminUser(
32
+ login="admin",
33
+ password=hashed_password
34
+ )
35
+
36
+ session.add(admin)
37
+ session.commit()
38
+
39
+ print("* [WafaHell] Usuario admin criado com sucesso.")
40
+ print("* [WafaHell] Salve essa senha em um lugar seguro, não será mostrada novamente")
41
+ print("Senha: ", raw_password)
42
+
43
+ def admin(fn):
44
+ @wraps(fn)
45
+ def wrapper(*args, **kwargs):
46
+ if not session.get("logged_in"):
47
+ return redirect(url_for("login", next=request.path))
48
+ return fn(*args, **kwargs)
49
+ return wrapper
50
+
51
+ class Dashboard:
52
+ def __init__(self):
53
+ self.geo_db_path = os.path.join(os.path.dirname(__file__), 'GeoLite2-Country.mmdb')
54
+
55
+ def dashboard_setup(self):
56
+ json = {}
57
+
58
+ def get_waf_version():
59
+ base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
60
+ toml_path = os.path.join(base_path, "pyproject.toml")
61
+
62
+ try:
63
+ with open(toml_path, "rb") as f:
64
+ data = tomllib.load(f)
65
+ return data["project"]["version"]
66
+ except FileNotFoundError:
67
+ return "v.0.0.0"
68
+
69
+ def get_server_info():
70
+ server_time = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
71
+ node_id = socket.gethostname()
72
+ avg_latency = waf_cache.get('latency_avg', default=0.0)
73
+ system_status = "critical" if avg_latency > 500 else "degraded" if avg_latency > 200 else "healthy"
74
+ return {
75
+ "server_time": server_time,
76
+ "node_id": node_id,
77
+ "average_latency_ms": round(float(avg_latency), 2),
78
+ "system_status": system_status,
79
+ "version": get_waf_version()
80
+ }
81
+
82
+ def get_kpis():
83
+ now = datetime.now(timezone.utc)
84
+ last_24h = now - timedelta(hours=24)
85
+ prev_24h = now - timedelta(hours=48)
86
+ session = get_session()
87
+
88
+ try:
89
+ # --- TOTAIS DE HOJE ---
90
+ total_today = session.query(func.count(WafLog.id)).filter(WafLog.timestamp >= last_24h).scalar() or 0
91
+ blocked_today = session.query(func.count(WafLog.id)).filter(
92
+ WafLog.timestamp >= last_24h,
93
+ WafLog.attack_type != 'INFO'
94
+ ).scalar() or 0
95
+
96
+ # --- TOTAIS DE ONTEM (Para Tendência) ---
97
+ total_yesterday = session.query(func.count(WafLog.id)).filter(
98
+ WafLog.timestamp >= prev_24h,
99
+ WafLog.timestamp < last_24h
100
+ ).scalar() or 0
101
+
102
+ blocked_yesterday = session.query(func.count(WafLog.id)).filter(
103
+ WafLog.timestamp >= prev_24h,
104
+ WafLog.timestamp < last_24h,
105
+ WafLog.attack_type != 'INFO'
106
+ ).scalar() or 0
107
+
108
+ # --- CÁLCULO DE TENDÊNCIA (%) ---
109
+ def calc_trend(current, previous):
110
+ if previous == 0:
111
+ return 100.0 if current > 0 else 0.0
112
+ return round(((current - previous) / previous * 100), 1)
113
+
114
+ trend_total = calc_trend(total_today, total_yesterday)
115
+ trend_attacks = calc_trend(blocked_today, blocked_yesterday)
116
+
117
+ # --- INFOS COMPLEMENTARES ---
118
+ from globals import waf_cache
119
+ # Latência e RPS vindos do Cache Global
120
+ last_second_timestamp = int(time.time()) - 1
121
+ rps_key = f"rps_{last_second_timestamp}"
122
+
123
+ # 2. Lê o valor real do cache para esse segundo específico
124
+ current_rps = waf_cache.get(rps_key, default=0)
125
+
126
+ # 3. Lógica do Pico (Peak)
127
+ # Aqui continuamos usando uma chave fixa 'rps_peak_hour' para guardar o recorde
128
+ peak_rps = waf_cache.get('rps_peak_hour', default=0)
129
+
130
+ if current_rps > peak_rps:
131
+ peak_rps = current_rps
132
+ # Atualiza o recorde no cache por 1 hora
133
+ waf_cache.set('rps_peak_hour', peak_rps, expire=3600)
134
+
135
+ # Dados da Blacklist
136
+ total_blacklist = session.query(func.count(Blocked.id)).scalar() or 0
137
+ # Como blocked_at é String formatada no seu modelo, comparamos com o horário
138
+ added_today = session.query(func.count(Blocked.id)).filter(
139
+ Blocked.blocked_until >= now # Um IP "adicionado hoje" é tecnicamente um ainda bloqueado
140
+ ).scalar() or 0
141
+
142
+ return {
143
+ "total_requests_24h": {
144
+ "value": total_today,
145
+ "trend_percent": trend_total
146
+ },
147
+ "attacks_mitigated_24h": {
148
+ "value": blocked_today,
149
+ "trend_percent": trend_attacks # Adicionado aqui
150
+ },
151
+ "throughput": {
152
+ "current_req_per_sec": current_rps,
153
+ "peak_last_hour": peak_rps
154
+ },
155
+ "blacklist_count": {
156
+ "total_ips": total_blacklist,
157
+ "added_today": added_today
158
+ }
159
+ }
160
+
161
+ except Exception as e:
162
+ print(f"Erro no Dashboard: {e}")
163
+ finally:
164
+ session.close()
165
+
166
+ def get_traffic_chart():
167
+ now = datetime.now(timezone.utc)
168
+ start_time = now - timedelta(minutes=40)
169
+ session = get_session()
170
+ try:
171
+ # Query para agrupar por minuto
172
+ # No SQLite usamos strftime, no Postgres/MySQL seria date_format ou similar
173
+ query = session.query(
174
+ func.strftime('%H:%M', WafLog.timestamp).label('minute'),
175
+ func.count(WafLog.id).label('total'),
176
+ func.sum(case({WafLog.attack_type == 'INFO': 1}, else_=0)).label('legit'),
177
+ func.sum(case({WafLog.attack_type != 'INFO': 1}, else_=0)).label('attacks')
178
+ ).filter(WafLog.timestamp >= start_time)\
179
+ .group_by('minute')\
180
+ .order_by('minute').all()
181
+
182
+ labels = []
183
+ series_legit = []
184
+ series_attacks = []
185
+
186
+ # Preenche os arrays para o gráfico
187
+ for row in query:
188
+ labels.append(row.minute)
189
+ series_legit.append(row.legit or 0)
190
+ series_attacks.append(row.attacks or 0)
191
+
192
+ # Caso não existam dados nos últimos 40 min, retorna arrays vazios para não quebrar o front
193
+ return {
194
+ "labels": labels,
195
+ "series_legit": series_legit,
196
+ "series_attacks": series_attacks
197
+ }
198
+ except Exception as e:
199
+ print(f"Erro no Dashboard: {e}")
200
+ finally:
201
+ session.close()
202
+
203
+ def get_distribution_vectors():
204
+ now = datetime.now(timezone.utc)
205
+ last_24h = now - timedelta(hours=24)
206
+ session = get_session()
207
+ try:
208
+ # 1. Buscamos a contagem agrupada por tipo de ataque
209
+ # Filtramos para não incluir tráfego legítimo (INFO)
210
+ query = session.query(
211
+ WafLog.attack_type,
212
+ func.count(WafLog.id).label('count')
213
+ ).filter(
214
+ WafLog.timestamp >= last_24h,
215
+ WafLog.attack_type != 'INFO'
216
+ ).group_by(WafLog.attack_type).all()
217
+
218
+ # 2. Calculamos o total de ataques para obter a porcentagem
219
+ total_attacks = sum(row.count for row in query)
220
+
221
+ distribution = []
222
+
223
+ for row in query:
224
+ percentage = round((row.count / total_attacks * 100), 1) if total_attacks > 0 else 0
225
+ distribution.append({
226
+ "label": row.attack_type,
227
+ "count": row.count,
228
+ "percentage": percentage
229
+ })
230
+
231
+ # Caso não haja ataques, retornamos uma lista vazia ou um placeholder
232
+ return distribution
233
+ except Exception as e:
234
+ print(f"Erro no Dashboard: {e}")
235
+ finally:
236
+ session.close()
237
+
238
+ def get_top_geo():
239
+ def resolve_ip(ip):
240
+ try:
241
+ if not os.path.exists(self.geo_db_path):
242
+ print("ERRO: Arquivo GeoLite2-Country.mmdb não encontrado!")
243
+ return "XX", "Unknown"
244
+
245
+ with geoip2.database.Reader(self.geo_db_path) as reader:
246
+ response = reader.country(ip)
247
+ return response.country.iso_code, response.country.name
248
+ except Exception as e:
249
+ print(f"Erro na consulta GeoIP: {e}") # Isso vai te dizer se o banco está corrompido ou o IP é inválido
250
+ return "XX", "Unknown"
251
+
252
+ now = datetime.now(timezone.utc)
253
+ last_24h = now - timedelta(hours=24)
254
+ session = get_session()
255
+ # 1. Busca todos os ataques agrupados por IP
256
+ try:
257
+ query = session.query(
258
+ WafLog.ip,
259
+ func.count(WafLog.id).label('count')
260
+ ).filter(
261
+ WafLog.timestamp >= last_24h,
262
+ WafLog.attack_type != 'INFO'
263
+ ).group_by(WafLog.ip).all()
264
+
265
+ geo_stats = {}
266
+ total_attacks = 0
267
+
268
+ # 2. Processa cada IP real usando o GeoIP2
269
+ for row in query:
270
+ code, name = resolve_ip(row.ip)
271
+
272
+ if code not in geo_stats:
273
+ geo_stats[code] = {"name": name, "count": 0}
274
+
275
+ geo_stats[code]["count"] += row.count
276
+ total_attacks += row.count
277
+
278
+ # 3. Formata o Top 5
279
+ top_geo = []
280
+ sorted_geo = sorted(geo_stats.items(), key=lambda x: x[1]['count'], reverse=True)[:5]
281
+
282
+ for code, data in sorted_geo:
283
+ percentage = round((data["count"] / total_attacks * 100), 1) if total_attacks > 0 else 0
284
+ top_geo.append({
285
+ "country_code": code,
286
+ "country_name": data["name"],
287
+ "count": data["count"],
288
+ "percentage": percentage
289
+ })
290
+
291
+ return top_geo
292
+ except Exception as e:
293
+ print(f"Erro no Dashboard: {e}")
294
+ finally:
295
+ session.close()
296
+
297
+ def get_top_offenders():
298
+ now = datetime.now(timezone.utc)
299
+ last_24h = now - timedelta(hours=24)
300
+ session = get_session()
301
+ # 1. Agrupamos por IP e contamos os hits e os tipos de ataques diferentes
302
+ # Ignoramos tráfego INFO
303
+ try:
304
+ query = session.query(
305
+ WafLog.ip,
306
+ func.count(WafLog.id).label('hits_count'),
307
+ func.count(func.distinct(WafLog.attack_type)).label('unique_vectors')
308
+ ).filter(
309
+ WafLog.timestamp >= last_24h,
310
+ WafLog.attack_type != 'INFO'
311
+ ).group_by(WafLog.ip).order_by(text('hits_count DESC')).limit(5).all()
312
+
313
+ offenders = []
314
+ for row in query:
315
+ # 2. Lógica de Risk Score (0 a 100)
316
+ # Baseada em volume e diversidade de ataques
317
+ # Ex: Cada vetor único vale 20 pontos + 1 ponto para cada 50 hits (até o teto de 100)
318
+ vector_points = row.unique_vectors * 20
319
+ hit_points = row.hits_count // 50
320
+ risk_score = min(100, vector_points + hit_points)
321
+
322
+ offenders.append({
323
+ "ip": row.ip,
324
+ "risk_score": int(risk_score),
325
+ "hits_count": row.hits_count
326
+ })
327
+
328
+ return offenders
329
+ except Exception as e:
330
+ print(f"Erro no Dashboard: {e}")
331
+ finally:
332
+ session.close()
333
+
334
+ try:
335
+ json['meta'] = get_server_info()
336
+ json['kpis'] = get_kpis()
337
+ json['traffic_chart'] = get_traffic_chart()
338
+ json['distribution_vectors'] = get_distribution_vectors()
339
+ json['top_geo'] = get_top_geo()
340
+ json['top_offenders'] = get_top_offenders()
341
+
342
+ return json
343
+
344
+ except Exception as e:
345
+ print(f"Erro no Dashboard: {e}")
346
+ return {"error": str(e)}
347
+
348
+ def seed_default_whitelist():
349
+ """
350
+ Popula a Whitelist com IPs essenciais (Localhost, DNS públicos, etc)
351
+ Executar na inicialização do app.
352
+ """
353
+ # Lista de IPs Padrão (Adicione aqui IPs unitários que confia)
354
+ DEFAULT_IPS = [
355
+ # --- Localhost / Loopback (Essencial) ---
356
+ "127.0.0.1",
357
+ "::1",
358
+
359
+ # Redes Privadas (As gigantes)
360
+ "10.0.0.0/8", # 16 milhões de IPs
361
+ "172.16.0.0/12", # 1 milhão de IPs
362
+ "192.168.0.0/16", # 65 mil IPs
363
+
364
+ # --- DNS Públicos (Google) ---
365
+ "8.8.8.8",
366
+ "8.8.4.4",
367
+
368
+ # --- DNS Públicos (Cloudflare) ---
369
+ "1.1.1.1",
370
+ "1.0.0.1",
371
+
372
+ # --- DNS Públicos (OpenDNS) ---
373
+ "208.67.222.222",
374
+ "208.67.220.220",
375
+
376
+ # --- DNS Públicos (Quad9) ---
377
+ "9.9.9.9",
378
+ "149.112.112.112"
379
+ ]
380
+
381
+ session = get_session()
382
+ try:
383
+ # 1. Descobre o que já existe no banco para não duplicar
384
+ existing_query = session.query(Whitelist.ip).filter(Whitelist.ip.in_(DEFAULT_IPS)).all()
385
+ existing_ips = {row.ip for row in existing_query}
386
+
387
+ # 2. Filtra apenas os novos
388
+ ips_to_insert = set(DEFAULT_IPS) - existing_ips
389
+
390
+ if not ips_to_insert:
391
+ print(" * [WafaHell] Whitelist padrão já está atualizada.")
392
+ return
393
+
394
+ # 3. Prepara Bulk Insert e Cache
395
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
396
+ bulk_data = []
397
+
398
+ for ip in ips_to_insert:
399
+ bulk_data.append({
400
+ "ip": ip,
401
+ "added_at": now_str
402
+ })
403
+ # Já coloca no cache para funcionar imediatamente
404
+ waf_cache.set(f"whitelist_{ip}", True, expire=3600)
405
+
406
+ # 4. Grava no Banco
407
+ session.bulk_insert_mappings(Whitelist, bulk_data)
408
+ session.commit()
409
+
410
+ print(f" * [WafaHell] Seed Whitelist: {len(ips_to_insert)} IPs padrão adicionados.")
411
+
412
+ except Exception as e:
413
+ session.rollback()
414
+ print(f"Erro ao semear whitelist: {e}")
415
+ finally:
416
+ session.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafaHell
3
- Version: 0.2.0
3
+ Version: 1.1.0
4
4
  Summary: Middleware WAF to Flask
5
5
  Author-email: Yago Martins <yagomartins30@gmail.com>
6
6
  License: MIT License
@@ -33,6 +33,10 @@ Requires-Python: >=3.9
33
33
  Description-Content-Type: text/markdown
34
34
  License-File: LICENSE
35
35
  Requires-Dist: Flask>=2.0
36
+ Requires-Dist: sqlalchemy>=2.0.0
37
+ Requires-Dist: requests
38
+ Requires-Dist: diskcache
39
+ Requires-Dist: geoip2
36
40
  Dynamic: license-file
37
41
 
38
42
  # WafHell
@@ -0,0 +1,15 @@
1
+ wafaHell/__init__.py,sha256=bdafr3LRK7_frr-V6umEJZUqt8RMrOiLJy72DSQfs2o,60
2
+ wafaHell/app.py,sha256=oZCqS6rwvm3vU1pc56yYiNc_Mh4DOI52H00qnVovt-o,986
3
+ wafaHell/globals.py,sha256=keIe0YCNsb5si0yUhUjd6nlk1s0YgfH5G7lH5-05Zws,160
4
+ wafaHell/logger.py,sha256=_h4ceGM2UYnEQ5SY7nbYtR0rZprCOC0rN1IehtkkM_o,5734
5
+ wafaHell/middleware.py,sha256=DlSGA-1YHaHG-iQ2lBIWJIuzQnxkRITBPMSQogM8hdA,19625
6
+ wafaHell/mock.py,sha256=EgfSCKYV3rUTerKoEyFQPSf47wrOo-E0yqe7JpQUe3w,2105
7
+ wafaHell/model.py,sha256=o3wrIp8H17vlIB1tW28ApYedXtJLIGTbtrB7zKjzbtE,2919
8
+ wafaHell/panel.py,sha256=mq_vUg4LWLaFEWLvXpuDB3_QUw46WHhw3ruKb6hc_Sc,15926
9
+ wafaHell/rateLimiter.py,sha256=p4IDxha-ZrRPROFf8Fa1gRTbbUTQdbD9TgBTmyMR2hQ,664
10
+ wafaHell/utils.py,sha256=nQk1A4J1OeuvDnX5lj8pIE8IyUF5f5wbtXt0XSdsF7A,16810
11
+ wafahell-1.1.0.dist-info/licenses/LICENSE,sha256=6bv9v4HamenV3rqm3mhaGOecwGFrgxtVTW7JPfFDmeY,1086
12
+ wafahell-1.1.0.dist-info/METADATA,sha256=8fF9VBaal86TMs5EwmntQeKPA7wY9bNbo-9VPvEWBEw,2087
13
+ wafahell-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ wafahell-1.1.0.dist-info/top_level.txt,sha256=VGBo2g3pOeTH2qIXfZDJCSblJgijemMHUHmI0bBgrls,9
15
+ wafahell-1.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
wafaHell/teste.py DELETED
@@ -1,16 +0,0 @@
1
- from middleware import WafaHell
2
- from flask import Flask
3
- import logging
4
-
5
- app = Flask(__name__)
6
- waf = WafaHell(app, monitor_mode=True, block_ip=True, rate_limit=True)
7
-
8
- # log = logging.getLogger('werkzeug')
9
- # log.setLevel(logging.ERROR)
10
-
11
- @app.route('/')
12
- def home():
13
- return "Bem-vindo ao site seguro!"
14
-
15
- if __name__ == '__main__':
16
- app.run(host='0.0.0.0', port=5000, debug=False)
@@ -1,12 +0,0 @@
1
- wafaHell/__init__.py,sha256=bdafr3LRK7_frr-V6umEJZUqt8RMrOiLJy72DSQfs2o,60
2
- wafaHell/logger.py,sha256=yMEz5FY6RdNOzQ9RacAiKpfmwtGjnNVha5umeeSotAM,1354
3
- wafaHell/middleware.py,sha256=nFhfrvoU3yNaS-_yKjAGbP7egdgG9gYwz5pxtFhVmTw,6052
4
- wafaHell/model.py,sha256=QnnOOgsEvqs0tHWLN6SkxQlnWR6r4KYiutyMxQl1nDo,902
5
- wafaHell/rateLimiter.py,sha256=_aU3ZORwpH2CCNpRWF44FyVM5slE3QvLyEgqWRcELr4,666
6
- wafaHell/teste.py,sha256=rnO3lyYQ-d-fJvlX7EQ-qo2T3DkAM6VS_z897Hht77E,399
7
- wafaHell/utils.py,sha256=htvGKGKY4VLsN8fcDOSQKDYqkTXbq8f3X29MQWq08P0,354
8
- wafahell-0.2.0.dist-info/licenses/LICENSE,sha256=6bv9v4HamenV3rqm3mhaGOecwGFrgxtVTW7JPfFDmeY,1086
9
- wafahell-0.2.0.dist-info/METADATA,sha256=v5yAr069OcRN4c3BxogmtQEAA-KKWr96a9ujgjuzbAc,1979
10
- wafahell-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- wafahell-0.2.0.dist-info/top_level.txt,sha256=VGBo2g3pOeTH2qIXfZDJCSblJgijemMHUHmI0bBgrls,9
12
- wafahell-0.2.0.dist-info/RECORD,,