GuardianUnivalle-Benito-Yucra 0.1.28__py3-none-any.whl → 0.1.30__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.

Potentially problematic release.


This version of GuardianUnivalle-Benito-Yucra might be problematic. Click here for more details.

@@ -1,15 +1,233 @@
1
+ """
2
+ CSRF Defense Middleware
3
+ ========================
4
+ Detecta y registra posibles ataques CSRF (Cross-Site Request Forgery).
5
+
6
+ Algoritmos relacionados:
7
+ * Uso de secreto aleatorio criptográfico (generar_token_csrf).
8
+ * Validación simple por comparación (validar_token_csrf).
9
+ * Contribución a fórmula de amenaza S:
10
+ S_csrf = w_csrf * intentos_csrf
11
+ S_csrf = 0.2 * 1
12
+ """
13
+
14
+ from __future__ import annotations
1
15
  import secrets
2
- from ..auditoria.registro_auditoria import registrar_evento
16
+ import logging
17
+ import re
18
+ import json
19
+ from typing import List
20
+ from urllib.parse import urlparse
21
+ from django.conf import settings
22
+ from django.utils.deprecation import MiddlewareMixin
23
+
24
+ # ======================================================
25
+ # === CONFIGURACIÓN DE LOGGER ===
26
+ # ======================================================
27
+ logger = logging.getLogger("csrfdefense")
28
+ logger.setLevel(logging.INFO)
29
+ if not logger.handlers:
30
+ handler = logging.StreamHandler()
31
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
32
+ logger.addHandler(handler)
33
+
34
+
35
+ # ======================================================
36
+ # === FUNCIONES AUXILIARES DE TOKEN CSRF ===
37
+ # ======================================================
38
+ def registrar_evento(tipo: str, mensaje: str):
39
+ """Registra eventos importantes en los logs."""
40
+ logger.warning(f"[{tipo}] {mensaje}")
41
+
3
42
 
4
43
  def generar_token_csrf() -> str:
5
- return secrets.token_hex(32)
44
+ """Genera un token CSRF seguro."""
45
+ token = secrets.token_hex(32)
46
+ registrar_evento("CSRF", "Token CSRF generado")
47
+ return token
48
+
6
49
 
7
50
  def validar_token_csrf(token: str, token_sesion: str) -> bool:
51
+ """Valida que el token recibido coincida con el token en sesión."""
8
52
  valido = token == token_sesion
9
53
  if not valido:
10
- registrar_evento("CSRF", "Intento de CSRF detectado")
54
+ registrar_evento("CSRF", "Intento de CSRF detectado (token no coincide)")
11
55
  return valido
12
56
 
