GuardianUnivalle-Benito-Yucra 0.1.61__tar.gz → 0.1.63__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.

Files changed (25) hide show
  1. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py +47 -44
  2. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +26 -63
  3. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra.egg-info/PKG-INFO +1 -1
  4. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/PKG-INFO +1 -1
  5. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/pyproject.toml +1 -1
  6. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/__init__.py +0 -0
  7. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py +0 -0
  8. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py +0 -0
  9. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py +0 -0
  10. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/criptografia/kdf.py +0 -0
  11. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py +0 -0
  12. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/detectores/detector_keylogger.py +0 -0
  13. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +0 -0
  14. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py +0 -0
  15. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py +0 -0
  16. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py +0 -0
  17. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py +0 -0
  18. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra/utilidades.py +0 -0
  19. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra.egg-info/SOURCES.txt +0 -0
  20. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra.egg-info/dependency_links.txt +0 -0
  21. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra.egg-info/requires.txt +0 -0
  22. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/GuardianUnivalle_Benito_Yucra.egg-info/top_level.txt +0 -0
  23. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/LICENSE +0 -0
  24. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/README.md +0 -0
  25. {guardianunivalle_benito_yucra-0.1.61 → guardianunivalle_benito_yucra-0.1.63}/setup.cfg +0 -0
@@ -1,4 +1,4 @@
1
- # CSRF defense (parche recomendado)
1
+ # CSRF defense (versión reforzada)
2
2
  from __future__ import annotations
3
3
  import secrets
4
4
  import logging
@@ -21,23 +21,24 @@ CSRF_HEADER_NAMES = ("HTTP_X_CSRFTOKEN", "HTTP_X_CSRF_TOKEN")
21
21
  CSRF_COOKIE_NAME = getattr(settings, "CSRF_COOKIE_NAME", "csrftoken")
22
22
  POST_FIELD_NAME = "csrfmiddlewaretoken"
23
23
 
24
- # Nota: NO consideramos 'application/json' sospechoso aquí por defecto,
25
- # porque muchas APIs legítimas usan JSON.
24
+ # Patrón de Content-Type sospechoso
26
25
  SUSPICIOUS_CT_PATTERNS = [
27
26
  re.compile(r"text/plain", re.I),
28
27
  re.compile(r"application/x-www-form-urlencoded", re.I),
29
28
  re.compile(r"multipart/form-data", re.I),
30
29
  ]
31
30
 
32
- # Umbral minimo de "señales" para marcar como ataque (configurable)
31
+ # Parámetros sensibles típicos de CSRF
32
+ SENSITIVE_PARAMS = [
33
+ "password", "csrfmiddlewaretoken", "token", "amount", "transfer", "delete", "update"
34
+ ]
35
+
33
36
  CSRF_DEFENSE_MIN_SIGNALS = getattr(settings, "CSRF_DEFENSE_MIN_SIGNALS", 1)
34
- # Opción para excluir rutas de API que manejan JSON (cambia según tu proyecto)
35
37
  CSRF_DEFENSE_EXCLUDED_API_PREFIXES = getattr(settings, "CSRF_DEFENSE_EXCLUDED_API_PREFIXES", ["/api/"])
36
38
 
37
39
  def get_client_ip(request):
38
40
  x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
39
41
  if x_forwarded_for:
40
- # toma la primera IP real
41
42
  ips = [ip.strip() for ip in x_forwarded_for.split(",") if ip.strip()]
42
43
  if ips:
43
44
  return ips[0]
@@ -63,25 +64,22 @@ def origin_matches_host(request) -> bool:
63
64
  referer = request.META.get("HTTP_REFERER", "")
64
65
  origin_host = host_from_header(origin)
65
66
  referer_host = host_from_header(referer)
66
- # bloquear obvious javascript: referers
67
+ # Bloquear obvious javascript: referers
67
68
  if any(re.search(r"(javascript:|<script|data:text/html)", h or "", re.I) for h in [origin, referer]):
68
69
  return False
69
70
  if origin_host and origin_host == host:
70
71
  return True
71
72
  if referer_host and referer_host == host:
72
73
  return True
73
- # si no hay origin ni referer, lo consideramos neutral (no marcar)
74
74
  if not origin and not referer:
75
75
  return True
76
76
  return False
77
77
 
78
78
  def has_csrf_token(request) -> bool:
79
- # busca header, cookie o campo form
80
79
  for h in CSRF_HEADER_NAMES:
81
80
  if request.META.get(h):
82
81
  return True
83
- cookie_val = request.COOKIES.get(CSRF_COOKIE_NAME)
84
- if cookie_val:
82
+ if request.COOKIES.get(CSRF_COOKIE_NAME):
85
83
  return True
86
84
  try:
87
85
  if request.method == "POST" and hasattr(request, "POST"):
@@ -106,12 +104,25 @@ def extract_payload_text(request) -> str:
106
104
  parts.append(request.META.get("HTTP_REFERER", ""))
107
105
  return " ".join([p for p in parts if p])
108
106
 
107
+ def extract_parameters(request) -> List[str]:
108
+ params = []
109
+ if hasattr(request, "POST"):
110
+ params.extend(request.POST.keys())
111
+ if hasattr(request, "GET"):
112
+ params.extend(request.GET.keys())
113
+ try:
114
+ if request.body and "application/json" in (request.META.get("CONTENT_TYPE") or ""):
115
+ data = json.loads(request.body)
116
+ params.extend(data.keys())
117
+ except Exception:
118
+ pass
119
+ return params
120
+
109
121
  class CSRFDefenseMiddleware(MiddlewareMixin):
110
122
  def process_request(self, request):
111
- # 1) Excluir APIs JSON si se configuró así
123
+ # Excluir APIs JSON si se configuró así
112
124
  for prefix in CSRF_DEFENSE_EXCLUDED_API_PREFIXES:
113
125
  if request.path.startswith(prefix):
114
- # debug log opcional
115
126
  logger.debug(f"[CSRFDefense] Skip analysis for API prefix {prefix} path {request.path}")
116
127
  return None
117
128
 
@@ -130,6 +141,7 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
130
141
 
131
142
  descripcion: List[str] = []
132
143
  payload = extract_payload_text(request)
144
+ params = extract_parameters(request)
133
145
 
134
146
  # 1) Falta token CSRF
135
147
  if not has_csrf_token(request):
@@ -139,24 +151,33 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
139
151
  if not origin_matches_host(request):
140
152
  descripcion.append("Origin/Referer no coinciden con Host (posible cross-site)")
141
153
 
142
- # 3) Content-Type sospechoso (solo marcaremos si coincide uno de los patterns)
154
+ # 3) Content-Type sospechoso
143
155
  content_type = (request.META.get("CONTENT_TYPE") or "")
144
156
  for patt in SUSPICIOUS_CT_PATTERNS:
145
157
  if patt.search(content_type):
146
158
  descripcion.append(f"Content-Type sospechoso: {content_type}")
147
159
  break
148
160
 
149
- # 4) Referer ausente y sin header CSRF
161
+ # 4) Referer ausente y sin token CSRF
150
162
  referer = request.META.get("HTTP_REFERER", "")
151
163
  if not referer and not any(request.META.get(h) for h in CSRF_HEADER_NAMES):
152
164
  descripcion.append("Referer ausente y sin X-CSRFToken")
153
165
 
154
- # Si señales >= umbral entonces marcamos para auditoría
166
+ # 5) Parámetros sensibles en GET/JSON
167
+ for p in params:
168
+ if p.lower() in SENSITIVE_PARAMS and method == "GET":
169
+ descripcion.append(f"Parámetro sensible '{p}' enviado en GET (posible CSRF)")
170
+
171
+ # 6) JSON sospechoso desde dominio externo
172
+ if "application/json" in content_type:
173
+ origin = request.META.get("HTTP_ORIGIN") or ""
174
+ if origin and host_from_header(origin) != (request.META.get("HTTP_HOST") or "").split(":")[0]:
175
+ descripcion.append("JSON POST desde origen externo (posible CSRF)")
176
+
177
+ # Señales >= umbral => marcar
155
178
  if descripcion and len(descripcion) >= CSRF_DEFENSE_MIN_SIGNALS:
156
179
  w_csrf = getattr(settings, "CSRF_DEFENSE_WEIGHT", 0.2)
157
- intentos_csrf = len(descripcion)
158
- s_csrf = w_csrf * intentos_csrf
159
-
180
+ s_csrf = w_csrf * len(descripcion)
160
181
  request.csrf_attack_info = {
161
182
  "ip": client_ip,
162
183
  "tipos": ["CSRF"],
@@ -164,40 +185,22 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
164
185
  "payload": payload,
165
186
  "score": s_csrf,
166
187
  }
167
-
168
188
  logger.warning(
169
189
  "CSRF detectado desde IP %s: %s ; path=%s ; Content-Type=%s ; score=%.2f",
170
190
  client_ip, descripcion, request.path, content_type, s_csrf
171
191
  )
172
192
  else:
173
- # debug útil: saber por qué NO se marcó
174
193
  if descripcion:
175
194
  logger.debug(f"[CSRFDefense] low-signals ({len(descripcion)}) not marking: {descripcion}")
176
195
 
177
196
  return None
178
197
 
179
198
  """
180
- CSRF Defense Middleware
181
- ========================
182
- Detecta y registra posibles ataques CSRF (Cross-Site Request Forgery).
183
-
184
- Algoritmos relacionados:
185
- * Uso de secreto aleatorio criptográfico (generar_token_csrf).
186
- * Validación simple por comparación (validar_token_csrf).
187
- * Integración con detección XSS/SQL Injection mediante registro unificado.
188
-
189
- Fórmula de amenaza:
190
- S_csrf = w_csrf * intentos_csrf
191
- S_csrf = 0.2 * 1
192
- """
193
-
194
-
195
- """
196
- Algoritmos relacionados:
197
- *Uso de secreto aleatorio criptográfico.
198
- *Opcionalmente derivación con PBKDF2 / Argon2 para reforzar token.
199
- Contribución a fórmula de amenaza S:
200
- S_csrf = w_csrf * intentos_csrf
201
- S_csrf = 0.2 * 1
202
- donde w_csrf es peso asignado a CSRF y intentos_csrf es la cantidad de intentos detectados.
199
+ CSRF Defense Middleware - Reforzado
200
+ ===================================
201
+ - Detecta múltiples categorías de CSRF: clásico, login, logout, password change, file/action, JSON API.
202
+ - Escanea payloads POST, GET y JSON.
203
+ - Detecta parámetros sensibles enviados en GET o JSON desde origen externo.
204
+ - Scoring configurable y logging detallado.
205
+ - Fácil integración con auditoría XSS/SQLi.
203
206
  """
@@ -10,6 +10,9 @@ import urllib.parse
10
10
  import html
11
11
  from typing import List, Tuple, Dict
12
12
 
13
+ # ----------------------------
14
+ # Configuración del logger
15
+ # ----------------------------
13
16
  logger = logging.getLogger("sqlidefense")
14
17
  logger.setLevel(logging.INFO)
15
18
  if not logger.handlers:
@@ -18,7 +21,7 @@ if not logger.handlers:
18
21
  logger.addHandler(handler)
19
22
 
20
23
  # =====================================================
21
- # ===        PATRONES DE ATAQUE SQL DEFINIDOS       ===
24
+ # === PATRONES DE ATAQUE SQL DEFINIDOS ===
22
25
  # =====================================================
23
26
  SQL_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
24
27
  # ------------------ In‑Band / Exfiltration (muy alto) ------------------
@@ -50,7 +53,7 @@ SQL_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
50
53
  # ------------------ Metadata / Recon (alto) ------------------
51
54
  (re.compile(r"\binformation_schema\b", re.I), "INFORMATION_SCHEMA (recon meta‑datos)", 0.92),
52
55
  (re.compile(r"\b(information_schema\.tables|information_schema\.columns)\b", re.I), "INFORMATION_SCHEMA.tables/columns", 0.92),
