wafaHell 0.1.1__tar.gz → 0.2.0__tar.gz

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.
@@ -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
  ```
@@ -5,14 +5,14 @@ Middleware WAF for Flask, to detect SQLi and XSS.
5
5
  ## Instalation
6
6
 
7
7
  ```bash
8
- pip install wafaHell
8
+ pip install wafahell
9
9
  ```
10
10
 
11
11
  ## Usage
12
12
  ```python
13
13
  from flask import Flask
14
- from flask_waf import FlaskWAF
14
+ from wafahell import WafaHell
15
15
 
16
16
  app = Flask(__name__)
17
- waf = FlaskWAF(app)
17
+ waf = WafaHell(app)
18
18
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wafaHell"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "Middleware WAF to Flask"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1,3 @@
1
+ from .middleware import WafaHell
2
+
3
+ __all__ = ["WafaHell"]
@@ -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)
@@ -0,0 +1,151 @@
1
+ import re
2
+ from flask import request as req, abort
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)
12
+
13
+ class WafaHell:
14
+ def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False, block_ip=False, rate_limit=False):
15
+ self.app = app
16
+ self.block_code = block_code
17
+ self.log = log_func or Logger()
18
+ self.monitor_mode = monitor_mode
19
+ self.block_ip = block_ip
20
+ self.rate_limit = rate_limit
21
+
22
+ self.rules = [
23
+ r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
24
+ r"' OR '1'='1",
25
+ r"<script.*?>.*?</script>",
26
+ r"javascript:",
27
+ ]
28
+
29
+ if app is not None:
30
+ self.init_app(app)
31
+
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
+
51
+ @app.before_request
52
+ def waf_check():
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))
65
+
66
+ def detect_attack(self, data: str) -> bool:
67
+ for pattern in self.rules:
68
+ if re.search(pattern, data, re.IGNORECASE):
69
+ return True
70
+ return False
71
+
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
76
+
77
+ for key, value in req.args.items():
78
+ if self.detect_attack(value):
79
+ return True, f"QUERY '{key}'", value
80
+
81
+ for key, value in req.headers.items():
82
+ if self.detect_attack(value):
83
+ return True, f"HEADER '{key}'", value
84
+
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()
133
+
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:
145
+ ip = req.remote_addr
146
+ user_agent = req.headers.get("User-Agent", "unknown")
147
+ path = req.path
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
@@ -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
@@ -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)
@@ -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
  ```
@@ -2,7 +2,12 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  wafaHell/__init__.py
5
+ wafaHell/logger.py
5
6
  wafaHell/middleware.py
7
+ wafaHell/model.py
8
+ wafaHell/rateLimiter.py
9
+ wafaHell/teste.py
10
+ wafaHell/utils.py
6
11
  wafaHell.egg-info/PKG-INFO
7
12
  wafaHell.egg-info/SOURCES.txt
8
13
  wafaHell.egg-info/dependency_links.txt
@@ -1,3 +0,0 @@
1
- from .middleware import WafaHell
2
-
3
- # agora a classe está disponível diretamente ao importar o pacote
@@ -1,65 +0,0 @@
1
- import re
2
- from flask import request, abort
3
- from urllib.parse import unquote
4
-
5
- class WafaHell:
6
- def __init__(self, app=None, block_code=403, log_func=None, monitor_mode=False):
7
- self.app = app
8
- self.block_code = block_code
9
- self.log_func = log_func or (lambda msg: print(f"[WAF] {msg}"))
10
- self.monitor_mode = monitor_mode
11
- # Regras básicas
12
- self.rules = [
13
- r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
14
- r"' OR '1'='1",
15
- r"<script.*?>.*?</script>",
16
- r"javascript:",
17
- ]
18
-
19
- if app is not None:
20
- self.init_app(app)
21
-
22
- def init_app(self, app):
23
- @app.before_request
24
- 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)
29
-
30
- def detect_attack(self, data: str) -> bool:
31
- for pattern in self.rules:
32
- if re.search(pattern, data, re.IGNORECASE):
33
- return True
34
- return False
35
-
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
40
-
41
- # Headers
42
- for _, value in req.headers.items():
43
- if self.detect_attack(value):
44
- return True
45
-
46
- # Body
47
- if req.data and self.detect_attack(req.data.decode(errors="ignore")):
48
- return True
49
-
50
- return False
51
-
52
- def log_attack(self, req):
53
- ip = req.remote_addr
54
- user_agent = req.headers.get("User-Agent", "unknown")
55
- 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)
File without changes
File without changes