57
+
58
+ # ======================================================
59
+ # === CONSTANTES Y CONFIGURACIONES ===
60
+ # ======================================================
61
+ STATE_CHANGING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
62
+ CSRF_HEADER_NAMES = (
63
+ "HTTP_X_CSRFTOKEN",
64
+ "HTTP_X_CSRF_TOKEN",
65
+ )
66
+ CSRF_COOKIE_NAME = getattr(settings, "CSRF_COOKIE_NAME", "csrftoken")
67
+ POST_FIELD_NAME = "csrfmiddlewaretoken"
68
+
69
+ SUSPICIOUS_CT_PATTERNS = [
70
+ re.compile(r"application/x-www-form-urlencoded", re.I),
71
+ re.compile(r"multipart/form-data", re.I),
72
+ ]
73
+
74
+
75
+ # ======================================================
76
+ # === FUNCIONES DE APOYO ===
77
+ # ======================================================
78
+ def get_client_ip(request):
79
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
80
+ if x_forwarded_for:
81
+ return x_forwarded_for.split(",")[0].strip()
82
+ return request.META.get("REMOTE_ADDR", "")
83
+
84
+
85
+ def host_from_header(header_value: str) -> str | None:
86
+ if not header_value:
87
+ return None
88
+ try:
89
+ parsed = urlparse(header_value)
90
+ if parsed.netloc:
91
+ return parsed.netloc.split(":")[0]
92
+ return header_value.split(":")[0]
93
+ except Exception:
94
+ return None
95
+
96
+
97
+ def origin_matches_host(request) -> bool:
98
+ """Verifica si Origin/Referer coinciden con Host."""
99
+ host_header = request.META.get("HTTP_HOST") or request.META.get("SERVER_NAME")
100
+ if not host_header:
101
+ return True
102
+
103
+ host = host_header.split(":")[0]
104
+ origin = request.META.get("HTTP_ORIGIN", "")
105
+ referer = request.META.get("HTTP_REFERER", "")
106
+
107
+ origin_host = host_from_header(origin)
108
+ referer_host = host_from_header(referer)
109
+
110
+ if origin_host and origin_host == host:
111
+ return True
112
+ if referer_host and referer_host == host:
113
+ return True
114
+ if not origin and not referer:
115
+ return True
116
+
117
+ return False
118
+
119
+
120
+ def has_csrf_token(request) -> bool:
121
+ """Comprueba si hay signos de token CSRF presente."""
122
+ for h in CSRF_HEADER_NAMES:
123
+ if request.META.get(h):
124
+ return True
125
+
126
+ cookie_val = request.COOKIES.get(CSRF_COOKIE_NAME)
127
+ if cookie_val:
128
+ return True
129
+
130
+ try:
131
+ if request.method == "POST" and hasattr(request, "POST"):
132
+ if request.POST.get(POST_FIELD_NAME):
133
+ return True
134
+ except Exception:
135
+ pass
136
+
137
+ return False
138
+
139
+
140
+ def extract_payload_text(request) -> str:
141
+ """Extrae contenido útil de la solicitud para análisis."""
142
+ parts: List[str] = []
143
+ try:
144
+ body = request.body.decode("utf-8", errors="ignore")
145
+ if body:
146
+ parts.append(body)
147
+ except Exception:
148
+ pass
149
+ qs = request.META.get("QUERY_STRING", "")
150
+ if qs:
151
+ parts.append(qs)
152
+ parts.append(request.META.get("HTTP_USER_AGENT", ""))
153
+ parts.append(request.META.get("HTTP_REFERER", ""))
154
+ return " ".join([p for p in parts if p])
155
+
156
+
157
+ # ======================================================
158
+ # === MIDDLEWARE DE DEFENSA CSRF ===
159
+ # ======================================================
160
+ class CSRFDefenseMiddleware(MiddlewareMixin):
161
+ """
162
+ Middleware para DETECTAR intentos de CSRF:
163
+ - Marca request.sql_attack_info con 'tipos': ['CSRF'] y 'descripcion' con razones.
164
+ - No bloquea la petición directamente, permite que AuditoriaMiddleware lo maneje.
165
+ """
166
+
167
+ def process_request(self, request):
168
+ client_ip = get_client_ip(request)
169
+ trusted_ips = getattr(settings, "CSRF_DEFENSE_TRUSTED_IPS", [])
170
+ if client_ip in trusted_ips:
171
+ return None
172
+
173
+ excluded_paths = getattr(settings, "CSRF_DEFENSE_EXCLUDED_PATHS", [])
174
+ if any(request.path.startswith(p) for p in excluded_paths):
175
+ return None
176
+
177
+ method = (request.method or "").upper()
178
+ if method not in STATE_CHANGING_METHODS:
179
+ return None
180
+
181
+ descripcion: List[str] = []
182
+ payload = extract_payload_text(request)
183
+
184
+ # 1) Falta token CSRF
185
+ if not has_csrf_token(request):
186
+ descripcion.append("Falta token CSRF en cookie/header/form")
187
+
188
+ # 2) Origin/Referer no coinciden
189
+ if not origin_matches_host(request):
190
+ descripcion.append(
191
+ "Origin/Referer no coinciden con Host (posible cross-site)"
192
+ )
193
+
194
+ # 3) Content-Type sospechoso
195
+ content_type = request.META.get("CONTENT_TYPE", "") or ""
196
+ for patt in SUSPICIOUS_CT_PATTERNS:
197
+ if patt.search(content_type):
198
+ descripcion.append(f"Content-Type sospechoso: {content_type}")
199
+ break
200
+
201
+ # 4) Referer ausente
202
+ referer = request.META.get("HTTP_REFERER", "")
203
+ if not referer and not any(request.META.get(h) for h in CSRF_HEADER_NAMES):
204
+ descripcion.append("Referer ausente y sin X-CSRFToken")
205
+
206
+ # Si hay señales, calculamos puntaje y registramos
207
+ if descripcion:
208
+ w_csrf = getattr(settings, "CSRF_DEFENSE_WEIGHT", 0.2)
209
+ intentos_csrf = len(descripcion)
210
+ s_csrf = w_csrf * intentos_csrf
211
+
212
+ request.sql_attack_info = {
213
+ "ip": client_ip,
214
+ "tipos": ["CSRF"],
215
+ "descripcion": descripcion,
216
+ "payload": payload,
217
+ "score": s_csrf,
218
+ }
219
+
220
+ logger.warning(
221
+ "CSRF detectado desde IP %s: %s ; payload: %.200s ; score: %.2f",
222
+ client_ip,
223
+ descripcion,
224
+ payload,
225
+ s_csrf,
226
+ )
227
+
228
+ return None
229
+
230
+
13
231
  """
14
232
  Algoritmos relacionados:
15
233
  *Uso de secreto aleatorio criptográfico.
@@ -18,4 +236,4 @@ Contribución a fórmula de amenaza S:
18
236
  S_csrf = w_csrf * intentos_csrf
19
237
  S_csrf = 0.2 * 1
20
238
  donde w_csrf es peso asignado a CSRF y intentos_csrf es la cantidad de intentos detectados.
21
- """
239
+ """
@@ -1,11 +1,160 @@
1
+ """
2
+ Detector de ataques de tipo DoS (Denial of Service)
3
+ ====================================================
4
+
5
+ Este módulo forma parte del sistema de detección de amenazas.
6
+ Detecta tasas de petición anómalas en base a límites configurables,
7
+ captura datos del atacante (IP, agente, cabeceras)
8
+ y registra los incidentes para su auditoría.
9
+
10
+ Componentes:
11
+ - DOSDefenseMiddleware: Middleware principal de detección.
12
+ - detectar_dos(): Evalúa si la tasa supera el umbral permitido.
13
+ - calcular_nivel_amenaza_dos(): Calcula la severidad proporcional.
14
+ - registrar_evento(): Registra los incidentes en auditoría.
15
+
16
+ Algoritmos relacionados:
17
+ * Rate Limiting basado en ventana deslizante.
18
+ * Cálculo de score: S_dos = w_dos * (tasa_peticion / limite)
19
+ """
20
+
21
+ from __future__ import annotations
22
+ import time
23
+ import logging
24
+ import json
25
+ from typing import Dict, List
26
+ from django.conf import settings
27
+ from django.utils.deprecation import MiddlewareMixin
1
28
  from ..mitigacion.limitador_peticion import limitar_peticion
2
29
  from ..auditoria.registro_auditoria import registrar_evento
3
30
 
4
- def detectar_dos(tasa_peticion: int, limite: int = 100) -> bool:
31
+ # =====================================================
32
+ # === CONFIGURACIÓN DEL LOGGER ===
33
+ # =====================================================
34
+ logger = logging.getLogger("dosdefense")
35
+ logger.setLevel(logging.INFO)
36
+ if not logger.handlers:
37
+ handler = logging.StreamHandler()
38
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
39
+ logger.addHandler(handler)
40
+
41
+
42
+ # =====================================================
43
+ # === PARÁMETROS DE CONFIGURACIÓN BASE ===
44
+ # =====================================================
45
+ LIMITE_PETICIONES = getattr(settings, "DOS_LIMITE_PETICIONES", 100) # por minuto
46
+ VENTANA_SEGUNDOS = getattr(settings, "DOS_VENTANA_SEGUNDOS", 60)
47
+ PESO_DOS = getattr(settings, "DOS_PESO", 0.6)
48
+
49
+
50
+ # =====================================================
51
+ # === REGISTRO TEMPORAL DE SOLICITUDES POR IP ===
52
+ # =====================================================
53
+ # En producción se recomienda usar Redis o Memcached
54
+ # para este tipo de conteo, aquí usamos una memoria temporal.
55
+ REGISTRO_SOLICITUDES: Dict[str, List[float]] = {}
56
+
57
+
58
+ # =====================================================
59
+ # === FUNCIONES AUXILIARES ===
60
+ # =====================================================
61
+ def get_client_ip(request) -> str:
62
+ """Obtiene la IP real del cliente (considera proxies)."""
63
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
64
+ if x_forwarded_for:
65
+ return x_forwarded_for.split(",")[0].strip()
66
+ return request.META.get("REMOTE_ADDR", "")
67
+
68
+
69
+ def limpiar_registro(ip: str):
70
+ """Limpia peticiones antiguas fuera de la ventana de tiempo."""
71
+ ahora = time.time()
72
+ REGISTRO_SOLICITUDES[ip] = [
73
+ t for t in REGISTRO_SOLICITUDES.get(ip, []) if ahora - t < VENTANA_SEGUNDOS
74
+ ]
75
+
76
+
77
+ def calcular_nivel_amenaza_dos(
78
+ tasa_peticion: int, limite: int = LIMITE_PETICIONES
79
+ ) -> float:
80
+ """
81
+ Calcula la puntuación de amenaza DoS basada en el peso configurado.
82
+ Fórmula: S_dos = w_dos * (tasa_peticion / limite)
83
+ """
84
+ proporcion = tasa_peticion / limite
85
+ s_dos = PESO_DOS * proporcion
86
+ return round(min(s_dos, 1.0), 3)
87
+
88
+
89
+ def detectar_dos(ip: str, tasa_peticion: int, limite: int = LIMITE_PETICIONES) -> bool:
90
+ """Evalúa si la tasa de peticiones excede el umbral permitido."""
5
91
  if tasa_peticion > limite:
6
- registrar_evento("DoS", f"Tasa de petición elevada: {tasa_peticion}")
92
+ registrar_evento(
93
+ tipo="DoS",
94
+ descripcion=f"Alta tasa de peticiones desde {ip}: {tasa_peticion} req/min (límite {limite})",
95
+ severidad="ALTA",
96
+ )
97
+ limitar_peticion() # Acción de mitigación
7
98
  return True
8
99
  return False
100
+
101
+
102
+ # =====================================================
103
+ # === MIDDLEWARE DE DETECCIÓN DE DoS ===
104
+ # =====================================================
105
+ class DOSDefenseMiddleware(MiddlewareMixin):
106
+ """
107
+ Middleware para detección y registro de ataques DoS.
108
+ - Captura IP, agente y cabeceras sospechosas.
109
+ - Evalúa la frecuencia de peticiones por IP.
110
+ - Marca request.sql_attack_info con información del intento.
111
+ """
112
+
113
+ def process_request(self, request):
114
+ client_ip = get_client_ip(request)
115
+ user_agent = request.META.get("HTTP_USER_AGENT", "Desconocido")
116
+ referer = request.META.get("HTTP_REFERER", "")
117
+ path = request.path
118
+
119
+ # Limpieza de registro anterior
120
+ limpiar_registro(client_ip)
121
+
122
+ # Registrar nueva petición
123
+ REGISTRO_SOLICITUDES.setdefault(client_ip, []).append(time.time())
124
+ tasa = len(REGISTRO_SOLICITUDES[client_ip])
125
+ nivel = calcular_nivel_amenaza_dos(tasa)
126
+ es_dos = detectar_dos(client_ip, tasa)
127
+
128
+ if es_dos:
129
+ descripcion = [
130
+ f"Tasa de {tasa} req/min excede límite {LIMITE_PETICIONES}",
131
+ f"User-Agent: {user_agent}",
132
+ f"Referer: {referer or 'N/A'}",
133
+ f"Ruta: {path}",
134
+ ]
135
+
136
+ # Log profesional
137
+ logger.warning(
138
+ "DoS detectado desde IP %s: %s ; nivel: %.2f",
139
+ client_ip,
140
+ descripcion,
141
+ nivel,
142
+ )
143
+
144
+ # Enviar a sistema de auditoría
145
+ request.sql_attack_info = {
146
+ "ip": client_ip,
147
+ "tipos": ["DoS"],
148
+ "descripcion": descripcion,
149
+ "payload": json.dumps(
150
+ {"user_agent": user_agent, "referer": referer, "path": path}
151
+ ),
152
+ "score": nivel,
153
+ }
154
+
155
+ return None
156
+
157
+
9
158
  """
