GuardianUnivalle-Benito-Yucra 1.1.18__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.
@@ -0,0 +1,25 @@
1
+ """ Funciones principales """
2
+ # GuardianUnivalle_Benito_Yucra/__init__.py
3
+
4
+ """
5
+ Paquete principal de GuardianUnivalle.
6
+ Incluye módulos de criptografía, detección de ataques, mitigación, auditoría y puntuación de amenazas.
7
+ """
8
+ from . import criptografia
9
+ from . import detectores
10
+ from . import mitigacion
11
+ from . import auditoria
12
+ from . import puntuacion
13
+ from . import middleware_web
14
+ from . import utilidades
15
+
16
+ def protect_app():
17
+ """
18
+ Activa todas las protecciones de seguridad de forma automática.
19
+ """
20
+ print("🔒 GuardianUnivalle-Benito-Yucra: Seguridad activada")
21
+ # Aquí podríamos llamar funciones automáticamente si queremos
22
+ # scan_malware()
23
+ # rate_limiter()
24
+ # sanitize_input()
25
+ # check_csrf()
@@ -0,0 +1,40 @@
1
+ import os
2
+ import datetime
3
+ import json
4
+
5
+ LOG_FILE = "auditoria_guardian.log"
6
+
7
+
8
+ def registrar_evento(
9
+ tipo: str,
10
+ descripcion: str = "",
11
+ severidad: str = "MEDIA",
12
+ extra: dict | None = None,
13
+ ):
14
+ try:
15
+ evento = {
16
+ "fecha": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
17
+ "tipo": tipo,
18
+ "descripcion": descripcion,
19
+ "severidad": severidad,
20
+ "extra": extra or {},
21
+ }
22
+
23
+ # ✅ Crear carpeta solo si hay directorio en la ruta
24
+ log_dir = os.path.dirname(LOG_FILE)
25
+ if log_dir:
26
+ os.makedirs(log_dir, exist_ok=True)
27
+
28
+ with open(LOG_FILE, "a", encoding="utf-8") as f:
29
+ f.write(json.dumps(evento, ensure_ascii=False) + "\n")
30
+
31
+ except Exception as e:
32
+ print(f"[Auditoría] Error al registrar evento: {e}")
33
+
34
+
35
+ def generar_reporte() -> str:
36
+ """Devuelve todo el contenido del archivo de auditoría."""
37
+ if not os.path.exists(LOG_FILE):
38
+ return "No hay registros aún."
39
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
40
+ return f.read()
@@ -0,0 +1,25 @@
1
+ """
2
+ Cifrado simétrico autenticado: AES-GCM y ChaCha20-Poly1305
3
+ """
4
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
5
+ import os
6
+
7
+ def cifrar_aes_gcm(mensaje: bytes, clave: bytes) -> dict:
8
+ aes = AESGCM(clave)
9
+ nonce = os.urandom(12)
10
+ ciphertext = aes.encrypt(nonce, mensaje, None)
11
+ return {"nonce": nonce, "ciphertext": ciphertext}
12
+
13
+ def descifrar_aes_gcm(cipher: dict, clave: bytes) -> bytes:
14
+ aes = AESGCM(clave)
15
+ return aes.decrypt(cipher["nonce"], cipher["ciphertext"], None)
16
+
17
+ def cifrar_chacha20(mensaje: bytes, clave: bytes) -> dict:
18
+ cipher = ChaCha20Poly1305(clave)
19
+ nonce = os.urandom(12)
20
+ ciphertext = cipher.encrypt(nonce, mensaje, None)
21
+ return {"nonce": nonce, "ciphertext": ciphertext}
22
+
23
+ def descifrar_chacha20(cipher: dict, clave: bytes) -> bytes:
24
+ cipher_obj = ChaCha20Poly1305(clave)
25
+ return cipher_obj.decrypt(cipher["nonce"], cipher["ciphertext"], None)
@@ -0,0 +1,23 @@
1
+ """
2
+ Gestión de intercambio de claves con ECDH y derivación HKDF.
3
+ """
4
+ from cryptography.hazmat.primitives.asymmetric import ec
5
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
6
+ from cryptography.hazmat.primitives import hashes
7
+
8
+ def generar_claves_ecdh():
9
+ """Genera clave privada y pública ECDH"""
10
+ clave_privada = ec.generate_private_key(ec.SECP384R1())
11
+ clave_publica = clave_privada.public_key()
12
+ return clave_privada, clave_publica
13
+
14
+ def derivar_clave_secreta(clave_privada, clave_publica):
15
+ """Deriva una clave compartida usando HKDF"""
16
+ shared_key = clave_privada.exchange(ec.ECDH(), clave_publica)
17
+ derived_key = HKDF(
18
+ algorithm=hashes.SHA256(),
19
+ length=32,
20
+ salt=None,
21
+ info=b'guardianclave'
22
+ ).derive(shared_key)
23
+ return derived_key
@@ -0,0 +1,23 @@
1
+ """
2
+ Wrappers para derivación de claves segura: PBKDF2 y Argon2
3
+ """
4
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
5
+ from argon2 import PasswordHasher
6
+ from cryptography.hazmat.primitives import hashes
7
+ import os
8
+
9
+ def pbkdf2_derivar_clave(password: str, salt: bytes = None) -> bytes:
10
+ """Deriva clave usando PBKDF2"""
11
+ salt = salt or os.urandom(16)
12
+ kdf = PBKDF2HMAC(
13
+ algorithm=hashes.SHA256(),
14
+ length=32,
15
+ salt=salt,
16
+ iterations=100_000,
17
+ )
18
+ return kdf.derive(password.encode()), salt
19
+
20
+ def argon2_derivar_clave(password: str) -> str:
21
+ """Deriva clave usando Argon2"""
22
+ ph = PasswordHasher()
23
+ return ph.hash(password)
@@ -0,0 +1,488 @@
1
+ from __future__ import annotations
2
+ import secrets
3
+ import logging
4
+ import re
5
+ import json
6
+ import base64
7
+ import os
8
+ import time
9
+ from typing import List, Dict, Any
10
+ from urllib.parse import urlparse
11
+ from django.conf import settings
12
+ from django.utils.deprecation import MiddlewareMixin
13
+ from django.core.cache import cache
14
+
15
+ # cryptography & argon2
16
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
17
+ from cryptography.hazmat.primitives import hashes, hmac as crypto_hmac
18
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
19
+ from cryptography.exceptions import InvalidSignature
20
+ from argon2.low_level import hash_secret_raw, Type as Argon2Type
21
+
22
+ logger = logging.getLogger("csrfdefense")
23
+ logger.setLevel(logging.INFO)
24
+ if not logger.handlers:
25
+ handler = logging.StreamHandler()
26
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
27
+ logger.addHandler(handler)
28
+
29
+ STATE_CHANGING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
30
+ CSRF_HEADER_NAMES = ("HTTP_X_CSRFTOKEN", "HTTP_X_CSRF_TOKEN")
31
+ CSRF_COOKIE_NAME = getattr(settings, "CSRF_COOKIE_NAME", "csrftoken")
32
+ POST_FIELD_NAME = "csrfmiddlewaretoken"
33
+
34
+ # Patrón de Content-Type sospechoso - EXPANDIDO
35
+ SUSPICIOUS_CT_PATTERNS = [
36
+ re.compile(r"text/plain", re.I),
37
+ re.compile(r"application/x-www-form-urlencoded", re.I),
38
+ re.compile(r"multipart/form-data", re.I),
39
+ re.compile(r"application/json", re.I),
40
+ re.compile(r"text/html", re.I), # Agregado para HTML CSRF
41
+ ]
42
+
43
+ # Parámetros sensibles típicos de CSRF - EXPANDIDO
44
+ SENSITIVE_PARAMS = [
45
+ "password", "csrfmiddlewaretoken", "token", "amount", "transfer", "delete", "update", "action", "email", "username"
46
+ ]
47
+
48
+ # Campos sensibles: ANALIZAMOS COMPLETAMENTE SIN DESCUENTO PARA ROBUSTEZ MÁXIMA
49
+ SENSITIVE_FIELDS = ["password", "csrfmiddlewaretoken", "token", "auth", "email", "username"]
50
+
51
+ CSRF_DEFENSE_MIN_SIGNALS = getattr(settings, "CSRF_DEFENSE_MIN_SIGNALS", 1)
52
+ CSRF_DEFENSE_EXCLUDED_API_PREFIXES = getattr(settings, "CSRF_DEFENSE_EXCLUDED_API_PREFIXES", ["/api/"])
53
+
54
+ # PATRONES EXPANDIDOS PARA ANÁLISIS DE PAYLOAD EN TODOS LOS CAMPOS (SIN DESCUENTO)
55
+ CSRF_PAYLOAD_PATTERNS = [
56
+ (re.compile(r"<script[^>]*>.*?</script>", re.I | re.S), "Script tag en payload", 0.9),
57
+ (re.compile(r"javascript\s*:", re.I), "URI javascript: en payload", 0.8),
58
+ (re.compile(r"http[s]?://[^\s]+", re.I), "URL externa en payload", 0.7),
59
+ (re.compile(r"eval\s*\$", re.I), "eval() en payload", 1.0),
60
+ (re.compile(r"document\.cookie", re.I), "Acceso a cookie en payload", 0.9),
61
+ (re.compile(r"innerHTML\s*=", re.I), "Manipulación DOM innerHTML", 0.8),
62
+ (re.compile(r"XMLHttpRequest", re.I), "XHR en payload", 0.7),
63
+ (re.compile(r"fetch\s*\$", re.I), "fetch() en payload", 0.7),
64
+ (re.compile(r"&#x[0-9a-fA-F]+;", re.I), "Entidades HTML en payload", 0.6),
65
+ (re.compile(r"%3Cscript", re.I), "Script URL-encoded en payload", 0.8),
66
+ (re.compile(r"on\w+\s*=", re.I), "Eventos on* en payload", 0.7),
67
+ (re.compile(r"alert\s*\$", re.I), "alert() en payload (prueba)", 0.5),
68
+ ]
69
+
70
+ # ----------------------------
71
+ # Configuraciones criptográficas (similar a XSS/SQLi)
72
+ # ----------------------------
73
+ MASTER_KEY_B64 = getattr(settings, "CSRF_DEFENSE_MASTER_KEY", None)
74
+ if not MASTER_KEY_B64:
75
+ MASTER_KEY = os.urandom(32)
76
+ else:
77
+ try:
78
+ MASTER_KEY = base64.b64decode(MASTER_KEY_B64)
79
+ except Exception:
80
+ MASTER_KEY = MASTER_KEY_B64.encode() if isinstance(MASTER_KEY_B64, str) else MASTER_KEY_B64
81
+
82
+ AEAD_CHOICE = getattr(settings, "CSRF_DEFENSE_AEAD", "AESGCM").upper() # AESGCM o CHACHA20
83
+ ARGON2_CONFIG = getattr(settings, "CSRF_DEFENSE_ARGON2", {
84
+ "time_cost": 2,
85
+ "memory_cost": 65536,
86
+ "parallelism": 1,
87
+ "hash_len": 32,
88
+ "type": Argon2Type.ID,
89
+ })
90
+ HMAC_LABEL = getattr(settings, "CSRF_HMAC_LABEL", b"csrfdefense-hmac")
91
+ AEAD_LABEL = getattr(settings, "CSRF_AEAD_LABEL", b"csrfdefense-aead")
92
+ HASH_CHOICE = getattr(settings, "CSRF_DEFENSE_HASH", "SHA256").upper() # SHA256 o SHA3
93
+
94
+ # ----------------------------
95
+ # Funciones criptográficas (derivación, AEAD, HMAC, hash)
96
+ # ----------------------------
97
+ def derive_key(label: bytes, context: bytes = b"") -> bytes:
98
+ salt = (label + context)[:16].ljust(16, b"\0")
99
+ try:
100
+ raw = hash_secret_raw(secret=MASTER_KEY if isinstance(MASTER_KEY, (bytes, bytearray)) else MASTER_KEY.encode(),
101
+ salt=salt,
102
+ time_cost=ARGON2_CONFIG["time_cost"],
103
+ memory_cost=ARGON2_CONFIG["memory_cost"],
104
+ parallelism=ARGON2_CONFIG["parallelism"],
105
+ hash_len=ARGON2_CONFIG["hash_len"],
106
+ type=ARGON2_CONFIG["type"])
107
+ hk = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=label + context)
108
+ return hk.derive(raw)
109
+ except Exception:
110
+ hk = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=label + context)
111
+ return hk.derive(MASTER_KEY if isinstance(MASTER_KEY, bytes) else MASTER_KEY.encode())
112
+
113
+ def aead_encrypt(plaintext: bytes, aad: bytes = b"", context: bytes = b"") -> Dict[str, bytes]:
114
+ key = derive_key(AEAD_LABEL, context)
115
+ if AEAD_CHOICE == "CHACHA20":
116
+ aead = ChaCha20Poly1305(key)
117
+ nonce = os.urandom(12)
118
+ ct = aead.encrypt(nonce, plaintext, aad)
119
+ return {"alg": "CHACHA20-POLY1305", "nonce": nonce, "ciphertext": ct}
120
+ else:
121
+ aead = AESGCM(key)
122
+ nonce = os.urandom(12)
123
+ ct = aead.encrypt(nonce, plaintext, aad)
124
+ return {"alg": "AES-GCM", "nonce": nonce, "ciphertext": ct}
125
+
126
+ def aead_decrypt(payload: Dict[str, bytes], aad: bytes = b"", context: bytes = b"") -> bytes:
127
+ key = derive_key(AEAD_LABEL, context)
128
+ alg = payload.get("alg", "AES-GCM")
129
+ nonce = payload.get("nonce")
130
+ ct = payload.get("ciphertext")
131
+ if not nonce or not ct:
132
+ raise ValueError("invalid payload for AEAD decrypt")
133
+ if alg.startswith("CHACHA20"):
134
+ aead = ChaCha20Poly1305(key)
135
+ return aead.decrypt(nonce, ct, aad)
136
+ else:
137
+ aead = AESGCM(key)
138
+ return aead.decrypt(nonce, ct, aad)
139
+
140
+ def compute_hmac(data: bytes, context: bytes = b"") -> bytes:
141
+ key = derive_key(HMAC_LABEL, context)
142
+ h = crypto_hmac.HMAC(key, hashes.SHA256())
143
+ h.update(data)
144
+ return h.finalize()
145
+
146
+ def verify_hmac(data: bytes, tag: bytes, context: bytes = b"") -> bool:
147
+ key = derive_key(HMAC_LABEL, context)
148
+ h = crypto_hmac.HMAC(key, hashes.SHA256())
149
+ h.update(data)
150
+ try:
151
+ h.verify(tag)
152
+ return True
153
+ except InvalidSignature:
154
+ return False
155
+
156
+ def compute_hash(data: bytes) -> str:
157
+ if HASH_CHOICE == "SHA3":
158
+ h = hashes.Hash(hashes.SHA3_256())
159
+ else:
160
+ h = hashes.Hash(hashes.SHA256())
161
+ h.update(data)
162
+ return base64.b64encode(h.finalize()).decode()
163
+
164
+ # ----------------------------
165
+ # Funciones para tokens CSRF firmados/cifrados
166
+ # ----------------------------
167
+ def sign_csrf_token(token: str, context: bytes = b"") -> str:
168
+ """Firma un token CSRF con HMAC-SHA256 para evitar alteraciones."""
169
+ data = token.encode("utf-8")
170
+ tag = compute_hmac(data, context)
171
+ return f"{token}.{base64.b64encode(tag).decode()}"
172
+
173
+ def verify_csrf_token_signature(signed_token: str, context: bytes = b"") -> str:
174
+ """Verifica la firma de un token CSRF y retorna el token original si es válido."""
175
+ try:
176
+ token, tag_b64 = signed_token.rsplit(".", 1)
177
+ tag = base64.b64decode(tag_b64)
178
+ if verify_hmac(token.encode("utf-8"), tag, context):
179
+ return token
180
+ else:
181
+ raise ValueError("Invalid signature")
182
+ except Exception:
183
+ raise ValueError("Invalid signed CSRF token")
184
+
185
+ def encrypt_csrf_token(token: str, context: bytes = b"") -> str:
186
+ """Cifra un token CSRF sensible con AEAD."""
187
+ enc = aead_encrypt(token.encode("utf-8"), context=context)
188
+ return base64.b64encode(json.dumps(enc).encode()).decode()
189
+
190
+ def decrypt_csrf_token(encrypted_token: str, context: bytes = b"") -> str:
191
+ """Descifra un token CSRF."""
192
+ try:
193
+ enc = json.loads(base64.b64decode(encrypted_token))
194
+ plaintext = aead_decrypt(enc, context=context)
195
+ return plaintext.decode("utf-8")
196
+ except Exception:
197
+ raise ValueError("Invalid encrypted CSRF token")
198
+
199
+ # ----------------------------
200
+ # Registro cifrado de eventos (similar a XSS/SQLi)
201
+ # ----------------------------
202
+ def record_csrf_event(event: dict) -> None:
203
+ try:
204
+ ts = int(time.time())
205
+ # cifrar payload si existe
206
+ if "payload" in event and event["payload"]:
207
+ try:
208
+ ctx = f"{event.get('ip','')}-{ts}".encode()
209
+ enc = aead_encrypt(json.dumps(event["payload"], ensure_ascii=False).encode("utf-8"), context=ctx)
210
+ htag = compute_hmac(enc["ciphertext"], context=ctx)
211
+ event["_payload_encrypted"] = {
212
+ "alg": enc["alg"],
213
+ "nonce": base64.b64encode(enc["nonce"]).decode(),
214
+ "ciphertext": base64.b64encode(enc["ciphertext"]).decode(),
215
+ "hmac": base64.b64encode(htag).decode(),
216
+ }
217
+ del event["payload"] # no almacenar plaintext
218
+ except Exception:
219
+ # si falla, simplemente no incluimos payload
220
+ event.pop("payload", None)
221
+ key = f"csrf_event:{ts}:{event.get('ip', '')}"
222
+ cache.set(key, json.dumps(event, ensure_ascii=False), timeout=60 * 60 * 24)
223
+ except Exception:
224
+ logger.exception("record_csrf_event failed")
225
+
226
+ # ----------------------------
227
+ # Funciones auxiliares (igual que antes)
228
+ # ----------------------------
229
+ def get_client_ip(request):
230
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
231
+ if x_forwarded_for:
232
+ ips = [ip.strip() for ip in x_forwarded_for.split(",") if ip.strip()]
233
+ if ips:
234
+ return ips[0]
235
+ return request.META.get("REMOTE_ADDR", "")
236
+
237
+ def host_from_header(header_value: str) -> str | None:
238
+ if not header_value:
239
+ return None
240
+ try:
241
+ parsed = urlparse(header_value)
242
+ if parsed.netloc:
243
+ return parsed.netloc.split(":")[0]
244
+ return header_value.split(":")[0]
245
+ except Exception:
246
+ return None
247
+
248
+ def origin_matches_host(request) -> bool:
249
+ host_header = request.META.get("HTTP_HOST") or request.META.get("SERVER_NAME")
250
+ if not host_header:
251
+ return True
252
+ host = host_header.split(":")[0]
253
+ origin = request.META.get("HTTP_ORIGIN", "")
254
+ referer = request.META.get("HTTP_REFERER", "")
255
+ # Bloquear obvious javascript: referers
256
+ if any(re.search(r"(javascript:|<script|data:text/html)", h or "", re.I) for h in [origin, referer]):
257
+ return False
258
+ if origin_host := host_from_header(origin):
259
+ if origin_host == host:
260
+ return True
261
+ if referer_host := host_from_header(referer):
262
+ if referer_host == host:
263
+ return True
264
+ if not origin and not referer:
265
+ return True
266
+ return False
267
+
268
+ def has_csrf_token(request) -> bool:
269
+ for h in CSRF_HEADER_NAMES:
270
+ if request.META.get(h):
271
+ return True
272
+ if request.COOKIES.get(CSRF_COOKIE_NAME):
273
+ return True
274
+ try:
275
+ if request.method == "POST" and hasattr(request, "POST"):
276
+ if request.POST.get(POST_FIELD_NAME):
277
+ return True
278
+ except Exception:
279
+ pass
280
+ return False
281
+
282
+ def extract_payload_text(request) -> str:
283
+ parts: List[str] = []
284
+ try:
285
+ body = request.body.decode("utf-8", errors="ignore")
286
+ if body:
287
+ parts.append(body)
288
+ except Exception:
289
+ pass
290
+ qs = request.META.get("QUERY_STRING", "")
291
+ if qs:
292
+ parts.append(qs)
293
+ parts.append(request.META.get("HTTP_USER_AGENT", ""))
294
+ parts.append(request.META.get("HTTP_REFERER", ""))
295
+ return " ".join([p for p in parts if p])
296
+
297
+ def extract_parameters(request) -> List[str]:
298
+ params = []
299
+ if hasattr(request, "POST"):
300
+ params.extend(request.POST.keys())
301
+ if hasattr(request, "GET"):
302
+ params.extend(request.GET.keys())
303
+ try:
304
+ if request.body and "application/json" in (request.META.get("CONTENT_TYPE") or ""):
305
+ data = json.loads(request.body)
306
+ params.extend(data.keys())
307
+ except Exception:
308
+ pass
309
+ return params
310
+
311
+ # FUNCIÓN ROBUSTA: Analizar payload en TODOS los campos (sin descuento)
312
+ def analyze_payload(value: str) -> float:
313
+ score = 0.0
314
+ for patt, desc, weight in CSRF_PAYLOAD_PATTERNS:
315
+ if patt.search(value):
316
+ score += weight # Score full, sin descuento
317
+ return round(score, 3)
318
+
319
+ # NUEVA FUNCIÓN: Extraer y analizar query string
320
+ def analyze_query_string(request) -> float:
321
+ qs = request.META.get("QUERY_STRING", "")
322
+ if qs:
323
+ return analyze_payload(qs)
324
+ return 0.0
325
+
326
+ # NUEVA FUNCIÓN: Analizar headers adicionales
327
+ def analyze_headers(request) -> List[str]:
328
+ issues = []
329
+ ua = request.META.get("HTTP_USER_AGENT", "")
330
+ if re.search(r"(script|<|eval|bot|crawler)", ua, re.I):
331
+ issues.append("User-Agent sospechoso (posible automatización/bot)")
332
+
333
+
334
+ accept_lang = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
335
+ if not accept_lang or len(accept_lang) < 2:
336
+ issues.append("Accept-Language ausente o muy corto (posible bot)")
337
+
338
+ return issues
339
+
340
+ class CSRFDefenseMiddleware(MiddlewareMixin):
341
+ def process_request(self, request):
342
+ # Excluir APIs JSON si se configuró así
343
+ for prefix in CSRF_DEFENSE_EXCLUDED_API_PREFIXES:
344
+ if request.path.startswith(prefix):
345
+ logger.debug(f"[CSRFDefense] Skip analysis for API prefix {prefix} path {request.path}")
346
+ return None
347
+
348
+ client_ip = get_client_ip(request)
349
+ trusted_ips = getattr(settings, "CSRF_DEFENSE_TRUSTED_IPS", [])
350
+ if client_ip in trusted_ips:
351
+ return None
352
+
353
+ excluded_paths = getattr(settings, "CSRF_DEFENSE_EXCLUDED_PATHS", [])
354
+ if any(request.path.startswith(p) for p in excluded_paths):
355
+ return None
356
+
357
+ method = (request.method or "").upper()
358
+ if method not in STATE_CHANGING_METHODS:
359
+ return None
360
+
361
+ descripcion: List[str] = []
362
+ payload = extract_payload_text(request)
363
+ params = extract_parameters(request)
364
+
365
+ # 1) Falta token CSRF
366
+ if not has_csrf_token(request):
367
+ descripcion.append("Falta token CSRF en cookie/header/form")
368
+
369
+ # 2) Origin/Referer no coinciden
370
+ if not origin_matches_host(request):
371
+ descripcion.append("Origin/Referer no coinciden con Host (posible cross-site)")
372
+
373
+ # 3) Content-Type sospechoso
374
+ content_type = (request.META.get("CONTENT_TYPE") or "")
375
+ for patt in SUSPICIOUS_CT_PATTERNS:
376
+ if patt.search(content_type):
377
+ descripcion.append(f"Content-Type sospechoso: {content_type}")
378
+ break
379
+
380
+ # 4) Referer ausente y sin token CSRF
381
+ referer = request.META.get("HTTP_REFERER", "")
382
+ if not referer and not any(request.META.get(h) for h in CSRF_HEADER_NAMES):
383
+ descripcion.append("Referer ausente y sin X-CSRFToken")
384
+
385
+ # 5) Parámetros sensibles en GET/JSON
386
+ for p in params:
387
+ if p.lower() in SENSITIVE_PARAMS and method == "GET":
388
+ descripcion.append(f"Parámetro sensible '{p}' enviado en GET (posible CSRF)")
389
+
390
+ # 6) JSON sospechoso desde dominio externo
391
+ if "application/json" in content_type:
392
+ origin = request.META.get("HTTP_ORIGIN") or ""
393
+ if origin and host_from_header(origin) != (request.META.get("HTTP_HOST") or "").split(":")[0]:
394
+ descripcion.append("JSON POST desde origen externo (posible CSRF)")
395
+
396
+ # 7) Análisis ROBUSTO de payload en TODOS los campos (sin descuento)
397
+ payload_score = 0.0
398
+ payload_summary: List[Dict[str, Any]] = []
399
+ try:
400
+ # Analizar POST
401
+ if hasattr(request, "POST"):
402
+ for key, value in request.POST.items():
403
+ if isinstance(value, str):
404
+ score = analyze_payload(value)
405
+ payload_score += score
406
+ if score > 0:
407
+ payload_summary.append({"field": key, "snippet": value[:300], "score": score})
408
+ # Analizar JSON
409
+ if "application/json" in content_type:
410
+ data = json.loads(request.body.decode("utf-8") or "{}")
411
+ for key, value in data.items():
412
+ if isinstance(value, str):
413
+ score = analyze_payload(value)
414
+ payload_score += score
415
+ if score > 0:
416
+ payload_summary.append({"field": key, "snippet": value[:300], "score": score})
417
+ except Exception as e:
418
+ logger.debug(f"Error analizando payload: {e}")
419
+
420
+ if payload_score > 0:
421
+ descripcion.append(f"Payload sospechoso detectado (score total: {payload_score})")
422
+
423
+ # 8) Análisis de query string
424
+ qs_score = analyze_query_string(request)
425
+ if qs_score > 0:
426
+ descripcion.append(f"Query string sospechosa (score: {qs_score})")
427
+ payload_score += qs_score
428
+
429
+ # 9) Análisis de headers adicionales
430
+ header_issues = analyze_headers(request)
431
+ descripcion.extend(header_issues)
432
+
433
+ # Señales >= umbral => marcar y registrar evento cifrado
434
+ total_signals = len(descripcion)
435
+ if descripcion and total_signals >= CSRF_DEFENSE_MIN_SIGNALS:
436
+ w_csrf = getattr(settings, "CSRF_DEFENSE_WEIGHT", 0.2)
437
+ s_csrf = w_csrf * total_signals + payload_score # Score full sin descuento
438
+ request.csrf_attack_info = {
439
+ "ip": client_ip,
440
+ "tipos": ["CSRF"],
441
+ "descripcion": descripcion,
442
+ "payload": json.dumps(payload_summary, ensure_ascii=False)[:1000],
443
+ "score": s_csrf,
444
+ }
445
+ logger.warning(
446
+ "CSRF detectado desde IP %s: %s ; path=%s ; Content-Type=%s ; score=%.2f (Ultra-Robust: nada ignorado)",
447
+ client_ip, descripcion, request.path, content_type, s_csrf
448
+ )
449
+ # Registrar evento cifrado para auditoría
450
+ try:
451
+ record_csrf_event({
452
+ "ts": int(time.time()),
453
+ "ip": client_ip,
454
+ "score": s_csrf,
455
+ "desc": descripcion,
456
+ "url": request.build_absolute_uri(),
457
+ "payload": payload_summary, # se cifrará en record_csrf_event
458
+ })
459
+ except Exception:
460
+ logger.exception("failed to record CSRF event")
461
+ else:
462
+ if descripcion:
463
+ logger.debug(f"[CSRFDefense] low-signals ({total_signals}) not marking: {descripcion}")
464
+
465
+ return None
466
+
467
+ # =====================================================
468
+ # === INFORMACIÓN EXTRA ===
469
+ # =====================================================
470
+ """
471
+ Algoritmos relacionados para protección contra CSRF:
472
+ - HMAC-SHA256: Usado para firmar tokens CSRF (sign_csrf_token, verify_csrf_token_signature).
473
+ - SHA-256/SHA-3: Usado para hashes de contenido (compute_hash).
474
+ - AES-GCM/ChaCha20-Poly1305: Usado para cifrar tokens sensibles o payloads (encrypt_csrf_token, decrypt_csrf_token).
475
+ - HKDF: Usado para derivar claves seguras (derive_key).
476
+ - Argon2id: Usado para derivar claves con resistencia a ataques de fuerza bruta (derive_key).
477
+ - Registro cifrado de eventos: Para asegurar auditoría sin exponer datos sensibles.
478
+
479
+ Contribución a fórmula de amenaza S:
480
+ S_csrf = w_csrf * señales_csrf + payload_score
481
+ Ejemplo: S_csrf = 0.2 * 3 + 1.5 = 2.1
482
+
483
+ Notas sobre implementación de algoritmos de seguridad:
484
+ - Integra con Django's CSRF middleware: Usa este middleware para detección adicional, y genera tokens firmados si es necesario.
485
+ - Para usar en producción: Configura CSRF_DEFENSE_MASTER_KEY en settings.py.
486
+ - Ajusta umbrales y configuraciones según necesidades.
487
+ - Combina con CSP (Content Security Policy) para mayor protección.
488
+ """