wafaHell 0.1.1__py3-none-any.whl → 0.2.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/logger.py ADDED
@@ -0,0 +1,45 @@
1
+ import logging
2
+
3
+ class Logger:
4
+ def __init__(self, name="WAF", log_file="waf.log", level=logging.INFO):
5
+ # Cria logger com nome
6
+ self.logger = logging.getLogger(name)
7
+ self.logger.setLevel(level)
8
+
9
+ # Evita handlers duplicados ao reinicializar
10
+ if not self.logger.handlers:
11
+ # Handler para console
12
+ console_handler = logging.StreamHandler()
13
+ console_handler.setLevel(level)
14
+
15
+ # Handler para arquivo
16
+ file_handler = logging.FileHandler(log_file)
17
+ file_handler.setLevel(level)
18
+
19
+ # Formato
20
+ formatter = logging.Formatter(
21
+ "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s",
22
+ datefmt="%H:%M:%S - %d/%m/%Y"
23
+ )
24
+
25
+ console_handler.setFormatter(formatter)
26
+ file_handler.setFormatter(formatter)
27
+
28
+ # Adiciona handlers
29
+ self.logger.addHandler(console_handler)
30
+ self.logger.addHandler(file_handler)
31
+
32
+ def info(self, msg):
33
+ self.logger.info(msg)
34
+
35
+ def warning(self, msg):
36
+ self.logger.warning(msg)
37
+
38
+ def error(self, msg):
39
+ self.logger.error(msg)
40
+
41
+ def critical(self, msg):
42
+ self.logger.critical(msg)
43
+
44
+ def debug(self, msg):
45
+ self.logger.debug(msg)
wafaHell/middleware.py CHANGED
@@ -1,14 +1,24 @@
1
1
  import re
2
- from flask import request, abort
2
+ from flask import request as req, abort
3
3
  from urllib.parse import unquote
4
+ from model import Blocked, get_session
5
+ from logger import Logger
6
+ from utils import is_block_expired
7
+ from rateLimiter import RateLimiter
8
+ from sqlalchemy.exc import OperationalError
9
+
10
+ # Inicializa o RateLimiter
11
+ limiter = RateLimiter(limit=100, window=60)
4
12
 
5
13
  class WafaHell:
6
- def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False):
14
+ def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False, block_ip=False, rate_limit=False):
7
15
  self.app = app
8
16
  self.block_code = block_code
9
- self.log_func = log_func or (lambda msg: print(f"[WAF] {msg}"))
17
+ self.log = log_func or Logger()
10
18
  self.monitor_mode = monitor_mode
11
- # Regras básicas
19
+ self.block_ip = block_ip
20
+ self.rate_limit = rate_limit
21
+
12
22
  self.rules = [
13
23
  r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
14
24
  r"' OR '1'='1",
@@ -20,12 +30,38 @@ class WafaHell:
20
30
  self.init_app(app)
21
31
 
22
32
  def init_app(self, app):
33
+ # Configura a sessão para cada requisição
34
+ @app.before_request
35
+ def create_session():
36
+ try:
37
+ req.session = get_session() # Cria uma nova sessão usando get_session
38
+ except Exception as e:
39
+ self.log.error(f"Erro ao criar sessão para requisição: {e}")
40
+ abort(self.block_code) # Retorna erro 500 se não conseguir criar a sessão
41
+
42
+ # Fecha a sessão após cada requisição
43
+ @app.teardown_request
44
+ def close_session(exc=None):
45
+ if hasattr(req, 'session'):
46
+ try:
47
+ req.session.close() # Fecha a sessão para liberar a conexão
48
+ except Exception as e:
49
+ self.log.error(f"Erro ao fechar sessão: {e}")
50
+
23
51
  @app.before_request
24
52
  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)
53
+ self.verify_client_blocked(req)
54
+ self.verify_rate_limit(req)
55
+ is_malicious, attack_local, payload = self.is_malicious(req)
56
+
57
+ if not is_malicious:
58
+ return
59
+
60
+ if not self.monitor_mode:
61
+ self.log.warning(self.parse_req(req, payload,attack_local))
62
+ self.block_ip_address(req.remote_addr, req.headers.get("User-Agent", "unknown"))
63
+ else:
64
+ self.log.info(self.parse_req(req, payload, attack_local))
29
65
 
30
66
  def detect_attack(self, data: str) -> bool:
31
67
  for pattern in self.rules:
@@ -33,33 +69,83 @@ class WafaHell:
33
69
  return True
34
70
  return False
35
71
 
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
72
+ def is_malicious(self, req) -> tuple[bool, str | None, str | None]:
73
+
74
+ if self.detect_attack(req.base_url):
75
+ return True, "URL", req.base_url
40
76
 
41
- # Headers
42
- for _, value in req.headers.items():
77
+ for key, value in req.args.items():
43
78
  if self.detect_attack(value):
44
- return True
79
+ return True, f"QUERY '{key}'", value
45
80
 
46
- # Body
47
- if req.data and self.detect_attack(req.data.decode(errors="ignore")):
48
- return True
81
+ for key, value in req.headers.items():
82
+ if self.detect_attack(value):
83
+ return True, f"HEADER '{key}'", value
49
84
 