10
159
  Algoritmos relacionados:
11
160
  *Rate Limiting, listas de bloqueo.
@@ -14,4 +163,4 @@ Contribución a fórmula de amenaza S:
14
163
  S_dos = w_dos * (tasa_peticion / limite)
15
164
  S_dos = 0.6 * (150 / 100)
16
165
  donde w_dos es peso asignado a DoS y tasa_peticion / limite es la proporción de la tasa actual sobre el límite.
17
- """
166
+ """
@@ -1,13 +1,172 @@
1
+ """
2
+ Detector extendido de Keyloggers
3
+ ================================
4
+
5
+ Módulo avanzado de detección de keyloggers y software espía en el sistema.
6
+ Incluye revisión de procesos activos, archivos ejecutables sospechosos y
7
+ aplicaciones instaladas en el sistema operativo Windows.
8
+
9
+ Componentes:
10
+ - Escaneo de procesos activos.
11
+ - Detección de archivos con extensiones críticas (.exe, .dll, .scr, .bat, .cmd, .msi).
12
+ - Revisión de aplicaciones instaladas (si se ejecuta en Windows).
13
+ - Cálculo de nivel de amenaza y registro de auditoría.
14
+
15
+ Algoritmos:
16
+ * Revisión de procesos (psutil)
17
+ * Análisis de archivos con extensiones críticas
18
+ * Detección de software instalado
19
+ * Registro cifrado con AES-256 + SHA-512
20
+ * Fórmula: S_keylogger = w_keylogger * (procesos + archivos + instalaciones)
21
+ """
22
+
23
+ from __future__ import annotations
1
24
  import psutil
25
+ import os
26
+ import logging
27
+ import platform
28
+ import subprocess
29
+ from typing import List, Dict
30
+ from django.conf import settings
2
31
  from ..auditoria.registro_auditoria import registrar_evento
3
32
 
