wafaHell 0.1.1__py3-none-any.whl → 1.0.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/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from .middleware import WafaHell
2
2
 
3
- # agora a classe está disponível diretamente ao importar o pacote
3
+ __all__ = ["WafaHell"]
wafaHell/app.py ADDED
@@ -0,0 +1,31 @@
1
+ from middleware import WafaHell
2
+ from flask import Flask, render_template, request
3
+ from werkzeug.middleware.proxy_fix import ProxyFix
4
+
5
+ app = Flask(__name__)
6
+
7
+
8
+ app.wsgi_app = ProxyFix(
9
+ app.wsgi_app,
10
+ x_for=1,
11
+ x_proto=1,
12
+ x_host=1,
13
+ x_port=1
14
+ )
15
+
16
+ @app.route('/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
17
+ def home():
18
+ return "<h1>Bem-vindo à minha aplicação Flask!</h1><p>Acesse /hello?nome=test</p>"
19
+
20
+ @app.route('/hello', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
21
+ def hello():
22
+ nome = request.args.get('nome', 'Visitante')
23
+ return f"<h1>Olá, {nome}!</h1>"
24
+
25
+ @app.route('/admin/dashboard')
26
+ def dashboard():
27
+ return "<h1>Dashboard Personalizado</h1><p>Este é o painel de controle personalizado.</p>"
28
+
29
+ if __name__ == '__main__':
30
+ WafaHell(app, dashboard_path='/hell/dashboard', block_durantion=1, rate_limit=True, block_ip=True)
31
+ app.run(debug=True, host='0.0.0.0', port=5000)
wafaHell/globals.py ADDED
@@ -0,0 +1,5 @@
1
+ import os
2
+ from diskcache import Cache
3
+
4
+ cache_path = '/dev/shm/waf_cache' if os.path.exists('/dev/shm') else './waf_cache_temp'
5
+ waf_cache = Cache(cache_path)
wafaHell/logger.py ADDED
@@ -0,0 +1,132 @@
1
+ from datetime import datetime
2
+ import logging
3
+ from model import WafLog, get_session
4
+ import re
5
+ # Handler customizado para salvar no SQLite via SQLAlchemy
6
+ class SQLAlchemyHandler(logging.Handler):
7
+ def __init__(self):
8
+ super().__init__()
9
+ self.session = get_session()
10
+ # Regex para extrair dados da string formatada pelo parse_req
11
+ self.attr_pattern = re.compile(
12
+ r"Attack_type: (?P<type>.*?), IP: (?P<ip>.*?), .*?Path: (?P<path>.*?), Method: (?P<method>.*?), Payload: (?P<payload>.*?), attack_local: (?P<local>.*)"
13
+ )
14
+
15
+ def emit(self, record):
16
+ session = self.session # Certifique-se de instanciar a sessão
17
+ try:
18
+ msg = record.getMessage()
19
+
20
+ # Valores padrão
21
+ ip, path, method, payload, local, attack_type = (None, None, None, msg, None, "Info")
22
+
23
+ # Expandimos a condição para incluir [BLOCKED]
24
+ if any(tag in msg for tag in ["[ATTACK]", "[RATE LIMIT]", "[BLOCKED]"]):
25
+
26
+ def extract(key, text):
27
+ # Regex ajustada para capturar até a vírgula ou fim da tag, permitindo espaços internos
28
+ match = re.search(rf"{key}:?\s*([^,\]]+)", text)
29
+ return match.group(1).strip() if match else None
30
+
31
+ ip = extract("IP", msg)
32
+
33
+ if "[BLOCKED]" in msg:
34
+ attack_type = "IP BLOCK"
35
+ ua = extract("UA", msg)
36
+ payload = f"IP Bloqueado. User-Agent: {ua}" if ua else "IP Bloqueado"
37
+ path = "---"
38
+ method = "---"
39
+ elif "[RATE LIMIT]" in msg:
40
+ attack_type = "RATE LIMIT"
41
+ payload = "Exceeded request limit"
42
+ path = extract("Path", msg)
43
+ method = extract("Method", msg)
44
+
45
+ bucket = datetime.utcnow().strftime('%Y-%m-%d %H:%M')
46
+ log_entry = WafLog(
47
+ level=record.levelname,
48
+ attack_type=attack_type,
49
+ ip=ip,
50
+ path=path,
51
+ method=method,
52
+ payload=payload,
53
+ attack_local=local,
54
+ time_bucket=bucket
55
+ )
56
+
57
+ session.add(log_entry)
58
+ session.commit()
59
+ else:
60
+ # Lógica para [ATTACK]
61
+ attack_type = extract("Attack_type", msg) or "Unknown"
62
+ path = extract("Path", msg)
63
+ method = extract("Method", msg)
64
+
65
+ # Captura o local completo (ex: HEADER 'User-Agent')
66
+ local_match = re.search(r"attack_local:\s*(.+)$", msg)
67
+ local = local_match.group(1).strip() if local_match else extract("attack_local", msg)
68
+
69
+ # Regex específica para payloads que podem conter vírgulas
70
+ payload_match = re.search(r"Payload: (.*?), attack_local:", msg)
71
+ payload = payload_match.group(1) if payload_match else extract("Payload", msg)
72
+
73
+ if not "[RATE LIMIT]" in msg:
74
+ log_entry = WafLog(
75
+ level=record.levelname,
76
+ attack_type=attack_type,
77
+ ip=ip,
78
+ path=path,
79
+ method=method,
80
+ payload=payload,
81
+ attack_local=local,
82
+ # time_bucket fica None aqui para não filtrar ataques normais no banco
83
+ )
84
+
85
+ session.add(log_entry)
86
+ session.commit()
87
+
88
+ except Exception as e:
89
+ session.rollback()
90
+ finally:
91
+ session.close()
92
+
93
+ class Logger:
94
+ def __init__(self, name="WAF", log_file="waf.log", level=logging.INFO):
95
+ self.logger = logging.getLogger(name)
96
+ self.logger.setLevel(level)
97
+
98
+ if not self.logger.handlers:
99
+ formatter = logging.Formatter(
100
+ "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s",
101
+ datefmt="%H:%M:%S - %d/%m/%Y"
102
+ )
103
+
104
+ # Handler 1: Console
105
+ console_handler = logging.StreamHandler()
106
+ console_handler.setFormatter(formatter)
107
+ self.logger.addHandler(console_handler)
108
+
109
+ # Handler 2: Arquivo
110
+ file_handler = logging.FileHandler(log_file)
111
+ file_handler.setFormatter(formatter)
112
+ self.logger.addHandler(file_handler)
113
+
114
+ # Handler 3: Banco de Dados (A Mágica acontece aqui)
115
+ db_handler = SQLAlchemyHandler()
116
+ db_handler.setLevel(logging.INFO) # Salva INFO, WARNING e acima no DB
117
+ self.logger.addHandler(db_handler)
118
+
119
+ def info(self, msg):
120
+ self.logger.info(msg)
121
+
122
+ def warning(self, msg):
123
+ self.logger.warning(msg)
124
+
125
+ def error(self, msg):
126
+ self.logger.error(msg)
127
+
128
+ def critical(self, msg):
129
+ self.logger.critical(msg)
130
+
131
+ def debug(self, msg):
132
+ self.logger.debug(msg)
wafaHell/middleware.py CHANGED
@@ -1,65 +1,351 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ import os
1
3
  import re
2
- from flask import request, abort
4
+ import subprocess
5
+ import time
6
+ from flask import request as req, abort, g
3
7
  from urllib.parse import unquote
8
+ from model import Base, Blocked, WafLog, get_session, engine
9
+ from logger import Logger
10
+ from rateLimiter import RateLimiter
11
+ from sqlalchemy.exc import OperationalError
12
+ from panel import setup_dashboard
13
+ from utils import Admin
14
+ from sqlalchemy import text
15
+ from globals import waf_cache
16
+
17
+ # Inicializa o RateLimiter
18
+ limiter = RateLimiter(limit=100, window=60)
4
19
 
5
20
  class WafaHell:
6
- def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False):
21
+ def __init__(self, app=None, block_code=403, block_durantion=5, block_ip=False, log_func=None, monitor_mode=False, rate_limit=False, dashboard_path=None):
7
22
  self.app = app
8
23
  self.block_code = block_code
9
- self.log_func = log_func or (lambda msg: print(f"[WAF] {msg}"))
24
+ self.log = log_func or Logger()
10
25
  self.monitor_mode = monitor_mode
11
- # Regras básicas
12
- self.rules = [
26
+ self.block_ip = block_ip
27
+ self.rate_limit = rate_limit
28
+ self.dashboard_path = dashboard_path
29
+ self.block_durantion = block_durantion
30
+ self.recent_blocks_cache = {}
31
+
32
+ self.rules_sqli = [
13
33
  r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
14
- r"' OR '1'='1",
34
+ r"' OR '1'='1"
35
+ ]
36
+ self.rules_xss = [
15
37
  r"<script.*?>.*?</script>",
16
- r"javascript:",
38
+ r"javascript:"
17
39
  ]
18
40
 
19
41
  if app is not None:
20
42
  self.init_app(app)
21
43
 
22
44
  def init_app(self, app):
45
+ Base.metadata.create_all(engine)
46
+ setup_dashboard(app, self.dashboard_path)
47
+ Admin.create_admin_user(get_session())
48
+
49
+ if not app.secret_key:
50
+ app.secret_key = os.urandom(24)
51
+
52
+ @app.before_request
53
+ def create_session():
54
+ try:
55
+ req.session = get_session()
56
+ except Exception as e:
57
+ self.log.error(f"Erro ao criar sessão para requisição: {e}")
58
+ abort(self.block_code)
59
+
60
+ @app.teardown_request
61
+ def close_session(exc=None):
62
+ if hasattr(req, 'session'):
63
+ try:
64
+ req.session.close()
65
+ except Exception as e:
66
+ self.log.error(f"Erro ao fechar sessão: {e}")
67
+
68
+
23
69
  @app.before_request
24
70
  def waf_check():
25
- if self.is_malicious(request):
26
- self.log_attack(request)
27
- if not self.monitor_mode:
28
- abort(self.block_code)
71
+ self.verify_client_blocked(req)
72
+ self.verify_rate_limit(req)
73
+ is_malicious, attack_local, payload, attack_type = self.is_malicious(req)
74
+
75
+ if not is_malicious:
76
+ return
77
+
78
+ if not self.monitor_mode:
79
+ self.log.warning(self.parse_req(req, payload, attack_local, attack_type))
80
+ self.block_ip_address(req.remote_addr, req.headers.get("User-Agent", "unknown"))
81
+ abort(self.block_code)
82
+ else:
83
+ self.log.info(self.parse_req(req, payload, attack_local, attack_type))
84
+
85
+ @app.before_request
86
+ def start_timer():
87
+ g.waf_start_time = time.time()
88
+
89
+ @app.after_request
90
+ def stop_timer(response):
91
+ ignored_paths = [self.dashboard_path, f'{self.dashboard_path}/stats', '/static']
92
+
93
+ # 1. Ignora rotas do próprio painel para não sujar os logs e métricas
94
+ if any(req.path.startswith(path) for path in ignored_paths):
95
+ return response
96
+
97
+ # 2. LOG DE TRÁFEGO LEGÍTIMO
98
+ # Se o status_code for menor que 400, significa que o WAF não deu abort()
99
+ # e a requisição seguiu o fluxo normal.
100
+ if response.status_code != self.block_code:
101
+ self.log_legit_access(req)
102
+
103
+ # 3. RPS Logic (Bucketing by second)
104
+ current_timestamp = int(time.time())
105
+ rps_key = f"rps_{current_timestamp}"
106
+ waf_cache.incr(rps_key, default=0)
107
+ waf_cache.expire(rps_key, 10)
108
+
109
+ # 4. Latency Logic
110
+ if hasattr(g, 'waf_start_time'):
111
+ latency = (time.time() - g.waf_start_time) * 1000
112
+
113
+ # Exponential Moving Average
114
+ old_avg = waf_cache.get('latency_avg', default=0.0)
115
+ new_avg = (old_avg * 0.95) + (latency * 0.05) if old_avg > 0 else latency
116
+
117
+ waf_cache.set('latency_avg', new_avg, expire=3600)
118
+
119
+ return response
120
+
121
+ def log_legit_access(self, req):
122
+ session = get_session()
123
+ try:
124
+ new_log = WafLog(
125
+ timestamp=datetime.now(timezone.utc),
126
+ attack_type='INFO', # Identificador de tráfego limpo
127
+ ip=req.remote_addr,
128
+ path=req.path,
129
+ method=req.method,
130
+ level='INFO'
131
+ )
132
+ session.add(new_log)
133
+ session.commit()
134
+ except Exception as e:
135
+ session.rollback()
136
+ self.log.error(f"Error logging legit access: {e}")
137
+ finally:
138
+ session.close()
29
139
 
30
140
  def detect_attack(self, data: str) -> bool:
31
- for pattern in self.rules:
141
+ for pattern in self.rules_xss:
142
+ if re.search(pattern, data, re.IGNORECASE):
143
+ return "XSS"
144
+ for pattern in self.rules_sqli:
32
145
  if re.search(pattern, data, re.IGNORECASE):
33
- return True
34
- return False
146
+ return "SQLI"
147
+ return None
35
148
 
36
- def is_malicious(self, req) -> bool:
37
- # URL + Query Params
38
- if self.detect_attack(req.url) or any(self.detect_attack(v) for v in req.args.values()):
39
- return True
149
+ def is_malicious(self, req) -> tuple:
150
+ attack = self.detect_attack(req.base_url)
151
+ if attack:
152
+ print(f"[DEBUG] Attack detected in URL: {attack}")
153
+ return True, "URL", req.base_url, attack
154
+
155
+ for key, value in req.form.items():
156
+ attack = self.detect_attack(value)
157
+ if attack:
158
+ print(f"[DEBUG] Attack detected in FORM '{key}': {attack}")
159
+ return True, f"FORM '{key}'", value, attack
160
+
161
+ for key, value in req.args.items():
162
+ attack = self.detect_attack(value)
163
+ if attack:
164
+ print(f"[DEBUG] Attack detected in QUERY '{key}': {attack}")
165
+ return True, f"QUERY '{key}'", value, attack
40
166
 
41
- # Headers
42
- for _, value in req.headers.items():
43
- if self.detect_attack(value):
44
- return True
167
+ for key, value in req.headers.items():
168
+ attack = self.detect_attack(value)
169
+ if attack:
170
+ print(f"[DEBUG] Attack detected in HEADER '{key}': {attack}")
171
+ return True, f"HEADER '{key}'", value, attack
45
172
 
46
- # Body
47
- if req.data and self.detect_attack(req.data.decode(errors="ignore")):
48
- return True
173
+ if req.data:
174
+ body_content = req.data.decode(errors="ignore")
175
+ attack = self.detect_attack(body_content)
176
+ if attack:
177
+ print(f"[DEBUG] Attack detected in BODY: {attack}")
178
+ return True, "BODY", body_content, attack
179
+
180
+ if req.is_json:
181
+ json_data = req.get_json(silent=True)
182
+ if json_data:
183
+ import json
184
+ json_str = json.dumps(json_data)
185
+ attack = self.detect_attack(json_str)
186
+ if attack:
187
+ print(f"[DEBUG] Attack detected in JSON BODY: {attack}")
188
+ return True, "JSON BODY", json_str, attack
49
189
 
50
- return False
190
+ return False, None, None, None
51
191
 
52
- def log_attack(self, req):
192
+ def verify_client_blocked(self, req) -> None:
193
+ session = req.session
194
+ try:
195
+ client_blocked = session.query(Blocked).filter_by(
196
+ ip=req.remote_addr,
197
+ ).first()
198
+
199
+ if client_blocked:
200
+
201
+ now = datetime.now(timezone.utc) if client_blocked.blocked_until.tzinfo else datetime.utcnow()
202
+
203
+ if client_blocked.blocked_until <= now:
204
+ # --- TRAVA DE DESBLOQUEIO (Anti-Race Condition) ---
205
+ # Usamos um marcador no cache para saber se alguém já está desbloqueando este IP
206
+ cache_key = f"unblocking_{req.remote_addr}"
207
+ if cache_key in self.recent_blocks_cache:
208
+ return # Outra thread já está limpando este IP, apenas saia
209
+
210
+ self.recent_blocks_cache[cache_key] = True
211
+ # --------------------------------------------------
212
+
213
+ try:
214
+ session.delete(client_blocked)
215
+ session.commit()
216
+
217
+ # Limpa os caches de controle deste IP
218
+ self.recent_blocks_cache.pop(req.remote_addr, None)
219
+ self.recent_blocks_cache.pop(cache_key, None)
220
+
221
+ self.log.info(f"[UNBLOCKED] IP {req.remote_addr} bloqueio expirou.")
222
+ except Exception as e:
223
+ session.rollback()
224
+ self.recent_blocks_cache.pop(cache_key, None)
225
+ raise e
226
+ return
227
+
228
+ # Se chegou aqui, ainda está bloqueado
229
+ abort(self.block_code)
230
+
231
+ except OperationalError:
232
+ session.rollback()
233
+ abort(self.block_code)
234
+
235
+ try:
236
+
237
+ client_blocked = session.query(Blocked).filter_by(
238
+ ip=req.remote_addr,
239
+ user_agent=req.headers.get("User-Agent")
240
+ ).first()
241
+
242
+ if not client_blocked:
243
+ return
244
+
245
+ # Normaliza o tempo para comparação
246
+ now = datetime.now(timezone.utc) if client_blocked.blocked_until.tzinfo else datetime.utcnow()
247
+
248
+ if client_blocked.blocked_until <= now:
249
+ # --- TRAVA DE DESBLOQUEIO (Anti-Race Condition) ---
250
+ # Usamos um marcador no cache para saber se alguém já está desbloqueando este IP
251
+ cache_key = f"unblocking_{req.remote_addr}"
252
+ if cache_key in self.recent_blocks_cache:
253
+ return # Outra thread já está limpando este IP, apenas saia
254
+
255
+ self.recent_blocks_cache[cache_key] = True
256
+ # --------------------------------------------------
257
+
258
+ try:
259
+ session.delete(client_blocked)
260
+ session.commit()
261
+
262
+ # Limpa os caches de controle deste IP
263
+ self.recent_blocks_cache.pop(req.remote_addr, None)
264
+ self.recent_blocks_cache.pop(cache_key, None)
265
+
266
+ self.log.info(f"[UNBLOCKED] IP {req.remote_addr} bloqueio expirou.")
267
+ except Exception as e:
268
+ session.rollback()
269
+ self.recent_blocks_cache.pop(cache_key, None)
270
+ raise e
271
+ return
272
+
273
+ abort(self.block_code)
274
+
275
+ except OperationalError:
276
+ session.rollback()
277
+ abort(self.block_code)
278
+
279
+ def block_ip_address(self, ip, user_agent=None):
280
+ if not self.block_ip:
281
+ return
282
+
283
+ # 1. Trava de Memória (ajuda, mas não resolve 100% em multi-processo)
284
+ now_ts = datetime.now().timestamp()
285
+ if ip in self.recent_blocks_cache:
286
+ if now_ts - self.recent_blocks_cache[ip] < 5:
287
+ return
288
+ self.recent_blocks_cache[ip] = now_ts
289
+
290
+ session = get_session()
291
+ try:
292
+ # 2. TRAVA DE BANCO: Verifica se já houve um log desse IP nos últimos 2 segundos
293
+ # Isso evita que as 6 threads do ffuf que passaram pela trava de memória gravem no banco
294
+
295
+ time_threshold = datetime.now(timezone.utc) - timedelta(seconds=2)
296
+
297
+ # Buscamos na tabela de LOGS (WafLog) se já existe um registro recente
298
+
299
+ exists_recent_log = session.query(WafLog).filter(
300
+ WafLog.ip == ip,
301
+ WafLog.attack_type.in_(['RATE LIMIT', 'IP BLOCK']),
302
+ WafLog.timestamp >= time_threshold
303
+ ).first()
304
+
305
+ if not exists_recent_log:
306
+ # Só prossegue se não houver log recente
307
+ exists_block = session.query(Blocked).filter_by(ip=ip).first()
308
+ if not exists_block:
309
+ now = datetime.now(timezone.utc)
310
+ until = now + timedelta(minutes=self.block_durantion)
311
+
312
+ new_block = Blocked(
313
+ ip=ip, user_agent=user_agent,
314
+ blocked_at=now, blocked_until=until
315
+ )
316
+ session.add(new_block)
317
+ session.commit()
318
+
319
+
320
+
321
+ except Exception as e:
322
+ session.rollback()
323
+ self.log.error(f"Erro ao persistir bloqueio: {e}")
324
+ finally:
325
+ session.close()
326
+
327
+ def verify_rate_limit(self, req) -> None:
328
+ if self.rate_limit:
329
+ ip = req.remote_addr
330
+ ua = req.headers.get("User-Agent", "unknown")
331
+
332
+ if limiter.is_rate_limited(ip, ua):
333
+ if self.monitor_mode:
334
+ return
335
+
336
+
337
+ if self.block_ip:
338
+ self.block_ip_address(ip, ua)
339
+ self.log.warning(f"[RATE LIMIT] IP: {ip} exceeded limit.")
340
+ self.log.warning(f"[BLOCKED] IP: {ip}, UA: {ua}")
341
+ abort(self.block_code)
342
+
343
+ self.log.warning(f"[RATE LIMIT] IP: {ip} exceeded limit.")
344
+
345
+ def parse_req(self, req, payload, attack_local=None, attack_type=None) -> str:
53
346
  ip = req.remote_addr
54
347
  user_agent = req.headers.get("User-Agent", "unknown")
55
348
  path = req.path
56
- query = req.query_string.decode(errors="ignore") if req.query_string else ""
57
-
58
- msg = (
59
- f"Ataque detectado\n",
60
- f"IP: {ip}\n"
61
- f"User-Agent: {user_agent}\n"
62
- f"Rota: {path}\n"
63
- f"Query: {unquote(query)}\n"
64
- )
65
- self.log_func(msg)
349
+ method = req.method
350
+ attack_local = attack_local or "unknown"
351
+ return f"[ATTACK] Attack_type: {attack_type}, IP: {ip}, User-Agent: {user_agent}, Path: {path}, Method: {method}, Payload: {unquote(payload)}, attack_local: {attack_local}"
wafaHell/mock.py ADDED
@@ -0,0 +1,53 @@
1
+ import requests
2
+
3
+ # Configuração do alvo
4
+ BASE_URL = "http://127.0.0.1:5000/hello"
5
+
6
+ # Definição dos payloads de teste (SQLi e XSS clássicos)
7
+ payloads = {
8
+ "URL Query": {"url": f"{BASE_URL}?id=1' OR '1'='1"},
9
+ "Form Data": {"method": "POST", "data": {"user": "<script>alert(1)</script>"}},
10
+ "JSON Body": {"method": "POST", "json": {"search": "SELECT * FROM users"}},
11
+ "Cookies": {"method": "POST", "cookies": {"session": "UNION SELECT NULL,NULL--"}},
12
+ "Custom Header": {"method": "POST", "headers": {"User-Agent": "'; DROP TABLE logs;--"}},
13
+ "Multipart File": {"method": "POST", "files": {"file": ("README.md", "payload: <script>alert('XSS')</script>")}},
14
+ }
15
+
16
+ def run_mock():
17
+ print(f"🚀 Iniciando Mock de Testes WafaHell no alvo: {BASE_URL}\n")
18
+ print(f"{'VETOR DE TESTE':<20} | {'STATUS':<10} | {'RESULTADO'}")
19
+ print("-" * 55)
20
+
21
+ for test_name, config in payloads.items():
22
+ try:
23
+ # Prepara a requisição
24
+ url = config.get("url", BASE_URL)
25
+ method = config.get("method", "GET")
26
+
27
+ # Executa a requisição com os parâmetros dinâmicos
28
+ response = requests.request(
29
+ method=method,
30
+ url=url,
31
+ data=config.get("data"),
32
+ json=config.get("json"),
33
+ cookies=config.get("cookies"),
34
+ headers=config.get("headers"),
35
+ files=config.get("files"),
36
+ timeout=5
37
+ )
38
+
39
+ # Validação: 403 significa que o WAF bloqueou (Sucesso no teste)
40
+ if response.status_code == 403:
41
+ status_txt = "✅ BLOQUEADO"
42
+ result_txt = "Sucesso (WAF Ativo)"
43
+ else:
44
+ status_txt = f"❌ {response.status_code}"
45
+ result_txt = "Falha (Vulnerável!)"
46
+
47
+ print(f"{test_name:<20} | {status_txt:<10} | {result_txt}")
48
+
49
+ except Exception as e:
50
+ print(f"{test_name:<20} | ⚠️ ERRO | {str(e)}")
51
+
52
+ if __name__ == "__main__":
53
+ run_mock()
wafaHell/model.py ADDED
@@ -0,0 +1,100 @@
1
+ from datetime import datetime
2
+ from sqlalchemy import (
3
+ DateTime, Text, create_engine,
4
+ Column, Integer, String, UniqueConstraint
5
+ )
6
+ from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
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
+
28
+ Base = declarative_base()
29
+
30
+ # =========================
31
+ # MODELS
32
+ # =========================
33
+
34
+ class Blocked(Base):
35
+ __tablename__ = "blocks"
36
+
37
+ id = Column(Integer, primary_key=True, autoincrement=True)
38
+ ip = Column(String, nullable=True)
39
+ user_agent = Column(String, nullable=True)
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)
46
+
47
+ def __repr__(self):
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
+
56
+ class WafLog(Base):
57
+ __tablename__ = "waf_logs"
58
+
59
+ id = Column(Integer, primary_key=True)
60
+ timestamp = Column(DateTime, default=datetime.utcnow)
61
+
62
+ time_bucket = Column(String(20), index=True)
63
+
64
+ attack_type = Column(String(50))
65
+ ip = Column(String(50))
66
+ path = Column(String(200))
67
+ method = Column(String(10))
68
+ payload = Column(Text)
69
+ attack_local = Column(String(50))
70
+ level = Column(String(20))
71
+
72
+ __table_args__ = (
73
+ UniqueConstraint('ip', 'attack_type', 'time_bucket', name='uix_ip_attack_time'),
74
+ )
75
+
76
+
77
+ class AdminUser(Base):
78
+ __tablename__ = "admin_user"
79
+
80
+ id = Column(Integer, primary_key=True, autoincrement=True)
81
+ login = Column(String(50), nullable=False, unique=True)
82
+ password = Column(String(255), nullable=False)
83
+
84
+ def __repr__(self):
85
+ return f"<AdminUser(login='{self.login}')>"
86
+
87
+
88
+ # =========================
89
+ # DB INIT / SESSION
90
+ # =========================
91
+
92
+ def init_db():
93
+ """Cria as tabelas uma única vez"""
94
+ Base.metadata.create_all(bind=engine)
95
+
96
+
97
+ def get_session():
98
+ """Retorna uma sessão reutilizável"""
99
+ return SessionLocal()
100
+
wafaHell/panel.py ADDED
@@ -0,0 +1,192 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from flask import render_template_string, request, jsonify, make_response, flash, session, redirect, render_template
3
+ from model import AdminUser, WafLog, get_session, Blocked
4
+ from sqlalchemy import func
5
+ import csv
6
+ import io
7
+ from utils import Admin, Dashboard, admin
8
+ from werkzeug.security import check_password_hash
9
+ dashboard = Dashboard()
10
+
11
+ def get_logs_and_stats(ip_filter=None, type_filter=None, limit=100):
12
+ session = get_session()
13
+ try:
14
+ log_query = session.query(WafLog).order_by(WafLog.id.desc())
15
+ stat_query = session.query(func.count(WafLog.id))
16
+
17
+ if ip_filter:
18
+ log_query = log_query.filter(WafLog.ip == ip_filter)
19
+ stat_query = stat_query.filter(WafLog.ip == ip_filter)
20
+ if type_filter:
21
+ log_query = log_query.filter(WafLog.attack_type == type_filter)
22
+ stat_query = stat_query.filter(WafLog.attack_type == type_filter)
23
+
24
+ db_logs = log_query.limit(limit).all()
25
+
26
+ # Estatísticas Dinâmicas
27
+ stats = {
28
+ "total": stat_query.scalar() or 0,
29
+ # Agora attacks conta tudo que não é 'Info' ou 'System'
30
+ "attacks": stat_query.filter(WafLog.attack_type.in_(['SQLI', 'XSS', 'RATE LIMIT'])).scalar() or 0,
31
+ "sqli": stat_query.filter(WafLog.attack_type == 'SQLI').scalar() or 0,
32
+ "xss": stat_query.filter(WafLog.attack_type == 'XSS').scalar() or 0,
33
+ "rate_limit": stat_query.filter(WafLog.attack_type == 'RATE LIMIT').scalar() or 0,
34
+ "blocks": stat_query.filter(WafLog.attack_type == 'IP BLOCK').scalar() or 0
35
+ }
36
+
37
+ formatted_logs = []
38
+ for log in db_logs:
39
+ # Determinamos o "Tipo" visual para o HTML
40
+ display_type = "INFO"
41
+ if log.attack_type in ['SQLI', 'XSS', 'RATE LIMIT']:
42
+ display_type = "ATTACK"
43
+ elif log.attack_type == 'IP BLOCK':
44
+ display_type = "BLOCKED" # Mudança aqui para o dashboard
45
+
46
+ formatted_logs.append({
47
+ "timestamp": log.timestamp.strftime("%H:%M:%S - %d/%m/%Y"),
48
+ "type": display_type,
49
+ "details": {
50
+ "attack_type": log.attack_type,
51
+ "ip": log.ip or "---",
52
+ "path": log.path or "---",
53
+ "method": log.method or "---",
54
+ "payload": log.payload or "---",
55
+ "attack_local": log.attack_local or "---"
56
+ }
57
+ })
58
+ return formatted_logs, stats
59
+ finally:
60
+ session.close()
61
+
62
+ def setup_dashboard(app, custom_path=None):
63
+ target_path = custom_path or '/admin/dashboard'
64
+
65
+ @app.route(target_path + "/login", methods=["GET", "POST"])
66
+ def login():
67
+ if request.method == "POST":
68
+ username = request.form.get("user")
69
+ password = request.form.get("password")
70
+ db = get_session()
71
+ try:
72
+ admin = db.query(AdminUser).filter(AdminUser.login == username).first()
73
+
74
+ # 3. Valida (Aqui você deveria usar hash, mas vamos focar na lógica)
75
+ if admin and check_password_hash(admin.password, password):
76
+ session["logged_in"] = True
77
+ next_page = request.args.get("next", target_path)
78
+ return redirect(next_page)
79
+ else:
80
+ flash("Acesso Negado: Credenciais Inválidas", "error")
81
+ return redirect(request.url)
82
+ finally:
83
+ db.close()
84
+
85
+ return render_template("login.html")
86
+
87
+ @app.route(target_path + '/data')
88
+ @admin
89
+ def wafahell_data():
90
+ ip_f = request.args.get('ip')
91
+ type_f = request.args.get('type')
92
+ logs, stats = get_logs_and_stats(ip_filter=ip_f, type_filter=type_f)
93
+ return jsonify({"logs": logs, "stats": stats})
94
+
95
+ # Rota 2: Retorna o HTML inicial
96
+ @app.route(target_path)
97
+ @admin
98
+ def wafahell_dashboard():
99
+ ip_f = request.args.get('ip')
100
+ type_f = request.args.get('type')
101
+ logs, stats = get_logs_and_stats(ip_filter=ip_f, type_filter=type_f)
102
+ return render_template('dashboard.html', logs=logs, stats=stats, filters={'ip': ip_f, 'type': type_f})
103
+
104
+ @app.route(target_path + '/export/csv')
105
+ @admin
106
+ def export_csv():
107
+
108
+ ip_filter = request.args.get('ip')
109
+ type_filter = request.args.get('type')
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)
118
+
119
+ logs = query.order_by(WafLog.timestamp.desc()).all()
120
+ session.close()
121
+
122
+ # Criamos o CSV em memória
123
+ output = io.StringIO()
124
+ writer = csv.writer(output)
125
+
126
+ # Cabeçalho
127
+ writer.writerow(['Data/Hora', 'Tipo', 'IP', 'Endpoint', 'Metodo', 'Payload'])
128
+
129
+ for log in logs:
130
+ writer.writerow([
131
+ log.timestamp,
132
+ log.attack_type,
133
+ log.ip,
134
+ log.path,
135
+ log.method,
136
+ log.payload
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
143
+
144
+ @app.route(target_path + '/block_ip', methods=['POST'])
145
+ @admin
146
+ def block_ip() -> bool:
147
+ data = request.get_json()
148
+ ip = data.get('ip')
149
+ block_time = data.get('block_time_minutes', 5) # Pega do JSON ou assume 5
150
+
151
+ if not ip:
152
+ return jsonify({"status": "error", "message": "IP ausente"}), 400
153
+
154
+ session = get_session()
155
+
156
+ try:
157
+ exists = session.query(Blocked).filter_by(ip=ip, user_agent="MANUAL_BLOCK").first()
158
+ if exists:
159
+ exists.blocked_until = datetime.now(timezone.utc) + timedelta(minutes=int(block_time))
160
+ session.commit()
161
+ return jsonify({"status": "success", "message": "Tempo de bloqueio atualizado"})
162
+
163
+ now = datetime.now(timezone.utc)
164
+ until = now + timedelta(minutes=int(block_time))
165
+
166
+ new_block = Blocked(
167
+ ip=ip,
168
+ user_agent="MANUAL_BLOCK",
169
+ blocked_until=until
170
+ )
171
+
172
+ session.add(new_block)
173
+ session.commit()
174
+ return jsonify({"status": "success", "message": "IP bloqueado com sucesso"})
175
+ except Exception as e:
176
+ session.rollback()
177
+ return jsonify({"status": "error", "message": str(e)}), 500
178
+ finally:
179
+ session.close()
180
+
181
+ @app.route(target_path + '/graphs', methods=['GET'])
182
+ @admin
183
+ def graphs():
184
+ return render_template('graphs.html')
185
+
186
+ @app.route(target_path + '/stats', methods=['GET'])
187
+ @admin
188
+ def api_stats():
189
+ return jsonify(dashboard.dashboard_setup())
190
+
191
+ print(f" * [WafaHell] Dashboard e API de dados prontos em: {target_path}")
192
+
@@ -0,0 +1,20 @@
1
+
2
+ from collections import defaultdict, deque
3
+ from datetime import datetime, timedelta
4
+
5
+ class RateLimiter:
6
+ def __init__(self, limit=100, window=60):
7
+ self.limit = limit
8
+ self.window = window
9
+ self.requests_log = defaultdict(lambda: deque())
10
+
11
+ def is_rate_limited(self, ip, ua):
12
+ key = (ip, ua)
13
+ now = datetime.now()
14
+ window_start = now - timedelta(seconds=self.window)
15
+
16
+ while self.requests_log[key] and self.requests_log[key][0] < window_start:
17
+ self.requests_log[key].popleft()
18
+
19
+ self.requests_log[key].append(now)
20
+ return len(self.requests_log[key]) >= self.limit
wafaHell/utils.py ADDED
@@ -0,0 +1,312 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ import secrets
3
+ import string
4
+ import time
5
+ from werkzeug.security import generate_password_hash
6
+ from sqlalchemy.orm import Session
7
+ from sqlalchemy import text, func, case
8
+ from model import WafLog, Blocked
9
+ from model import AdminUser
10
+ from functools import wraps
11
+ from flask import session, redirect, url_for, request
12
+ from model import get_session
13
+ from globals import waf_cache
14
+ import geoip2.database
15
+ import os
16
+
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))
22
+
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)
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
+ print(f"DEBUG: Session logged_in status: {session.get('logged_in')}") # Adicione isso
47
+ if not session.get("logged_in"):
48
+ print("DEBUG: Redirecting to login...")
49
+ return redirect(url_for("login", next=request.path))
50
+ return fn(*args, **kwargs)
51
+ return wrapper
52
+
53
+ class Dashboard:
54
+ def __init__(self):
55
+ self.db_session = get_session()
56
+ self.geo_db_path = os.path.join(os.path.dirname(__file__), 'GeoLite2-Country.mmdb')
57
+
58
+ def dashboard_setup(self):
59
+ json = {}
60
+ def get_server_info():
61
+ server_time = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
62
+ node_id = "WAF-01"
63
+ avg_latency = waf_cache.get('latency_avg', default=0.0)
64
+ system_status = "critical" if avg_latency > 500 else "degraded" if avg_latency > 200 else "healthy"
65
+ return {
66
+ "server_time": server_time,
67
+ "node_id": node_id,
68
+ "average_latency_ms": round(float(avg_latency), 2),
69
+ "system_status": system_status
70
+ }
71
+
72
+ def get_kpis():
73
+ now = datetime.now(timezone.utc)
74
+ last_24h = now - timedelta(hours=24)
75
+ prev_24h = now - timedelta(hours=48)
76
+
77
+ # --- TOTAIS DE HOJE ---
78
+ total_today = self.db_session.query(func.count(WafLog.id)).filter(WafLog.timestamp >= last_24h).scalar() or 0
79
+ blocked_today = self.db_session.query(func.count(WafLog.id)).filter(
80
+ WafLog.timestamp >= last_24h,
81
+ WafLog.attack_type != 'INFO'
82
+ ).scalar() or 0
83
+
84
+ # --- TOTAIS DE ONTEM (Para Tendência) ---
85
+ total_yesterday = self.db_session.query(func.count(WafLog.id)).filter(
86
+ WafLog.timestamp >= prev_24h,
87
+ WafLog.timestamp < last_24h
88
+ ).scalar() or 0
89
+
90
+ blocked_yesterday = self.db_session.query(func.count(WafLog.id)).filter(
91
+ WafLog.timestamp >= prev_24h,
92
+ WafLog.timestamp < last_24h,
93
+ WafLog.attack_type != 'INFO'
94
+ ).scalar() or 0
95
+
96
+ # --- CÁLCULO DE TENDÊNCIA (%) ---
97
+ def calc_trend(current, previous):
98
+ if previous == 0:
99
+ return 100.0 if current > 0 else 0.0
100
+ return round(((current - previous) / previous * 100), 1)
101
+
102
+ trend_total = calc_trend(total_today, total_yesterday)
103
+ trend_attacks = calc_trend(blocked_today, blocked_yesterday)
104
+
105
+ # --- INFOS COMPLEMENTARES ---
106
+ from globals import waf_cache
107
+ # Latência e RPS vindos do Cache Global
108
+ last_second_timestamp = int(time.time()) - 1
109
+ rps_key = f"rps_{last_second_timestamp}"
110
+
111
+ # 2. Lê o valor real do cache para esse segundo específico
112
+ current_rps = waf_cache.get(rps_key, default=0)
113
+
114
+ # 3. Lógica do Pico (Peak)
115
+ # Aqui continuamos usando uma chave fixa 'rps_peak_hour' para guardar o recorde
116
+ peak_rps = waf_cache.get('rps_peak_hour', default=0)
117
+
118
+ if current_rps > peak_rps:
119
+ peak_rps = current_rps
120
+ # Atualiza o recorde no cache por 1 hora
121
+ waf_cache.set('rps_peak_hour', peak_rps, expire=3600)
122
+
123
+ # Dados da Blacklist
124
+ total_blacklist = self.db_session.query(func.count(Blocked.id)).scalar() or 0
125
+ # Como blocked_at é String formatada no seu modelo, comparamos com o horário
126
+ added_today = self.db_session.query(func.count(Blocked.id)).filter(
127
+ Blocked.blocked_until >= now # Um IP "adicionado hoje" é tecnicamente um ainda bloqueado
128
+ ).scalar() or 0
129
+
130
+ return {
131
+ "total_requests_24h": {
132
+ "value": total_today,
133
+ "trend_percent": trend_total
134
+ },
135
+ "attacks_mitigated_24h": {
136
+ "value": blocked_today,
137
+ "trend_percent": trend_attacks # Adicionado aqui
138
+ },
139
+ "throughput": {
140
+ "current_req_per_sec": current_rps,
141
+ "peak_last_hour": peak_rps
142
+ },
143
+ "blacklist_count": {
144
+ "total_ips": total_blacklist,
145
+ "added_today": added_today
146
+ }
147
+ }
148
+
149
+ def get_traffic_chart():
150
+ now = datetime.now(timezone.utc)
151
+ start_time = now - timedelta(minutes=40)
152
+
153
+ # Query para agrupar por minuto
154
+ # No SQLite usamos strftime, no Postgres/MySQL seria date_format ou similar
155
+ query = self.db_session.query(
156
+ func.strftime('%H:%M', WafLog.timestamp).label('minute'),
157
+ func.count(WafLog.id).label('total'),
158
+ func.sum(case({WafLog.attack_type == 'INFO': 1}, else_=0)).label('legit'),
159
+ func.sum(case({WafLog.attack_type != 'INFO': 1}, else_=0)).label('attacks')
160
+ ).filter(WafLog.timestamp >= start_time)\
161
+ .group_by('minute')\
162
+ .order_by('minute').all()
163
+
164
+ labels = []
165
+ series_legit = []
166
+ series_attacks = []
167
+
168
+ # Preenche os arrays para o gráfico
169
+ for row in query:
170
+ labels.append(row.minute)
171
+ series_legit.append(row.legit or 0)
172
+ series_attacks.append(row.attacks or 0)
173
+
174
+ # Caso não existam dados nos últimos 40 min, retorna arrays vazios para não quebrar o front
175
+ return {
176
+ "labels": labels,
177
+ "series_legit": series_legit,
178
+ "series_attacks": series_attacks
179
+ }
180
+
181
+ def get_distribution_vectors():
182
+ now = datetime.now(timezone.utc)
183
+ last_24h = now - timedelta(hours=24)
184
+
185
+ # 1. Buscamos a contagem agrupada por tipo de ataque
186
+ # Filtramos para não incluir tráfego legítimo (INFO)
187
+ query = self.db_session.query(
188
+ WafLog.attack_type,
189
+ func.count(WafLog.id).label('count')
190
+ ).filter(
191
+ WafLog.timestamp >= last_24h,
192
+ WafLog.attack_type != 'INFO'
193
+ ).group_by(WafLog.attack_type).all()
194
+
195
+ # 2. Calculamos o total de ataques para obter a porcentagem
196
+ total_attacks = sum(row.count for row in query)
197
+
198
+ distribution = []
199
+
200
+ for row in query:
201
+ percentage = round((row.count / total_attacks * 100), 1) if total_attacks > 0 else 0
202
+ distribution.append({
203
+ "label": row.attack_type,
204
+ "count": row.count,
205
+ "percentage": percentage
206
+ })
207
+
208
+ # Caso não haja ataques, retornamos uma lista vazia ou um placeholder
209
+ return distribution
210
+
211
+ def get_top_geo():
212
+ def resolve_ip(ip):
213
+ try:
214
+ if not os.path.exists(self.geo_db_path):
215
+ print("ERRO: Arquivo GeoLite2-Country.mmdb não encontrado!")
216
+ return "XX", "Unknown"
217
+
218
+ with geoip2.database.Reader(self.geo_db_path) as reader:
219
+ response = reader.country(ip)
220
+ return response.country.iso_code, response.country.name
221
+ except Exception as e:
222
+ print(f"Erro na consulta GeoIP: {e}") # Isso vai te dizer se o banco está corrompido ou o IP é inválido
223
+ return "XX", "Unknown"
224
+
225
+ now = datetime.now(timezone.utc)
226
+ last_24h = now - timedelta(hours=24)
227
+
228
+ # 1. Busca todos os ataques agrupados por IP
229
+ query = self.db_session.query(
230
+ WafLog.ip,
231
+ func.count(WafLog.id).label('count')
232
+ ).filter(
233
+ WafLog.timestamp >= last_24h,
234
+ WafLog.attack_type != 'INFO'
235
+ ).group_by(WafLog.ip).all()
236
+
237
+ geo_stats = {}
238
+ total_attacks = 0
239
+
240
+ # 2. Processa cada IP real usando o GeoIP2
241
+ for row in query:
242
+ code, name = resolve_ip(row.ip)
243
+
244
+ if code not in geo_stats:
245
+ geo_stats[code] = {"name": name, "count": 0}
246
+
247
+ geo_stats[code]["count"] += row.count
248
+ total_attacks += row.count
249
+
250
+ # 3. Formata o Top 5
251
+ top_geo = []
252
+ sorted_geo = sorted(geo_stats.items(), key=lambda x: x[1]['count'], reverse=True)[:5]
253
+
254
+ for code, data in sorted_geo:
255
+ percentage = round((data["count"] / total_attacks * 100), 1) if total_attacks > 0 else 0
256
+ top_geo.append({
257
+ "country_code": code,
258
+ "country_name": data["name"],
259
+ "count": data["count"],
260
+ "percentage": percentage
261
+ })
262
+
263
+ return top_geo
264
+
265
+ def get_top_offenders():
266
+ now = datetime.now(timezone.utc)
267
+ last_24h = now - timedelta(hours=24)
268
+
269
+ # 1. Agrupamos por IP e contamos os hits e os tipos de ataques diferentes
270
+ # Ignoramos tráfego INFO
271
+ query = self.db_session.query(
272
+ WafLog.ip,
273
+ func.count(WafLog.id).label('hits_count'),
274
+ func.count(func.distinct(WafLog.attack_type)).label('unique_vectors')
275
+ ).filter(
276
+ WafLog.timestamp >= last_24h,
277
+ WafLog.attack_type != 'INFO'
278
+ ).group_by(WafLog.ip).order_by(text('hits_count DESC')).limit(5).all()
279
+
280
+ offenders = []
281
+ for row in query:
282
+ # 2. Lógica de Risk Score (0 a 100)
283
+ # Baseada em volume e diversidade de ataques
284
+ # Ex: Cada vetor único vale 20 pontos + 1 ponto para cada 50 hits (até o teto de 100)
285
+ vector_points = row.unique_vectors * 20
286
+ hit_points = row.hits_count // 50
287
+ risk_score = min(100, vector_points + hit_points)
288
+
289
+ offenders.append({
290
+ "ip": row.ip,
291
+ "risk_score": int(risk_score),
292
+ "hits_count": row.hits_count
293
+ })
294
+
295
+ return offenders
296
+
297
+ try:
298
+ json['meta'] = get_server_info()
299
+ json['kpis'] = get_kpis()
300
+ json['traffic_chart'] = get_traffic_chart()
301
+ json['distribution_vectors'] = get_distribution_vectors()
302
+ json['top_geo'] = get_top_geo()
303
+ json['top_offenders'] = get_top_offenders()
304
+
305
+ return json
306
+
307
+ except Exception as e:
308
+ print(f"Erro no Dashboard: {e}")
309
+ return {"error": str(e)}
310
+
311
+ finally:
312
+ self.db_session.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafaHell
3
- Version: 0.1.1
3
+ Version: 1.0.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
@@ -42,14 +46,14 @@ Middleware WAF for Flask, to detect SQLi and XSS.
42
46
  ## Instalation
