GuardianUnivalle-Benito-Yucra 0.1.2__tar.gz → 0.1.3__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.
Potentially problematic release.
This version of GuardianUnivalle-Benito-Yucra might be problematic. Click here for more details.
- guardianunivalle_benito_yucra-0.1.3/GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +89 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra.egg-info/PKG-INFO +1 -1
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/PKG-INFO +1 -1
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/pyproject.toml +1 -1
- guardianunivalle_benito_yucra-0.1.2/GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +0 -192
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/__init__.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/criptografia/kdf.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/detectores/detector_keylogger.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra/utilidades.py +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra.egg-info/SOURCES.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra.egg-info/dependency_links.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra.egg-info/requires.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/GuardianUnivalle_Benito_Yucra.egg-info/top_level.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/LICENSE +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/README.md +0 -0
- {guardianunivalle_benito_yucra-0.1.2 → guardianunivalle_benito_yucra-0.1.3}/setup.cfg +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# middleware_sql_defense.py
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
from django.http import JsonResponse
|
|
6
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
7
|
+
|
|
8
|
+
# ---------- Patrones y normalización ----------
|
|
9
|
+
_literal_single = re.compile(r"'([^'\\]|\\.)*'")
|
|
10
|
+
_literal_double = re.compile(r'"([^"\\]|\\.)*"')
|
|
11
|
+
_comment_sql = re.compile(r"(--[^\n]*|/\*.*?\*/)", re.S)
|
|
12
|
+
|
|
13
|
+
PATTERNS = [
|
|
14
|
+
(re.compile(r"\bunion\b\s+(all\s+)?\bselect\b", re.I), 0.95),
|
|
15
|
+
(re.compile(r"(?<!\w)or(?=\s+1=1)", re.I), 0.99),
|
|
16
|
+
(re.compile(r"\b(select\b.*\bfrom\b.*\bwhere\b.*\b(or|and)\b.*=)", re.I), 0.7),
|
|
17
|
+
(re.compile(r"\b(drop|truncate|delete|insert|update)\b", re.I), 0.8),
|
|
18
|
+
(re.compile(r"(--|#|;)", re.I), 0.4),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------- Helpers ----------
|
|
23
|
+
def normalize_text(s: str) -> str:
|
|
24
|
+
"""Quita literales y comentarios para reducir falsos positivos"""
|
|
25
|
+
if not s:
|
|
26
|
+
return ""
|
|
27
|
+
s = _comment_sql.sub(" ", s)
|
|
28
|
+
s = _literal_single.sub("''", s)
|
|
29
|
+
s = _literal_double.sub('""', s)
|
|
30
|
+
s = re.sub(r"\s+", " ", s).strip()
|
|
31
|
+
return s
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_payload_text(request) -> str:
|
|
35
|
+
"""Extrae texto potencialmente peligroso del request"""
|
|
36
|
+
parts = []
|
|
37
|
+
try:
|
|
38
|
+
# query params
|
|
39
|
+
if request.META.get("QUERY_STRING"):
|
|
40
|
+
parts.append(request.META.get("QUERY_STRING"))
|
|
41
|
+
# body
|
|
42
|
+
content_type = request.META.get("CONTENT_TYPE", "")
|
|
43
|
+
if "application/json" in content_type:
|
|
44
|
+
try:
|
|
45
|
+
body_json = json.loads(request.body.decode("utf-8") or "{}")
|
|
46
|
+
parts.append(json.dumps(body_json))
|
|
47
|
+
except Exception:
|
|
48
|
+
parts.append((request.body or b"").decode("utf-8", errors="ignore"))
|
|
49
|
+
else:
|
|
50
|
+
try:
|
|
51
|
+
parts.append(request.body.decode("utf-8", errors="ignore"))
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
# headers sospechosos
|
|
55
|
+
parts.append(request.META.get("HTTP_USER_AGENT", ""))
|
|
56
|
+
parts.append(request.META.get("HTTP_REFERER", ""))
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return " ".join([p for p in parts if p])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def detect_sqli_text(text: str) -> Tuple[bool, list]:
|
|
63
|
+
"""Detecta patrones en un texto normalizado"""
|
|
64
|
+
q = normalize_text(text)
|
|
65
|
+
matches = []
|
|
66
|
+
for patt, sev in PATTERNS:
|
|
67
|
+
if patt.search(q):
|
|
68
|
+
matches.append((patt.pattern, float(sev)))
|
|
69
|
+
return (len(matches) > 0, matches)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------- Middleware ----------
|
|
73
|
+
class SQLIDefenseMiddleware(MiddlewareMixin):
|
|
74
|
+
def process_request(self, request):
|
|
75
|
+
text = extract_payload_text(request)
|
|
76
|
+
if not text:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
flagged, matches = detect_sqli_text(text)
|
|
80
|
+
if flagged:
|
|
81
|
+
# Bloqueo inmediato solo para pruebas
|
|
82
|
+
return JsonResponse(
|
|
83
|
+
{
|
|
84
|
+
"detail": "Request bloqueado: posible inyección SQL detectada",
|
|
85
|
+
"matches": matches,
|
|
86
|
+
},
|
|
87
|
+
status=403,
|
|
88
|
+
)
|
|
89
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GuardianUnivalle-Benito-Yucra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Middleware y detectores de seguridad (SQLi, XSS, CSRF, DoS, Keylogger) para Django/Flask
|
|
5
5
|
Author-email: Andres Benito Calle Yucra <benitoandrescalle035@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GuardianUnivalle-Benito-Yucra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Middleware y detectores de seguridad (SQLi, XSS, CSRF, DoS, Keylogger) para Django/Flask
|
|
5
5
|
Author-email: Andres Benito Calle Yucra <benitoandrescalle035@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "GuardianUnivalle-Benito-Yucra" # usar mayúsculas consistente
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Middleware y detectores de seguridad (SQLi, XSS, CSRF, DoS, Keylogger) para Django/Flask"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Andres Benito Calle Yucra", email = "benitoandrescalle035@gmail.com" }
|
guardianunivalle_benito_yucra-0.1.2/GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
# middleware_sql_defense.py
|
|
2
|
-
import re
|
|
3
|
-
import json
|
|
4
|
-
import time
|
|
5
|
-
from typing import Tuple
|
|
6
|
-
from django.conf import settings
|
|
7
|
-
from django.http import JsonResponse
|
|
8
|
-
from django.utils.deprecation import MiddlewareMixin
|
|
9
|
-
import redis
|
|
10
|
-
|
|
11
|
-
# ---------- CONFIGURABLES (poner en settings.py preferiblemente) ----------
|
|
12
|
-
W_SQL = getattr(settings, "SQL_DEFENSE_W_SQL", 1.0)
|
|
13
|
-
THRESHOLD = getattr(settings, "SQL_DEFENSE_THRESHOLD", 0.8) # score normalizado 0..1
|
|
14
|
-
WINDOW_SEC = getattr(
|
|
15
|
-
settings, "SQL_DEFENSE_WINDOW_SEC", 300
|
|
16
|
-
) # ventana para conteo (ej. 5min)
|
|
17
|
-
MAX_EXPECTED_DETECTIONS = getattr(settings, "SQL_DEFENSE_MAX_EXPECTED_DETECTIONS", 10)
|
|
18
|
-
BLOCK_TTL = getattr(
|
|
19
|
-
settings, "SQL_DEFENSE_BLOCK_TTL", 600
|
|
20
|
-
) # bloqueo por IP en segundos (ej. 10min)
|
|
21
|
-
|
|
22
|
-
REDIS_URL = getattr(settings, "SQL_DEFENSE_REDIS_URL", "redis://localhost:6379/0")
|
|
23
|
-
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
|
24
|
-
|
|
25
|
-
# ---------- Patrones y normalización ----------
|
|
26
|
-
_literal_single = re.compile(r"'([^'\\]|\\.)*'")
|
|
27
|
-
_literal_double = re.compile(r'"([^"\\]|\\.)*"')
|
|
28
|
-
_comment_sql = re.compile(r"(--[^\n]*|/\*.*?\*/)", re.S)
|
|
29
|
-
|
|
30
|
-
PATTERNS = [
|
|
31
|
-
(re.compile(r"\bunion\b\s+(all\s+)?\bselect\b", re.I), 0.95),
|
|
32
|
-
(re.compile(r"(?<!\w)or(?=\s+1=1)", re.I), 0.99),
|
|
33
|
-
(re.compile(r"\b(select\b.*\bfrom\b.*\bwhere\b.*\b(or|and)\b.*=)", re.I), 0.7),
|
|
34
|
-
(re.compile(r"\b(drop|truncate|delete|insert|update)\b", re.I), 0.8),
|
|
35
|
-
(re.compile(r"(--|#|;)", re.I), 0.4),
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# ---------- Helpers ----------
|
|
40
|
-
def get_client_ip(request) -> str:
|
|
41
|
-
"""Obtiene la IP real (si hay proxies, usa X-Forwarded-For)"""
|
|
42
|
-
xff = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
43
|
-
if xff:
|
|
44
|
-
# X-Forwarded-For puede contener lista de IPs
|
|
45
|
-
ip = xff.split(",")[0].strip()
|
|
46
|
-
return ip
|
|
47
|
-
return request.META.get("REMOTE_ADDR", "")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def normalize_text(s: str) -> str:
|
|
51
|
-
"""Quita literales y comentarios para reducir falsos positivos"""
|
|
52
|
-
if not s:
|
|
53
|
-
return ""
|
|
54
|
-
s = _comment_sql.sub(" ", s)
|
|
55
|
-
s = _literal_single.sub("''", s)
|
|
56
|
-
s = _literal_double.sub('""', s)
|
|
57
|
-
s = re.sub(r"\s+", " ", s).strip()
|
|
58
|
-
return s
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def extract_payload_text(request) -> str:
|
|
62
|
-
"""
|
|
63
|
-
Extrae texto potencialmente peligroso del request:
|
|
64
|
-
- query string
|
|
65
|
-
- body (JSON o form)
|
|
66
|
-
- headers sospechosos (User-Agent, Referer)
|
|
67
|
-
"""
|
|
68
|
-
parts = []
|
|
69
|
-
try:
|
|
70
|
-
# query params
|
|
71
|
-
if request.META.get("QUERY_STRING"):
|
|
72
|
-
parts.append(request.META.get("QUERY_STRING"))
|
|
73
|
-
# body: intenta json, si no, raw text
|
|
74
|
-
content_type = request.META.get("CONTENT_TYPE", "")
|
|
75
|
-
if "application/json" in content_type:
|
|
76
|
-
try:
|
|
77
|
-
body_json = json.loads(request.body.decode("utf-8") or "{}")
|
|
78
|
-
parts.append(json.dumps(body_json))
|
|
79
|
-
except Exception:
|
|
80
|
-
parts.append((request.body or b"").decode("utf-8", errors="ignore"))
|
|
81
|
-
else:
|
|
82
|
-
# form-encoded or other text
|
|
83
|
-
try:
|
|
84
|
-
parts.append(request.body.decode("utf-8", errors="ignore"))
|
|
85
|
-
except Exception:
|
|
86
|
-
pass
|
|
87
|
-
# headers
|
|
88
|
-
parts.append(request.META.get("HTTP_USER_AGENT", ""))
|
|
89
|
-
parts.append(request.META.get("HTTP_REFERER", ""))
|
|
90
|
-
except Exception:
|
|
91
|
-
pass
|
|
92
|
-
return " ".join([p for p in parts if p])
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def detect_sqli_text(text: str) -> Tuple[bool, list]:
|
|
96
|
-
"""Detecta patrones en un texto normalizado; devuelve matches con severidad."""
|
|
97
|
-
q = normalize_text(text)
|
|
98
|
-
matches = []
|
|
99
|
-
for patt, sev in PATTERNS:
|
|
100
|
-
if patt.search(q):
|
|
101
|
-
matches.append((patt.pattern, float(sev)))
|
|
102
|
-
return (len(matches) > 0, matches)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# ---------- Redis keys ----------
|
|
106
|
-
def redis_count_key(ip: str) -> str:
|
|
107
|
-
return f"sqli:count:{ip}"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def redis_block_key(ip: str) -> str:
|
|
111
|
-
return f"sqli:block:{ip}"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# ---------- Cálculo de score S_sql/ip ----------
|
|
115
|
-
def compute_s_sql_ip(detections_count: int) -> float:
|
|
116
|
-
"""
|
|
117
|
-
Convertir conteo a una puntuación normalizada 0..1.
|
|
118
|
-
Usamos saturación en MAX_EXPECTED_DETECTIONS.
|
|
119
|
-
"""
|
|
120
|
-
norm = min(float(detections_count) / float(MAX_EXPECTED_DETECTIONS), 1.0)
|
|
121
|
-
score = float(W_SQL) * norm
|
|
122
|
-
# Normalizamos a 0..1 si W_SQL puede ser mayor que 1
|
|
123
|
-
return min(score, 1.0)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# ---------- Middleware ----------
|
|
127
|
-
class SQLIDefenseMiddleware(MiddlewareMixin):
|
|
128
|
-
def process_request(self, request):
|
|
129
|
-
# 1) obtener IP y comprobar si está bloqueada
|
|
130
|
-
ip = get_client_ip(request)
|
|
131
|
-
if not ip:
|
|
132
|
-
return None # no podemos hacer mucho sin IP
|
|
133
|
-
|
|
134
|
-
# comprobar bloqueo en Redis
|
|
135
|
-
block_key = redis_block_key(ip)
|
|
136
|
-
if redis_client.exists(block_key):
|
|
137
|
-
ttl = redis_client.ttl(block_key)
|
|
138
|
-
return JsonResponse(
|
|
139
|
-
{
|
|
140
|
-
"detail": "Acceso denegado (bloqueado por actividad sospechosa)",
|
|
141
|
-
"block_ttl_s": ttl,
|
|
142
|
-
},
|
|
143
|
-
status=403,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# 2) extraer texto y detectar patrones
|
|
147
|
-
text = extract_payload_text(request)
|
|
148
|
-
if not text:
|
|
149
|
-
return None
|
|
150
|
-
|
|
151
|
-
flagged, matches = detect_sqli_text(text)
|
|
152
|
-
if flagged:
|
|
153
|
-
# incrementar contador con TTL (ventana)
|
|
154
|
-
count_key = redis_count_key(ip)
|
|
155
|
-
# INCR y asegurar expiration
|
|
156
|
-
new_count = redis_client.incr(count_key)
|
|
157
|
-
# establecer TTL si fue creado de nuevo
|
|
158
|
-
if redis_client.ttl(count_key) == -1:
|
|
159
|
-
redis_client.expire(count_key, WINDOW_SEC)
|
|
160
|
-
|
|
161
|
-
# calcular score
|
|
162
|
-
current_count = int(new_count)
|
|
163
|
-
s_sql_ip = compute_s_sql_ip(current_count)
|
|
164
|
-
|
|
165
|
-
# registrar evento (puedes ampliar con logging o envío a SIEM)
|
|
166
|
-
# guardamos metadata mínima
|
|
167
|
-
event = {
|
|
168
|
-
"time": int(time.time()),
|
|
169
|
-
"ip": ip,
|
|
170
|
-
"count": current_count,
|
|
171
|
-
"score": s_sql_ip,
|
|
172
|
-
"matches": matches,
|
|
173
|
-
}
|
|
174
|
-
# Puedes push a lista en Redis o a un logger
|
|
175
|
-
redis_client.lpush("sqli:events", json.dumps(event))
|
|
176
|
-
redis_client.ltrim("sqli:events", 0, 999) # mantener últimos 1000 eventos
|
|
177
|
-
|
|
178
|
-
# Si supera THRESHOLD -> bloquear ip
|
|
179
|
-
if s_sql_ip >= float(THRESHOLD):
|
|
180
|
-
redis_client.set(redis_block_key(ip), "1", ex=BLOCK_TTL)
|
|
181
|
-
# opcional: publicar alerta en canal pubsub o webhook
|
|
182
|
-
return JsonResponse(
|
|
183
|
-
{"detail": "IP bloqueada por actividad sospechosa", "ip": ip},
|
|
184
|
-
status=403,
|
|
185
|
-
)
|
|
186
|
-
else:
|
|
187
|
-
# no bloqueo todavía: permitir continuar pero devolver alerta en header (opcional)
|
|
188
|
-
# Puedes añadir header para que la vista/log lo capture
|
|
189
|
-
request.META["X-SQLI-ALERT"] = json.dumps(event)
|
|
190
|
-
return None
|
|
191
|
-
|
|
192
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|