4
- def detectar_keylogger():
5
- sospechosos = []
6
- for proc in psutil.process_iter(['pid', 'name']):
7
- if "keylogger" in proc.info['name'].lower():
8
- sospechosos.append(proc.info)
9
- registrar_evento("Keylogger", f"Proceso sospechoso: {proc.info}")
10
- return sospechosos
33
+ # =====================================================
34
+ # === CONFIGURACIÓN DEL LOGGER ===
35
+ # =====================================================
36
+ logger = logging.getLogger("keyloggerdefense")
37
+ logger.setLevel(logging.INFO)
38
+ if not logger.handlers:
39
+ handler = logging.StreamHandler()
40
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
41
+ logger.addHandler(handler)
42
+
43
+ # =====================================================
44
+ # === CONFIGURACIÓN DE PARÁMETROS ===
45
+ # =====================================================
46
+ PESO_KEYLOGGER = getattr(settings, "KEYLOGGER_PESO", 0.4)
47
+ EXTENSIONES_SOSPECHOSAS = [".exe", ".dll", ".scr", ".bat", ".cmd", ".msi"]
48
+ CARPETAS_CRITICAS = [
49
+ "C:\\Users\\Public",
50
+ "C:\\Users\\%USERNAME%\\AppData\\Roaming",
51
+ "C:\\Users\\%USERNAME%\\AppData\\Local\\Temp",
52
+ "C:\\ProgramData",
53
+ "C:\\Windows\\Temp",
54
+ ]
55
+ PATRONES_NOMBRES = ["keylogger", "spy", "hook", "keyboard", "capture", "stealer"]
56
+
57
+
58
+ # =====================================================
59
+ # === FUNCIONES AUXILIARES ===
60
+ # =====================================================
61
+ def calcular_score_keylogger(total_items: int) -> float:
62
+ """Calcula el nivel de amenaza normalizado."""
63
+ return round(min(PESO_KEYLOGGER * total_items, 1.0), 3)
64
+
65
+
66
+ def detectar_procesos_sospechosos() -> List[Dict]:
67
+ """Escanea procesos activos y detecta posibles keyloggers."""
68
+ hallazgos = []
69
+ for proc in psutil.process_iter(["pid", "name", "exe"]):
70
+ try:
71
+ nombre = proc.info.get("name", "").lower()
72
+ if any(pat in nombre for pat in PATRONES_NOMBRES):
73
+ hallazgos.append(proc.info)
74
+ registrar_evento("Keylogger", f"Proceso sospechoso: {proc.info}")
75
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
76
+ continue
77
+ return hallazgos
78
+
79
+
80
+ def detectar_archivos_sospechosos() -> List[str]:
81
+ """
82
+ Busca archivos con extensiones peligrosas y nombres relacionados
83
+ a keyloggers en carpetas críticas del sistema.
84
+ """
85
+ hallazgos = []
86
+ for base in CARPETAS_CRITICAS:
87
+ base = os.path.expandvars(base) # reemplaza %USERNAME%
88
+ if not os.path.exists(base):
89
+ continue
90
+ for root, _, files in os.walk(base):
91
+ for file in files:
92
+ if any(file.lower().endswith(ext) for ext in EXTENSIONES_SOSPECHOSAS):
93
+ if any(pat in file.lower() for pat in PATRONES_NOMBRES):
94
+ ruta = os.path.join(root, file)
95
+ hallazgos.append(ruta)
96
+ registrar_evento("Keylogger", f"Archivo sospechoso: {ruta}")
97
+ return hallazgos
98
+
99
+
100
+ def detectar_programas_instalados() -> List[str]:
101
+ """
102
+ Analiza programas instalados en el sistema (solo Windows)
103
+ para encontrar software potencialmente malicioso.
104
+ """
105
+ hallazgos = []
106
+ if platform.system() != "Windows":
107
+ return hallazgos
108
+
109
+ try:
110
+ salida = subprocess.check_output(
111
+ ["wmic", "product", "get", "name"], stderr=subprocess.DEVNULL
112
+ ).decode("utf-8", errors="ignore")
113
+
114
+ for linea in salida.splitlines():
115
+ nombre = linea.strip().lower()
116
+ if any(pat in nombre for pat in PATRONES_NOMBRES):
117
+ hallazgos.append(nombre)
118
+ registrar_evento("Keylogger", f"Software sospechoso: {nombre}")
119
+ except Exception as e:
120
+ logger.error("Error al listar programas instalados: %s", e)
121
+
122
+ return hallazgos
123
+
124
+
125
+ # =====================================================
126
+ # === CLASE PRINCIPAL DE DETECCIÓN ===
127
+ # =====================================================
128
+ class KEYLOGGERDefense:
129
+ """
130
+ Escanea procesos, archivos y programas para detectar keyloggers
131
+ o software espía potencialmente malicioso.
132
+ """
133
+
134
+ def ejecutar_escaneo(self):
135
+ procesos = detectar_procesos_sospechosos()
136
+ archivos = detectar_archivos_sospechosos()
137
+ programas = detectar_programas_instalados()
138
+
139
+ total_hallazgos = len(procesos) + len(archivos) + len(programas)
140
+ score = calcular_score_keylogger(total_hallazgos)
141
+
142
+ if total_hallazgos > 0:
143
+ descripcion = [
144
+ f"Procesos sospechosos: {len(procesos)}",
145
+ f"Archivos sospechosos: {len(archivos)}",
146
+ f"Programas sospechosos: {len(programas)}",
147
+ ]
148
+
149
+ logger.warning("Keylogger detectado: %s ; nivel: %.2f", descripcion, score)
150
+
151
+ evento = {
152
+ "tipo": "Keylogger",
153
+ "descripcion": descripcion,
154
+ "procesos": procesos,
155
+ "archivos": archivos,
156
+ "programas": programas,
157
+ "score": score,
158
+ }
159
+
160
+ registrar_evento(
161
+ tipo="Keylogger",
162
+ descripcion=f"Detectados {total_hallazgos} elementos sospechosos.",
163
+ severidad="ALTA" if score >= 0.5 else "MEDIA",
164
+ )
165
+
166
+ return evento
167
+ else:
168
+ logger.info("Sin procesos, archivos o programas sospechosos detectados.")
169
+ return {"tipo": "Keylogger", "descripcion": "Sin hallazgos", "score": 0.0}
11
170
 
12
171
 