43
47
 
44
48
  ```bash
45
- pip install wafaHell
49
+ pip install wafahell
46
50
  ```
47
51
 
48
52
  ## Usage
49
53
  ```python
50
54
  from flask import Flask
51
- from flask_waf import FlaskWAF
55
+ from wafahell import WafaHell
52
56
 
53
57
  app = Flask(__name__)
54
- waf = FlaskWAF(app)
58
+ waf = WafaHell(app)
55
59
  ```
@@ -0,0 +1,15 @@
1
+ wafaHell/__init__.py,sha256=bdafr3LRK7_frr-V6umEJZUqt8RMrOiLJy72DSQfs2o,60
2
+ wafaHell/app.py,sha256=mXUCyOVrbU2Cdugh-UIDSQf-6zqvT2aazZtJQp_KKJI,980
3
+ wafaHell/globals.py,sha256=keIe0YCNsb5si0yUhUjd6nlk1s0YgfH5G7lH5-05Zws,160
4
+ wafaHell/logger.py,sha256=keT-Iw6g9w1dtrk0GhldxhgKtmNUVClgFGkSkAWXS5Y,5686
5
+ wafaHell/middleware.py,sha256=cse81wj9ocUV11MPfrmvleD5nYq6asjDx6dqXToIcIs,14348
6
+ wafaHell/mock.py,sha256=EgfSCKYV3rUTerKoEyFQPSf47wrOo-E0yqe7JpQUe3w,2105
7
+ wafaHell/model.py,sha256=OB4sEucMSgO-DBLgLhqh0oBx5fZRug23MDnRc8vS5w4,2501
8
+ wafaHell/panel.py,sha256=NpbvgXy5Regm4dQDoMapr-otW27TvC7Q-vdPrZDD-5k,7465
9
+ wafaHell/rateLimiter.py,sha256=p4IDxha-ZrRPROFf8Fa1gRTbbUTQdbD9TgBTmyMR2hQ,664
10
+ wafaHell/utils.py,sha256=v9NXo94AQiHjjCOgC38vwrlLoQOCZqp_qOjZVz00NEI,12750
11
+ wafahell-1.0.0.dist-info/licenses/LICENSE,sha256=6bv9v4HamenV3rqm3mhaGOecwGFrgxtVTW7JPfFDmeY,1086
12
+ wafahell-1.0.0.dist-info/METADATA,sha256=2EA_4vTwwoXimX_etFdZo_x_Z2_qijnbSjuIHfwE10Q,2087
13
+ wafahell-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ wafahell-1.0.0.dist-info/top_level.txt,sha256=VGBo2g3pOeTH2qIXfZDJCSblJgijemMHUHmI0bBgrls,9
15
+ wafahell-1.0.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
 
@@ -1,7 +0,0 @@
1
- wafaHell/__init__.py,sha256=enTIAbStl7o_cDo93-s8nq86BsN3OEGYu9AbtESQVXQ,105
2
- wafaHell/middleware.py,sha256=UQ5TPcuz6UDyy5FjyUNXSAZqv2IjnOoc3ze2M8He7Tw,2151
3
- wafahell-0.1.1.dist-info/licenses/LICENSE,sha256=6bv9v4HamenV3rqm3mhaGOecwGFrgxtVTW7JPfFDmeY,1086
4
- wafahell-0.1.1.dist-info/METADATA,sha256=DKx0AS6ZJ82ATAkBJetN_AG8doVO00xxMI7PwBdcBzQ,1980
5
- wafahell-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- wafahell-0.1.1.dist-info/top_level.txt,sha256=VGBo2g3pOeTH2qIXfZDJCSblJgijemMHUHmI0bBgrls,9
7
- wafahell-0.1.1.dist-info/RECORD,,