50
- return False
85
+ if req.data:
86
+ body_content = req.data.decode(errors="ignore")
87
+ if self.detect_attack(body_content):
88
+ return True, "BODY", body_content
89
+
90
+ if req.is_json:
91
+ json_data = req.get_json(silent=True)
92
+ if json_data:
93
+ import json
94
+ json_str = json.dumps(json_data)
95
+ if self.detect_attack(json_str):
96
+ return True, "JSON BODY", json_str
97
+
98
+ return False, None, None
99
+
100
+ def verify_client_blocked(self, req) -> None:
101
+ session = req.session
102
+ try:
103
+ client_blocked = session.query(Blocked).filter_by(
104
+ ip=req.remote_addr, user_agent=req.headers.get("User-Agent")
105
+ ).first()
106
+ if client_blocked:
107
+ if is_block_expired(client_blocked.blocked_at):
108
+ session.delete(client_blocked)
109
+ session.commit()
110
+ self.log.info(f"[UNBLOCKED] IP {req.remote_addr} desbloqueado apos expiracao do bloqueio.")
111
+ else:
112
+ abort(self.block_code)
113
+ except OperationalError as e:
114
+ session.rollback()
115
+ abort(self.block_code)
116
+
117
+
118
+ def block_ip_address(self, ip, user_agent=None):
119
+ if self.block_ip:
120
+ try:
121
+ session = req.session
122
+ blocked_client = Blocked(ip=ip, user_agent=user_agent)
123
+ session.add(blocked_client)
124
+ session.commit()
125
+ self.log.warning(f"[BLOCKED] IP: {ip}, User-Agent: {user_agent}")
126
+ except OperationalError as e:
127
+ self.log.error(f"Erro de banco de dados ao bloquear IP {ip}: {e}")
128
+ session.rollback()
129
+ abort(self.block_code)
130
+ except Exception as e:
131
+ self.log.error(f"Erro ao bloquear IP {ip}: {e}")
132
+ session.rollback()
51
133
 
52
- def log_attack(self, req):
134
+ def verify_rate_limit(self, req) -> None:
135
+ if self.rate_limit:
136
+ ip = req.remote_addr
137
+ ua = req.headers.get("User-Agent", "unknown")
138
+ if limiter.is_rate_limited(ip, ua):
139
+ self.log.warning(f"[RATE LIMIT] IP: {ip}, User-Agent: {ua} excedeu o limite de requisições.")
140
+ if self.block_ip:
141
+ self.block_ip_address(ip, ua)
142
+ abort(self.block_code)
143
+
144
+ def parse_req(self, req, payload, attack_local=None) -> str:
53
145
  ip = req.remote_addr
54
146
  user_agent = req.headers.get("User-Agent", "unknown")
55
147
  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)
148
+ method = req.method
149
+ attack_local = attack_local or "unknown"
150
+ msg = f"""[ATTACK] IP: {ip}, User-Agent: {user_agent}, Path: {path}, Method: {method}, Payload: {unquote(payload)}, attack_local: {attack_local}"""
151
+ return msg
wafaHell/model.py ADDED
@@ -0,0 +1,23 @@
1
+ from datetime import datetime
2
+ from sqlalchemy import create_engine, Column, Integer, String
3
+ from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
4
+
5
+ Base = declarative_base()
6
+
7
+ class Blocked(Base):
8
+ __tablename__ = "blocks"
9
+
10
+ id = Column(Integer, primary_key=True, autoincrement=True)
11
+ ip = Column(String, nullable=True)
12
+ user_agent = Column(String, nullable=True)
13
+ blocked_at = Column(String, nullable=False, default=lambda: datetime.now().strftime("%H:%M:%S"))
14
+
15
+ 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
@@ -0,0 +1,21 @@
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
+
21
+ return len(self.requests_log[key]) >= self.limit
wafaHell/teste.py ADDED
@@ -0,0 +1,16 @@
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)
wafaHell/utils.py ADDED
@@ -0,0 +1,10 @@
1
+ from datetime import datetime, timedelta
2
+
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)
7
+
8
+ diff = now - blocked_time
9
+ return diff >= timedelta(minutes=1)
10
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafaHell
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Middleware WAF to Flask
5
5
  Author-email: Yago Martins <yagomartins30@gmail.com>
6
6
  License: MIT License
@@ -42,14 +42,14 @@ Middleware WAF for Flask, to detect SQLi and XSS.
42
42
  ## Instalation
43
43
 
44
44
  ```bash
45
- pip install wafaHell
45
+ pip install wafahell
46
46
  ```
47
47
 
48
48
  ## Usage
49
49
  ```python
50
50
  from flask import Flask
51
- from flask_waf import FlaskWAF
51
+ from wafahell import WafaHell
52
52
 
53
53
  app = Flask(__name__)
54
- waf = FlaskWAF(app)
54
+ waf = WafaHell(app)
55
55
  ```
@@ -0,0 +1,12 @@
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,,
@@ -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,,