13
172
  """
@@ -18,4 +177,4 @@ S_keylogger = w_keylogger * numero_procesos_sospechosos
18
177
  S_keylogger = 0.4 * 2
19
178
  donde w_keylogger es peso asignado a keyloggers y numero_procesos_sospechosos es la cantidad de procesos detectados.
20
179
 
21
- """
180
+ """
@@ -1,7 +1,15 @@
1
- from django.utils.deprecation import MiddlewareMixin
1
+ # sql_defense.py
2
+ from __future__ import annotations
3
+ import json
4
+ import logging
5
+ import re
6
+ from typing import List, Tuple
2
7
  from django.conf import settings
3
- import logging, re, json
8
+ from django.utils.deprecation import MiddlewareMixin
4
9
 
10
+ # =====================================================
11
+ # === CONFIGURACIÓN DEL LOGGER ===
12
+ # =====================================================
5
13
  logger = logging.getLogger("sqlidefense")
6
14
  logger.setLevel(logging.INFO)
7
15
  if not logger.handlers:
@@ -9,80 +17,171 @@ if not logger.handlers:
9
17
  handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
10
18
  logger.addHandler(handler)
11
19
 
12
- # 🔹 Patrones de ataque SQL
13
- PATTERNS = [
14
- (re.compile(r"\bunion\b\s+(all\s+)?\bselect\b", re.I), "UNION SELECT"),
20
+
21
+ # =====================================================
22
+ # === PATRONES DE DETECCIÓN DE SQLi ===
23
+ # =====================================================
24
+ SQLI_PATTERNS: List[Tuple[re.Pattern, str]] = [
25
+ # Inyección clásica con UNION SELECT
26
+ (re.compile(r"\bunion\b\s+(all\s+)?\bselect\b", re.I), "Uso de UNION SELECT"),
27
+ # Combinaciones OR/AND en consultas WHERE
15
28
  (
16
29
  re.compile(r"\bselect\b.*\bfrom\b.*\bwhere\b.*\b(or|and)\b.*=", re.I),
17
30
  "SELECT con OR/AND",
18
31
  ),
19
- (re.compile(r"\b(or|and)\s+\d+\s*=\s*\d+", re.I), "OR/AND 1=1"),
32
+ # Comparaciones tautológicas (1=1)
33
+ (
34
+ re.compile(r"\b(or|and)\s+\d+\s*=\s*\d+", re.I),
35
+ "Expresión tautológica OR/AND 1=1",
36
+ ),
37
+ # Manipulación de tablas
20
38
  (
21
39
  re.compile(r"\b(drop|truncate|delete|insert|update)\b", re.I),
22
- "Manipulación de tabla",
40
+ "Comando de manipulación de tabla",
23
41
  ),
42
+ # Comentarios sospechosos o terminadores
24
43
  (re.compile(r"(--|#|;)", re.I), "Comentario o terminador sospechoso"),
25
- (re.compile(r"exec\s*\(", re.I), "Ejecución de procedimiento"),
44
+ # Ejecución directa de procedimientos
45
+ (re.compile(r"exec\s*\(", re.I), "Ejecución de procedimiento almacenado"),
46
+ # Subconsultas y SELECT anidados sospechosos
47
+ (re.compile(r"\(\s*select\b.*\)", re.I), "Subconsulta sospechosa"),
26
48
  ]
27
49
 
28
50
 
29
- def extract_payload_text(request):
30
- parts = []
51
+ # =====================================================
52
+ # === FUNCIONES AUXILIARES SQLi ===
53
+ # =====================================================
54
+ def extract_payload_text(request) -> str:
55
+ """
56
+ Extrae texto de interés desde el cuerpo, querystring,
57
+ encabezados y referencias para analizar posible SQLi.
58
+ """
59
+ parts: List[str] = []
60
+
31
61
  try:
32
- if "application/json" in request.META.get("CONTENT_TYPE", ""):
33
- body_json = json.loads(request.body.decode("utf-8") or "{}")
34
- parts.append(json.dumps(body_json))
62
+ content_type = request.META.get("CONTENT_TYPE", "")
63
+ if "application/json" in content_type:
64
+ data = json.loads(request.body.decode("utf-8") or "{}")
65
+ parts.append(json.dumps(data))
35
66
  else:
36
- parts.append(request.body.decode("utf-8", errors="ignore"))
37
- except:
67
+ body = request.body.decode("utf-8", errors="ignore")
68
+ if body:
69
+ parts.append(body)
70
+ except Exception:
38
71
  pass
39
- if request.META.get("QUERY_STRING"):
40
- parts.append(request.META.get("QUERY_STRING"))
72
+
73
+ qs = request.META.get("QUERY_STRING", "")
74
+ if qs:
75
+ parts.append(qs)
76
+
41
77
  parts.append(request.META.get("HTTP_USER_AGENT", ""))
42
78
  parts.append(request.META.get("HTTP_REFERER", ""))
79
+
43
80
  return " ".join([p for p in parts if p])
44
81
 
45
82
 
46
- def detect_sql_attack(text):
47
- descripcion = []
48
- for patt, msg in PATTERNS:
83
+ def detect_sql_attack(text: str) -> Tuple[bool, List[str]]:
84
+ """
85
+ Recorre el texto buscando patrones típicos de inyección SQL.
86
+ Retorna (True, lista_de_descripciones) si se detecta algún patrón.
87
+ """
88
+ descripcion: List[str] = []
89
+
90
+ for patt, msg in SQLI_PATTERNS:
49
91
  if patt.search(text):
50
92
  descripcion.append(msg)
93
+
51
94
  return (len(descripcion) > 0, descripcion)
52
95
 
53
96
 
54
- def get_client_ip(request):
97
+ def get_client_ip(request) -> str:
98
+ """
99
+ Obtiene la IP real del cliente considerando X-Forwarded-For.
100
+ """
55
101
  x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
56
102
  if x_forwarded_for:
57
103
  return x_forwarded_for.split(",")[0].strip()
58
- return request.META.get("REMOTE_ADDR")
104
+ return request.META.get("REMOTE_ADDR", "")
59
105
 
60
106
 
107
+ # =====================================================
108
+ # === MIDDLEWARE DE DEFENSA SQLi ===
109
+ # =====================================================
61
110
  class SQLIDefenseMiddleware(MiddlewareMixin):
111
+ """
112
+ Middleware profesional de detección de inyección SQL.
113
+ - Detecta patrones en parámetros, cuerpo y cabeceras.
114
+ - No bloquea directamente; marca el intento para auditoría.
115
+ """
116
+
62
117
  def process_request(self, request):
118
+ # ---------------------------------------------
119
+ # 1. Filtrar IPs confiables
120
+ # ---------------------------------------------
63
121
  client_ip = get_client_ip(request)
64
- trusted_ips = getattr(settings, "SQLI_DEFENSE_TRUSTED_IPS", [])
65
-
122
+ trusted_ips: List[str] = getattr(settings, "SQLI_DEFENSE_TRUSTED_IPS", [])
66
123
  if client_ip in trusted_ips:
67
124
  return None
68
125
 
69
- text = extract_payload_text(request)
70
- if not text:
126
+ # ---------------------------------------------
127
+ # 2. Extraer payload de la solicitud
128
+ # ---------------------------------------------
129
+ payload = extract_payload_text(request)
130
+ if not payload:
71
131
  return None
72
132
 
73
- flagged, descripcion = detect_sql_attack(text)
74
- if flagged:
75
- # Solo tipo SQL, descripción con los patrones detectados
76
- request.sql_attack_info = {
77
- "ip": client_ip,
78
- "tipos": ["SQL"],
79
- "descripcion": descripcion,
80
- "payload": text,
81
- }
82
-
83
- logger.warning(
84
- f"Ataque SQL detectado desde IP {client_ip}: {descripcion}, payload: {text}"
85
- )
86
-
87
- # No devolvemos JsonResponse, solo marcamos el ataque
133
+ # ---------------------------------------------
134
+ # 3. Analizar contenido en busca de patrones SQLi
135
+ # ---------------------------------------------
136
+ flagged, descripcion = detect_sql_attack(payload)
137
+ if not flagged:
88
138
  return None
139
+
140
+ # ---------------------------------------------
141
+ # 4. Calcular puntaje de amenaza S_sqli
142
+ # ---------------------------------------------
143
+ w_sqli = getattr(settings, "SQLI_DEFENSE_WEIGHT", 0.4)
144
+ detecciones_sqli = len(descripcion)
145
+ s_sqli = w_sqli * detecciones_sqli
146
+
147
+ # ---------------------------------------------
148
+ # 5. Registrar e informar el intento
149
+ # ---------------------------------------------
150
+ logger.warning(
151
+ "Inyección SQL detectada desde IP %s: %s ; payload: %.200s ; score: %.2f",
152
+ client_ip,
153
+ descripcion,
154
+ payload,
155
+ s_sqli,
156
+ )
157
+
158
+ # Marcar información del ataque para el sistema de auditoría
159
+ request.sql_attack_info = {
160
+ "ip": client_ip,
161
+ "tipos": ["SQLi"],
162
+ "descripcion": descripcion,
163
+ "payload": payload,
164
+ "score": s_sqli,
165
+ }
166
+
167
+ return None
168
+
169
+
170
+ # =====================================================
171
+ # === INFORMACIÓN EXTRA ===
172
+ # =====================================================
173
+ """
174
+ Algoritmos relacionados:
175
+ - Se recomienda almacenar logs SQLi cifrados (AES-GCM)
176
+ para proteger evidencia de intentos maliciosos.
177
+
178
+ Cálculo de puntaje de amenaza:
179
+ S_sqli = w_sqli * detecciones_sqli
180
+ Ejemplo: S_sqli = 0.4 * 3 = 1.2
181
+
182
+ Integración:
183
+ Este middleware puede combinarse con:
184
+ - CSRFDefenseMiddleware
185
+ - XSSDefenseMiddleware
186
+ para calcular un score total de amenaza y decidir bloqueo.
187
+ """
@@ -1,18 +1,15 @@
1
- """
2
- detector_xss.py (version separada con IPs confiables independientes)
3
- """
4
-
1
+ # xss_defense.py
5
2
  from __future__ import annotations
6
3
  import json
7
4
  import logging
8
5
  import re
9
6
  from typing import List, Tuple
10
-
11
7
  from django.conf import settings
12
- from django.http import JsonResponse
13
8
  from django.utils.deprecation import MiddlewareMixin
14
9
 
15
- # Logger
10
+ # =====================================================
11
+ # === CONFIGURACIÓN LOGGER ===
12
+ # =====================================================
16
13
  logger = logging.getLogger("xssdefense")
17
14
  logger.setLevel(logging.INFO)
18
15
  if not logger.handlers:
@@ -20,7 +17,9 @@ if not logger.handlers:
20
17
  handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
21
18
  logger.addHandler(handler)
22
19
 
23
- # Intentar usar bleach si está disponible
20
+ # =====================================================
21
+ # === INTENTAR CARGAR BLEACH ===
22
+ # =====================================================
24
23
  try:
25
24
  import bleach
26
25
 
@@ -28,32 +27,51 @@ try:
28
27
  except Exception:
29
28
  _BLEACH_AVAILABLE = False
30
29
 
31
- # Patrones de detección XSS
30
+ # =====================================================
31
+ # === PATRONES DE DETECCIÓN XSS ===
32
+ # =====================================================
32
33
  XSS_PATTERNS: List[Tuple[re.Pattern, str]] = [
33
34
  (re.compile(r"<\s*script\b", re.I), "Etiqueta <script>"),
34
- (re.compile(r"on\w+\s*=", re.I), "Atributo evento (on*)"),
35
+ (re.compile(r"on\w+\s*=", re.I), "Atributo de evento (on*)"),
35
36
  (re.compile(r"javascript:\s*", re.I), "URI javascript:"),
36
37
  (re.compile(r"<\s*iframe\b", re.I), "Etiqueta <iframe>"),
37
38
  (re.compile(r"<\s*embed\b", re.I), "Etiqueta <embed>"),
39
+ (re.compile(r"<\s*object\b", re.I), "Etiqueta <object>"),
40
+ (re.compile(r"document\.cookie", re.I), "Acceso a document.cookie"),
41
+ (re.compile(r"alert\s*\(", re.I), "Uso de alert() potencial"),
38
42
  ]
39
43
 
40
44
 
41
- # Funciones auxiliares
45
+ # =====================================================
46
+ # === FUNCIONES AUXILIARES XSS ===
47
+ # =====================================================
42
48
  def detect_xss_text(text: str) -> Tuple[bool, List[str]]:
49
+ """
50
+ Busca patrones de XSS conocidos dentro de un texto.
51
+ Devuelve (True, lista_de_coincidencias) si hay indicios.
52
+ """
43
53
  matches: List[str] = []
44
54
  if not text:
45
55
  return False, matches
56
+
46
57
  for patt, message in XSS_PATTERNS:
47
58
  if patt.search(text):
48
59
  matches.append(message)
60
+
49
61
  return len(matches) > 0, matches
50
62
 
51
63
 
52
64
  def sanitize_input_basic(text: str) -> str:
65
+ """
66
+ Sanitiza una cadena eliminando etiquetas o caracteres peligrosos.
67
+ Usa bleach si está disponible, de lo contrario hace escape manual.
68
+ """
53
69
  if text is None:
54
70
  return text
71
+
55
72
  if _BLEACH_AVAILABLE:
56
73
  return bleach.clean(text, tags=[], attributes={}, protocols=[], strip=True)
74
+
57
75
  replacements = [
58
76
  ("&", "&amp;"),
59
77
  ("<", "&lt;"),
@@ -69,101 +87,111 @@ def sanitize_input_basic(text: str) -> str:
69
87
 
70
88
 
71
89
  def extract_payload_text(request) -> str:
90
+ """
91
+ Extrae un texto combinado con información del cuerpo, querystring,
92
+ agente de usuario y referer para análisis XSS.
93
+ """
72
94
  parts: List[str] = []
95
+
73
96
  try:
74
- ct = request.META.get("CONTENT_TYPE", "")
75
- if "application/json" in ct:
76
- parts.append(
77
- json.dumps(
78
- json.loads(request.body.decode("utf-8") or "{}"), ensure_ascii=False
79
- )
80
- )
97
+ content_type = request.META.get("CONTENT_TYPE", "")
98
+
99
+ if "application/json" in content_type:
100
+ body_data = json.loads(request.body.decode("utf-8") or "{}")
101
+ parts.append(json.dumps(body_data, ensure_ascii=False))
81
102
  else:
82
- parts.append(request.body.decode("utf-8", errors="ignore"))
103
+ body_text = request.body.decode("utf-8", errors="ignore")
104
+ if body_text:
105
+ parts.append(body_text)
83
106
  except Exception:
84
107
  pass
108
+
85
109
  qs = request.META.get("QUERY_STRING", "")
86
110
  if qs:
87
111
  parts.append(qs)
112
+
88
113
  parts.append(request.META.get("HTTP_USER_AGENT", ""))
89
114
  parts.append(request.META.get("HTTP_REFERER", ""))
115
+
90
116
  return " ".join([p for p in parts if p])
91
117
 
92
118
 
93
- # Middleware XSS
119
+ # =====================================================
120
+ # === MIDDLEWARE DE DEFENSA XSS ===
121
+ # =====================================================
94
122
  class XSSDefenseMiddleware(MiddlewareMixin):
95
123
  """
