wafaHell 0.2.0__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/app.py +31 -0
- wafaHell/globals.py +5 -0
- wafaHell/logger.py +101 -14
- wafaHell/middleware.py +262 -62
- wafaHell/mock.py +53 -0
- wafaHell/model.py +87 -10
- wafaHell/panel.py +192 -0
- wafaHell/rateLimiter.py +0 -1
- wafaHell/utils.py +309 -7
- {wafahell-0.2.0.dist-info → wafahell-1.0.0.dist-info}/METADATA +5 -1
- wafahell-1.0.0.dist-info/RECORD +15 -0
- {wafahell-0.2.0.dist-info → wafahell-1.0.0.dist-info}/WHEEL +1 -1
- wafaHell/teste.py +0 -16
- wafahell-0.2.0.dist-info/RECORD +0 -12
- {wafahell-0.2.0.dist-info → wafahell-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {wafahell-0.2.0.dist-info → wafahell-1.0.0.dist-info}/top_level.txt +0 -0
wafaHell/model.py
CHANGED
|
@@ -1,23 +1,100 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from sqlalchemy import
|
|
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(
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
|
wafaHell/rateLimiter.py
CHANGED
wafaHell/utils.py
CHANGED
|
@@ -1,10 +1,312 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
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
|
|
2
16
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
+
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.
|
|
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
|
|
@@ -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,,
|
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)
|