53
- (re.compile(r"\b(sys\.tables|sys\.objects|sys\.databases|pg_catalog|pg_tables|pg_user)\b", re.I), "Catalogos del sistema (MSSQL/Postgres)", 0.9),
56
+ (re.compile(r"\b(sys\.tables|sys\.objects|sys\.databases|pg_catalog|pg_tables|pg_user)\b", re.I), "Catálogos del sistema (MSSQL/Postgres)", 0.9),
54
57
 
55
58
  # ------------------ DML/DDL Destructivo (alto) ------------------
56
59
  (re.compile(r"\b(drop\s+table|truncate\s+table|drop\s+database|drop\s+schema)\b", re.I), "DROP/TRUNCATE (DDL destructivo)", 0.95),
@@ -107,27 +110,23 @@ SQL_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
107
110
  (re.compile(r"\b(or)\b\s+1\s*=\s*1\b", re.I), "OR 1=1 tautology", 0.85),
108
111
  ]
109
112
 
110
-
111
113
  IGNORED_FIELDS = ["password", "csrfmiddlewaretoken", "token", "auth"]
112
114
 
115
+ # ----------------------------
116
+ # Obtener IP real del cliente
117
+ # ----------------------------
113
118
  def get_client_ip(request):
114
- """
115
- Obtiene la IP real del cliente.
116
- Primero revisa 'X-Forwarded-For', luego 'REMOTE_ADDR'.
117
- """
118
119
  x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
119
120
  if x_forwarded_for:
120
- # Render y otros proxies envían múltiples IPs separados por coma
121
121
  ips = [ip.strip() for ip in x_forwarded_for.split(",") if ip.strip()]
122
122
  if ips:
123
- return ips[0] # la primera IP es la IP real del cliente
124
- # Si no hay X-Forwarded-For, tomar REMOTE_ADDR
123
+ return ips[0]
125
124
  return request.META.get("REMOTE_ADDR", "")
126
125
 
127
-
128
-
126
+ # ----------------------------
127
+ # Extraer payload de la solicitud
128
+ # ----------------------------
129
129
  def extract_payload(request):
130
- """Extrae datos útiles de la solicitud para análisis."""
131
130
  parts = []
132
131
  try:
133
132
  if "application/json" in request.META.get("CONTENT_TYPE", ""):
@@ -146,50 +145,29 @@ def extract_payload(request):
146
145
 
147
146
  return " ".join(parts)
148
147
 
149
-
150
148
  # ----------------------------
151
149
  # Normalización / preprocesamiento
152
150
  # ----------------------------
153
151
  def normalize_input(s: str) -> str:
154
- """
155
- Normaliza la entrada:
156
- - decode URL encoding
157
- - unescape HTML entities
158
- - sustituye escapes de hex (\xNN) por su literal
159
- - colapsa múltiples espacios y newlines
160
- - lower() opcional (NO se hace porque algunos patrones usan case-insensitive)
161
- """
162
152
  if not s:
163
153
  return ""
164
154
  try:
165
- # URL decode
166
155
  s_dec = urllib.parse.unquote_plus(s)
167
156
  except Exception:
168
157
  s_dec = s
169
158
  try:
170
- # Unescape HTML entities
171
159
  s_dec = html.unescape(s_dec)
172
160
  except Exception:
173
161
  pass
174
- # Replace \xNN sequences visually (keep raw as text; we don't convert binary)
175
- s_dec = re.sub(r"\\x([0-9a-fA-F]{2})", r"\\x\1", s_dec)
176
- # Collapse whitespace
162
+ # Reemplazo seguro de secuencias \xNN
163
+ s_dec = re.sub(r"\\x([0-9a-fA-F]{2})", r"\\x\g<1>", s_dec)
177
164
  s_dec = re.sub(r"\s+", " ", s_dec)
178
165
  return s_dec.strip()
166
+
179
167
  # ----------------------------
180
- # Detector: retorna score, matches y detalles
168
+ # Detector SQLi
181
169
  # ----------------------------
182
170
  def detect_sql_injection(text: str) -> Dict:
183
- """
184
- Detecta coincidencias con SQL_PATTERNS en el texto normalizado.
185
- Devuelve:
186
- {
187
- "score": float,
188
- "matches": [("Etiqueta", "regex pattern string", weight), ...],
189
- "descriptions": [ "Etiqueta", ... ],
190
- "sample": excerpt (primeros 1200 chars)
191
- }
192
- """
193
171
  norm = normalize_input(text or "")
194
172
  score = 0.0
195
173
  matches = []
@@ -203,17 +181,10 @@ def detect_sql_injection(text: str) -> Dict:
203
181
  return {
204
182
  "score": round(score, 3),
205
183
  "matches": matches,
206
- "descriptions": list(dict.fromkeys(descriptions)), # unique
184
+ "descriptions": list(dict.fromkeys(descriptions)),
207
185
  "sample": norm[:1200],
208
186
  }
209
187
 
210
- # ----------------------------
211
- # Umbrales sugeridos
212
- # ----------------------------
213
- # - >= 1.8 : ALTA (bloquear + registrar)
214
- # - 1.0 - <1.8 : MEDIA (registrar, desafío/2FA, throttling)
215
- # - 0.5 - <1.0 : BAJA (registrar, monitorear)
216
- # - <0.5 : INFORMATIVO (log heurístico)
217
188
  DEFAULT_THRESHOLDS = {
218
189
  "HIGH": 1.8,
219
190
  "MEDIUM": 1.0,
@@ -221,17 +192,9 @@ DEFAULT_THRESHOLDS = {
221
192
  }
222
193
 
223
194
  # ----------------------------
224
- # Ejemplo de uso (snippet)
195
+ # Middleware SQLi
225
196
  # ----------------------------
226
- # payload = extract_payload(request) # tu función actual
227
- # result = detect_sql_injection(payload)
228
- # if result["score"] >= DEFAULT_THRESHOLDS["HIGH"]:
229
- # bloquear_y_registrar()
230
- # else:
231
- # registrar_solo()
232
197
  class SQLIDefenseMiddleware(MiddlewareMixin):
233
- """Middleware de detección SQL Injection."""
234
-
235
198
  def process_request(self, request):
236
199
  client_ip = get_client_ip(request)
237
200
  trusted_ips = getattr(settings, "SQLI_DEFENSE_TRUSTED_IPS", [])
@@ -246,35 +209,35 @@ class SQLIDefenseMiddleware(MiddlewareMixin):
246
209
  return None
247
210
 
248
211
  payload = extract_payload(request)
249
- score, descripciones = detect_sql_injection(payload)
212
+ result = detect_sql_injection(payload)
213
+ score = result["score"]
214
+ descripciones = result["descriptions"]
250
215
 
251
216
  if score == 0:
252
217
  return None
253
218
 
254
- # Registrar ataque completo
219
+ # Registrar ataque
255
220
  logger.warning(
256
221
  f"[SQLiDetect] IP={client_ip} Host={host} Referer={referer} "
257
222
  f"Score={score:.2f} Desc={descripciones} Payload={payload[:500]}"
258
223
  )
259
224
 
260
- # Guardar información del ataque en el request
225
+ # Guardar info en request
261
226
  request.sql_attack_info = {
262
227
  "ip": client_ip,
263
228
  "tipos": ["SQLi"],
264
229
  "descripcion": descripciones,
265
- "payload": payload[:1000], # guardar hasta 1000 caracteres
230
+ "payload": payload[:1000],
266
231
  "score": round(score, 2),
267
- "url": request.build_absolute_uri(), # registrar URL completa
232
+ "url": request.build_absolute_uri(),
268
233
  }
269
234
 
270
235
  return None
271
236
 
272
-
273
-
274
237
  # =====================================================
275
238
  # === INFORMACIÓN EXTRA ===
276
239
  # =====================================================
277
- """
240
+ r"""
278
241
  Algoritmos relacionados:
279
242
  - Se recomienda almacenar logs SQLi cifrados (AES-GCM)
280
243
  para proteger evidencia de intentos maliciosos.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GuardianUnivalle-Benito-Yucra
3
- Version: 0.1.61
3
+ Version: 0.1.63
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.61
3
+ Version: 0.1.63
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.61"
7
+ version = "0.1.63"
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" }