96
- Middleware Django que detecta XSS en IPs no confiables.
124
+ Middleware profesional de detección XSS.
125
+ - Detecta patrones maliciosos en solicitudes sospechosas.
126
+ - No bloquea directamente, solo marca el ataque para auditoría.
127
+ - Se integra con AuditoriaMiddleware (request.sql_attack_info).
97
128
  """
98
129
 
99
130
  def process_request(self, request):
100
- # 1) Obtener IP confiable específica para XSS
131
+ # ---------------------------------------------
132
+ # 1. Filtrar IPs de confianza
133
+ # ---------------------------------------------
101
134
  trusted_ips: List[str] = getattr(settings, "XSS_DEFENSE_TRUSTED_IPS", [])
102
- ip = request.META.get("REMOTE_ADDR", "")
103
- if ip in trusted_ips:
104
- return None # IP confiable → no analizar
135
+ client_ip = request.META.get("REMOTE_ADDR", "")
136
+ if client_ip in trusted_ips:
137
+ return None
105
138
 
106
- # 2) Verificar rutas excluidas
139
+ # ---------------------------------------------
140
+ # 2. Excluir rutas seguras
141
+ # ---------------------------------------------
107
142
  excluded_paths: List[str] = getattr(settings, "XSS_DEFENSE_EXCLUDED_PATHS", [])
108
143
  if any(request.path.startswith(p) for p in excluded_paths):
109
144
  return None
110
145
 
111
- # 3) Extraer payload
146
+ # ---------------------------------------------
147
+ # 3. Extraer y analizar payload
148
+ # ---------------------------------------------
112
149
  payload = extract_payload_text(request)
113
150
  if not payload:
114
151
  return None
115
152
 
116
- # 4) Detectar XSS
117
153
  flagged, matches = detect_xss_text(payload)
118
154
  if not flagged:
119
155
  return None
120
156
 
157
+ # ---------------------------------------------
158
+ # 4. Calcular puntaje de amenaza S_xss
159
+ # ---------------------------------------------
160
+ w_xss = getattr(settings, "XSS_DEFENSE_WEIGHT", 0.3)
161
+ detecciones_xss = len(matches)
162
+ s_xss = w_xss * detecciones_xss
163
+
164
+ # ---------------------------------------------
165
+ # 5. Loggear y marcar en el request
166
+ # ---------------------------------------------
121
167
  logger.warning(
122
- "XSS detectado desde IP %s: %s ; payload truncated: %.200s",
123
- ip,
168
+ "XSS detectado desde IP %s: %s ; payload: %.200s ; score: %.2f",
169
+ client_ip,
124
170
  matches,
125
171
  payload,
172
+ s_xss,
126
173
  )
127
174
 
128
- # 5) Sanitizar si está configurado
129
- if getattr(settings, "XSS_DEFENSE_SANITIZE_INPUT", False):
130
- try:
131
- if hasattr(request, "POST"):
132
- mutable_post = request.POST.copy()
133
- for k in mutable_post.keys():
134
- mutable_post[k] = sanitize_input_basic(mutable_post.get(k))
135
- request.POST = mutable_post
136
- if hasattr(request, "GET"):
137
- mutable_get = request.GET.copy()
138
- for k in mutable_get.keys():
139
- mutable_get[k] = sanitize_input_basic(mutable_get.get(k))
140
- request.GET = mutable_get
141
- except Exception:
142
- logger.debug("Error sanitizando inputs; continuar")
143
-
144
- # 6) Registrar ataque en AuditoriaMiddleware
145
175
  request.sql_attack_info = {
146
- "ip": ip,
176
+ "ip": client_ip,
147
177
  "tipos": ["XSS"],
148
178
  "descripcion": matches,
149
179
  "payload": payload,
180
+ "score": s_xss,
150
181
  }
151
182
 
152
- # 7) Bloquear petición si está configurado
153
- if getattr(settings, "XSS_DEFENSE_BLOCK", True):
154
- return JsonResponse(
155
- {"mensaje": "Ataque detectado (XSS)", "tipos": matches}, status=403
156
- )
157
-
158
183
  return None
159
184
 
160
185
 
161
- """
186
+ # =====================================================
187
+ # === INFORMACIÓN EXTRA ===
188
+ # =====================================================
189
+ """
162
190
  Algoritmos relacionados:
163
- *Guardar entradas sospechosas con AES-GCM para confidencialidad y autenticidad.
164
- Contribución a fórmula de amenaza S:
165
- S_xss = w_xss * detecciones_xss
166
- S_xss = 0.3 * 2
167
- donde w_xss es peso asignado a XSS y detecciones_xss es la cantidad de patrones detectados.
191
+ - Se recomienda almacenar los payloads XSS cifrados con AES-GCM
192
+ para confidencialidad e integridad.
168
193
 
194
+ Contribución a fórmula de amenaza S:
195
+ S_xss = w_xss * detecciones_xss
196
+ Ejemplo: S_xss = 0.3 * 2 = 0.6
169
197
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GuardianUnivalle-Benito-Yucra
3
- Version: 0.1.28
3
+ Version: 0.1.30
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,17 +4,17 @@ GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py,sha256=YxEtF6ZJj8M
4
4
  GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py,sha256=wfoRpaKvOqPbollNQsDNUNWClYJlXYTKTYvv0qcR6aI,962
5
5
  GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py,sha256=9djnlzb022hUhrDbQyWz7lWLbkn_vQZ4K7qar1FXYmo,829
6
6
  GuardianUnivalle_Benito_Yucra/criptografia/kdf.py,sha256=_sbepEY1qHEKga0ExrX2WRg1HeCPY5MC5CfXZWYyl-A,709
7
- GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py,sha256=EAYfLkHuxGC5rXSu4mZJ4yZDCbwBpTX8xZWGKz7i5wA,692
8
- GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py,sha256=lMWmCw6nccCEnek53nVjpoBCeiBqLdrSXxqRuI7VP2I,696
9
- GuardianUnivalle_Benito_Yucra/detectores/detector_keylogger.py,sha256=rEDG-Q_R56OsG2ypfHVBK7erolYjdvATnAxB3yvPXts,729
10
- GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py,sha256=b7pd4-CTH0FqW5-dwA6RA38Dls0bSBXKSLlmnKbLnbA,2982
11
- GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py,sha256=S0E8myh4uKxYjXMDvG6i1nc66XgB0iKIRa-9gDJErdA,5411
7
+ GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py,sha256=8gynkJlxF2XfHPLdWaMS048b5Tuhznsb4JI3bqggC0g,7892
8
+ GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py,sha256=ech5UmUwK84EPSVZvjI0Q4Pq7w8WGymuYc77LgAKiiA,6163
9
+ GuardianUnivalle_Benito_Yucra/detectores/detector_keylogger.py,sha256=T46HXHT3LZz3kke4GcxOM8urJSy-Vq2BEDfo9_F56Pw,6717
10
+ GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py,sha256=o2IXqF3Nbsh5roPihyLal42iCzgxfyX3D6Ef-cxOpVo,6644
11
+ GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py,sha256=kdXJsoDjvBVccrO2QiE65Q527sUjy1WmlzYg8rDjmqA,6795
12
12
  GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py,sha256=23pLLYqliUoMrIC6ZEwz3hKXeDjWfHSm9vYPWGmDDik,495
13
13
  GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py,sha256=ipMOebYhql-6mSyHs0ddYXOcXq9w8P_IXLlpiIqGncw,246
14
14
  GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py,sha256=6AYWII4mrmwCLHCvGTyoBxR4Oasr4raSHpFbVjqn7d8,193
15
15
  GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py,sha256=Wx5XfcII4oweLvZsTBEJ7kUc9pMpP5-36RfI5C5KJXo,561
16
- guardianunivalle_benito_yucra-0.1.28.dist-info/licenses/LICENSE,sha256=5e4IdL542v1E8Ft0A24GZjrxZeTsVK7XrS3mZEUhPtM,37
17
- guardianunivalle_benito_yucra-0.1.28.dist-info/METADATA,sha256=ljR3reAfOBK8g2jaAp6pbsG7BpTMcjNFoLK26q4v7PM,1893
18
- guardianunivalle_benito_yucra-0.1.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- guardianunivalle_benito_yucra-0.1.28.dist-info/top_level.txt,sha256=HTWfZM64WAV_QYr5cnXnLuabQt92dvlxqlR3pCwpbDQ,30
20
- guardianunivalle_benito_yucra-0.1.28.dist-info/RECORD,,
16
+ guardianunivalle_benito_yucra-0.1.30.dist-info/licenses/LICENSE,sha256=5e4IdL542v1E8Ft0A24GZjrxZeTsVK7XrS3mZEUhPtM,37
17
+ guardianunivalle_benito_yucra-0.1.30.dist-info/METADATA,sha256=yS2g75_JDicESJXiuBDwCJXOYiyCe0lrXzlSjEB8XNM,1893
18
+ guardianunivalle_benito_yucra-0.1.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ guardianunivalle_benito_yucra-0.1.30.dist-info/top_level.txt,sha256=HTWfZM64WAV_QYr5cnXnLuabQt92dvlxqlR3pCwpbDQ,30
20
+ guardianunivalle_benito_yucra-0.1.30.dist-info/RECORD,,