doctorlinux commited on
Commit
f228014
·
verified ·
1 Parent(s): 3fb5e8c

Upload 8 files

Browse files
.huggingface.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ sdk: gradio
2
+ sdk_version: 4.44.1
3
+ app_file: app.py
4
+ python_version: 3.10
README.md CHANGED
@@ -1,13 +1,20 @@
1
- ---
2
- title: Simulator
3
- emoji: 🔥
4
- colorFrom: gray
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 5.47.2
8
- app_file: app.py
9
- pinned: false
10
- license: gpl
11
- ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Simulador de Ataques - Doctor Linux
 
 
 
 
 
 
 
 
 
 
2
 
3
+ Contiene varias aplicaciones Gradio/Spaces:
4
+ - `app.py` → archivo principal (copia del simulador con descarga)
5
+ - `app_fixed.py` → detector de phishing corregido
6
+ - `redteam_simulator.py` → simulador básico (sin descarga)
7
+ - `redteam_simulator_with_download.py` → simulador con botón de descarga
8
+ - `requirements.txt` → dependencias (gradio, requests, tldextract, dnspython, dkimpy)
9
+ - `runtime.txt` → versión de Python (python-3.10)
10
+ - `.huggingface.yaml` → configuración del Space
11
+
12
+ ## Instrucciones de despliegue
13
+ 1. Subir todos los archivos a la pestaña *Files* de tu Space (o subir el ZIP y descomprimir).
14
+ 2. En *Settings → Variables and secrets* crea `OPENAI_API_KEY` con tu clave de OpenAI si quieres usar IA.
15
+ 3. Commit/Save y esperar a que el Space instale dependencias y arranque.
16
+ 4. Verifica la pestaña *Logs* si ocurre algún error.
17
+
18
+ ## Notas de seguridad
19
+ - Estas herramientas son educativas y defensivas. No generan instrucciones técnicas de explotación.
20
+ - Evita subir información sensible si no quieres que quede registrada en los logs del Space.
app.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # redteam_simulator_with_download.py
2
+ # Simulador Red Team (alto nivel, defensivo) - Gradio app + generación de reporte TXT
3
+ # Requisitos: gradio, requests
4
+ # Instrucciones: sube este archivo a tu Space y configura OPENAI_API_KEY en Settings -> Variables and secrets
5
+ import os
6
+ import json
7
+ import re
8
+ import time
9
+ import requests
10
+ import gradio as gr
11
+ from typing import Tuple
12
+
13
+ # ------------------ Config ------------------
14
+ OPENAI_API_URL = "https://api.openai.com/v1/responses"
15
+ OPENAI_MODEL_FALLBACK = ["gpt-4o-mini", "gpt-4o", "gpt-5-mini"]
16
+
17
+ PROMPT_TEMPLATE = \"\"\"Eres un asistente de ciberseguridad (red-team simulator) que debe simular a alto nivel cómo un atacante podría aprovechar el contenido que se le entrega (email, URL o fragmento). NORMAS (obligatorias):
18
+ 1. Proporciona sólo un resumen del objetivo del atacante y del vector. NO entregues instrucciones técnicas, comandos, código de exploit, payloads, o pasos paso-a-paso para cometer un ataque.
19
+ 2. Devuelve 3 secciones en JSON:
20
+ - \"simulation\": breve párrafo (1-3 frases) explicando la estrategia del atacante (alto nivel).
21
+ - \"iocs\": lista de indicadores accionables para detección (dominios, patrones de URL, encabezados sospechosos, extensiones).
22
+ - \"mitigations\": lista de contramedidas operativas (bloqueos, políticas, educación, verificación técnica).
23
+ 3. Si el material es insuficiente, indica qué faltaría.
24
+ 4. Limita la respuesta a lenguaje defensivo y educacional. NO ofrezcas código ni tácticas para explotar vulnerabilidades.
25
+ 5. Devuelve SOLO JSON válido (objetivo: {\"simulation\":..., \"iocs\":[...], \"mitigations\":[...]})
26
+
27
+ Contenido a analizar:
28
+ {input}
29
+ \"\"\"
30
+
31
+ FORBIDDEN_PATTERNS = [
32
+ r\"\\bexploit\\b\", r\"\\bpayload\\b\", r\"\\bmeterpreter\\b\", r\"\\bmsfconsole\\b\",
33
+ r\"curl\\b\", r\"wget\\b\", r\"sudo\\b\", r\"rm\\s+-rf\\b\", r\"reverse shell\\b\",
34
+ r\"exec\\b\", r\"bash -i\\b\", r\"nc\\b\", r\"ncat\\b\", r\"chmod\\b\", r\"chown\\b\",
35
+ r\"\\bsqlmap\\b\", r\"\\\\x\", r\"0x[0-9a-fA-F]{2,}\", r\"base64 -d\", r\"\\\\b\\\\$\\\\(\", r\"\\\\$\\\\{\"
36
+ ]
37
+ FORBIDDEN_REGEX = re.compile(\"|\".join(FORBIDDEN_PATTERNS), re.I)
38
+
39
+ def call_openai_responses(prompt: str, api_key: str, models=None, timeout: int = 20) -> Tuple[bool, str]:
40
+ if models is None:
41
+ models = OPENAI_MODEL_FALLBACK
42
+ headers = {\"Authorization\": f\"Bearer {api_key}\", \"Content-Type\": \"application/json\"}
43
+ for model in models:
44
+ payload = {\"model\": model, \"input\": prompt}
45
+ try:
46
+ r = requests.post(OPENAI_API_URL, headers=headers, json=payload, timeout=timeout)
47
+ except Exception as e:
48
+ return False, f\"Error de conexión al llamar a la API: {e}\"
49
+ if r.status_code == 200:
50
+ try:
51
+ j = r.json()
52
+ out = \"\"
53
+ if \"output\" in j:
54
+ if isinstance(j[\"output\"], list):
55
+ parts = []
56
+ for item in j[\"output\"]:
57
+ if isinstance(item, dict):
58
+ c = item.get(\"content\") or item.get(\"text\") or item.get(\"output_text\")
59
+ if isinstance(c, str):
60
+ parts.append(c)
61
+ elif isinstance(c, list):
62
+ for el in c:
63
+ if isinstance(el, dict):
64
+ txt = el.get(\"text\") or el.get(\"output_text\") or el.get(\"content\")
65
+ if txt:
66
+ parts.append(str(txt))
67
+ else:
68
+ parts.append(str(el))
69
+ out = \"\\n\".join(parts).strip()
70
+ elif isinstance(j[\"output\"], str):
71
+ out = j[\"output\"].strip()
72
+ if not out and \"choices\" in j and isinstance(j.get(\"choices\"), list) and j[\"choices\"]:
73
+ ch = j[\"choices\"][0]
74
+ out = ch.get(\"text\") or ch.get(\"message\", {}).get(\"content\", {}).get(\"text\") or \"\"
75
+ if not out:
76
+ out = json.dumps(j, ensure_ascii=False)[:4000]
77
+ return True, out
78
+ except Exception as e:
79
+ return False, f\"Error parseando respuesta de la API: {e}\"
80
+ else:
81
+ try:
82
+ ej = r.json()
83
+ msg = ej.get(\"error\", {}).get(\"message\") or ej.get(\"message\") or r.text
84
+ except Exception:
85
+ msg = r.text
86
+ if r.status_code == 401:
87
+ return False, \"AuthenticationError (401): OPENAI_API_KEY inválida o revocada.\"
88
+ if r.status_code == 429:
89
+ return False, \"RateLimitError (429): límite superado en OpenAI.\"
90
+ if isinstance(msg, str) and \"model\" in msg.lower():
91
+ continue
92
+ return False, f\"HTTP {r.status_code}: {msg}\"
93
+ return False, \"Ningún modelo disponible o permitido en la cuenta de OpenAI.\"
94
+
95
+ def contains_forbidden(text: str) -> bool:
96
+ if not text:
97
+ return False
98
+ return bool(FORBIDDEN_REGEX.search(text))
99
+
100
+ def safe_parse_json_from_model(text: str):
101
+ try:
102
+ return json.loads(text)
103
+ except Exception:
104
+ s = text.find('{')
105
+ e = text.rfind('}')
106
+ if s != -1 and e != -1 and e > s:
107
+ try:
108
+ return json.loads(text[s:e+1])
109
+ except Exception:
110
+ return {\"raw\": text}
111
+ return {\"raw\": text}
112
+
113
+ def generate_simulation(user_input: str, include_iocs: bool, include_mitigation: bool):
114
+ api_key = os.environ.get(\"OPENAI_API_KEY\")
115
+ if not api_key:
116
+ return \"<p style='color:crimson'><b>Error:</b> OPENAI_API_KEY no configurada en Settings → Variables and secrets.</p>\", \"\"
117
+
118
+ prompt = PROMPT_TEMPLATE.format(input=user_input)
119
+ ok, out = call_openai_responses(prompt, api_key)
120
+ if not ok:
121
+ return f\"<p style='color:crimson'><b>Error IA:</b> {out}</p>\", \"\"
122
+
123
+ if contains_forbidden(out):
124
+ safe_msg = (\"La respuesta original fue bloqueada por contener contenido sensible que podría ser instructivo para ataques. "
125
+ "He realizado un bloqueo por seguridad. Intenta proporcionar más contexto defensivo o limpia el contenido y vuelve a intentarlo.\")
126
+ return f\"<p style='color:crimson'><b>Contenido bloqueado por seguridad:</b></p><p>{safe_msg}</p>\", \"\"
127
+
128
+ parsed = safe_parse_json_from_model(out)
129
+
130
+ html = []
131
+ html.append(\"<h3>Simulación Red Team (alto nivel)</h3>\")
132
+ if isinstance(parsed, dict) and parsed.get(\"simulation\"):
133
+ html.append(f\"<p><b>Simulación:</b> {parsed['simulation']}</p>\")
134
+ else:
135
+ sim = parsed.get(\"simulation\") if isinstance(parsed, dict) else None
136
+ html.append(f\"<p><b>Simulación:</b> {json.dumps(sim, ensure_ascii=False)}</p>\")
137
+
138
+ if include_iocs:
139
+ html.append(\"<h4>Indicadores (IoCs) sugeridos</h4>\")
140
+ iocs = parsed.get(\"iocs\") if isinstance(parsed, dict) else None
141
+ if isinstance(iocs, list) and iocs:
142
+ html.append(\"<ul>\")
143
+ for i in iocs:
144
+ html.append(f\"<li>{i}</li>\")
145
+ html.append(\"</ul>\")
146
+ else:
147
+ html.append(f\"<p>{json.dumps(iocs, ensure_ascii=False)}</p>\")
148
+
149
+ if include_mitigation:
150
+ html.append(\"<h4>Contramedidas y mitigación</h4>\")
151
+ mit = parsed.get(\"mitigations\") if isinstance(parsed, dict) else None
152
+ if isinstance(mit, list) and mit:
153
+ html.append(\"<ul>\")
154
+ for m in mit:
155
+ html.append(f\"<li>{m}</li>\")
156
+ html.append(\"</ul>\")
157
+ else:
158
+ html.append(f\"<p>{json.dumps(mit, ensure_ascii=False)}</p>\")
159
+
160
+ html.append(\"<p style='font-size:0.9em;color:#bbb'>Nota: esta simulación es de alto nivel y educativa. No proporciona instrucciones de ataque. Use para mejorar defensas y detección.</p>\")
161
+ # devolvemos tambien el JSON parseado como string para uso en reporte
162
+ return \"\\n\".join(html), json.dumps(parsed, ensure_ascii=False, indent=2)
163
+
164
+ def generate_report(json_str: str, title: str = \"Reporte Red Team\") -> Tuple[str, str]:
165
+ \"\"\"Crea un archivo TXT con la simulación y mitigaciones y devuelve la ruta lista para descargar.\"\"\"
166
+ if not json_str:
167
+ return \"\", \"\"
168
+ try:
169
+ parsed = json.loads(json_str) if isinstance(json_str, str) else json_str
170
+ except Exception:
171
+ parsed = {\"raw\": str(json_str)}
172
+
173
+ timestamp = time.strftime(\"%Y%m%d_%H%M%S\")
174
+ filename = f\"/mnt/data/redteam_report_{timestamp}.txt\"
175
+ with open(filename, 'w', encoding='utf-8') as f:
176
+ f.write(f\"{title}\\nGenerated: {time.ctime()}\\n\\n\")
177
+ f.write(\"SIMULATION:\\n\")
178
+ sim = parsed.get(\"simulation\") if isinstance(parsed, dict) else None
179
+ f.write((sim or \"(no simulation)\") + \"\\n\\n\")
180
+ f.write(\"IOCS:\\n\")
181
+ for i in (parsed.get(\"iocs\") if isinstance(parsed, dict) and parsed.get(\"iocs\") else []):
182
+ f.write(f\"- {i}\\n\")
183
+ f.write(\"\\nMITIGATIONS:\\n\")
184
+ for m in (parsed.get(\"mitigations\") if isinstance(parsed, dict) and parsed.get(\"mitigations\") else []):
185
+ f.write(f\"- {m}\\n\")
186
+ f.write(\"\\nRAW:\\n\")
187
+ f.write(json.dumps(parsed, ensure_ascii=False, indent=2))
188
+ return filename, filename # return as two values (path, path) for compatibility
189
+
190
+ # ------------------ UI ------------------
191
+ with gr.Blocks(analytics_enabled=False) as demo:
192
+ gr.Markdown(\"## 🧯 Simulador Red Team (alto nivel) — Defender con IA\")
193
+ with gr.Row():
194
+ with gr.Column(scale=7):
195
+ inp = gr.Textbox(label=\"Pega aquí el correo RAW, URL o fragmento a analizar\", lines=20, placeholder=\"Pega cabeceras, cuerpo o URL completa\")
196
+ cb_iocs = gr.Checkbox(label=\"Incluir IoCs (indicadores) en la salida\", value=True)
197
+ cb_mit = gr.Checkbox(label=\"Incluir mitigaciones\", value=True)
198
+ btn = gr.Button(\"Simular ataque (alto nivel)\")
199
+ download_btn = gr.Button(\"Generar reporte (.txt)\")
200
+ with gr.Column(scale=5):
201
+ out_html = gr.HTML(\"<i>Resultado aparecerá aquí</i>\")
202
+ # componente invisible para guardar el JSON parseado
203
+ last_json = gr.Textbox(visible=False)
204
+ file_out = gr.File(label=\"Descargar reporte (.txt)\", visible=False)
205
+
206
+ # Al hacer click en Simular -> actualiza out_html y last_json (json string)
207
+ btn.click(generate_simulation, inputs=[inp, cb_iocs, cb_mit], outputs=[out_html, last_json])
208
+ # Al hacer click en Generar reporte -> crea archivo y lo muestra en file_out
209
+ download_btn.click(generate_report, inputs=[last_json, gr.Textbox(value=\"Reporte Red Team\", visible=False)], outputs=[file_out, file_out])
210
+
211
+ if __name__ == '__main__':
212
+ demo.launch(server_name='0.0.0.0', server_port=7860)
app_fixed.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_fixed.py
2
+ # Phishing detector - improved version with heuristics, SPF/DKIM checks and optional OpenAI integration.
3
+ # Requires: gradio, tldextract, dnspython, dkimpy, requests
4
+ import os
5
+ import re
6
+ import json
7
+ import traceback
8
+ import requests
9
+ import tldextract
10
+ import gradio as gr
11
+ import dns.resolver
12
+ import dkim
13
+ from email import policy
14
+ from email.parser import BytesParser
15
+ from typing import List, Dict, Any
16
+
17
+ # ----------------------------
18
+ # Config
19
+ # ----------------------------
20
+ SHORTENER_DOMAINS = {
21
+ "bit.ly", "t.co", "tinyurl.com", "goo.gl", "ow.ly", "is.gd", "buff.ly",
22
+ "shorturl.at", "rb.gy", "tiny.one", "clk.im"
23
+ }
24
+
25
+ # Model fallback list: try in order
26
+ OPENAI_MODEL_FALLBACK = [
27
+ "gpt-4o-mini",
28
+ "gpt-4o",
29
+ "gpt-5-mini",
30
+ ]
31
+
32
+ OPENAI_API_URL = "https://api.openai.com/v1/responses"
33
+
34
+ # ----------------------------
35
+ # Utilities
36
+ # ----------------------------
37
+ def extract_links(text: str) -> List[str]:
38
+ url_re = re.compile(r"(?i)\bhttps?://[^\s<>'\)\]]+")
39
+ found = set()
40
+ for m in url_re.finditer(text or ""):
41
+ url = m.group(0).rstrip('.,:;\")')
42
+ found.add(url)
43
+ return sorted(found)
44
+
45
+ def url_hostname(url: str) -> str:
46
+ try:
47
+ parsed = tldextract.extract(url)
48
+ if parsed.domain:
49
+ if parsed.suffix:
50
+ return f"{parsed.domain}.{parsed.suffix}"
51
+ return parsed.domain
52
+ m = re.match(r"https?://([^/]+)", url)
53
+ return m.group(1).lower() if m else url
54
+ except Exception:
55
+ return url
56
+
57
+ def is_shortener(url: str) -> bool:
58
+ host = url_hostname(url)
59
+ return any(host.endswith(s) for s in SHORTENER_DOMAINS)
60
+
61
+ def contains_ip(url: str) -> bool:
62
+ return bool(re.search(r"https?://(\d{1,3}(?:\.\d{1,3}){3})", url))
63
+
64
+ def contains_urgent_language(text: str) -> bool:
65
+ urgent_re = re.compile(r"\b(urgente|inmediatamente|verifique|actualice|pago|riesgo|suspendido|caduca|vencimiento|bloqueado|atenci[oó]n|urgencia)\b", re.I)
66
+ return bool(urgent_re.search(text or ""))
67
+
68
+ # ----------------------------
69
+ # Email parsing & checks
70
+ # ----------------------------
71
+ def parse_email_raw(raw_text: str) -> Dict[str, Any]:
72
+ """Try to parse headers and body from a raw email text. Returns dict."""
73
+ out = {"from": None, "reply_to": None, "subject": None, "body": raw_text, "raw_bytes": None}
74
+ try:
75
+ # Ensure bytes for the BytesParser
76
+ if isinstance(raw_text, str):
77
+ raw_bytes = raw_text.encode('utf-8', errors='ignore')
78
+ else:
79
+ raw_bytes = raw_text
80
+ out['raw_bytes'] = raw_bytes
81
+ parser = BytesParser(policy=policy.default)
82
+ try:
83
+ msg = parser.parsebytes(raw_bytes)
84
+ except Exception:
85
+ msg = None
86
+ if msg:
87
+ out['from'] = str(msg.get('From') or "").strip()
88
+ out['reply_to'] = str(msg.get('Reply-To') or "").strip()
89
+ out['subject'] = str(msg.get('Subject') or "").strip()
90
+ # get body (prefer plain)
91
+ if msg.is_multipart():
92
+ parts = []
93
+ for part in msg.walk():
94
+ ctype = part.get_content_type()
95
+ disp = str(part.get_content_disposition() or "")
96
+ if ctype == 'text/plain' and disp != 'attachment':
97
+ try:
98
+ parts.append(part.get_content())
99
+ except Exception:
100
+ parts.append(part.get_payload(decode=True).decode('utf-8', errors='ignore'))
101
+ out['body'] = "\n\n".join(p for p in parts if p)
102
+ if not out['body']:
103
+ # fallback to first text part
104
+ for part in msg.walk():
105
+ if part.get_content_type().startswith('text/'):
106
+ try:
107
+ out['body'] = part.get_content()
108
+ break
109
+ except:
110
+ pass
111
+ else:
112
+ try:
113
+ out['body'] = msg.get_content()
114
+ except:
115
+ out['body'] = msg.get_payload(decode=True).decode('utf-8', errors='ignore') if msg.get_payload(decode=True) else raw_text
116
+ except Exception as e:
117
+ print("PARSE RAW ERROR:", repr(e))
118
+ traceback.print_exc()
119
+ return out
120
+
121
+ def spf_check(ip: str, domain: str) -> Dict[str, Any]:
122
+ """Simple SPF presence check: queries TXT records for the domain and returns if spf record found."""
123
+ try:
124
+ answers = dns.resolver.resolve(domain, 'TXT', lifetime=5)
125
+ txts = [b"".join(r.strings).decode('utf-8', errors='ignore') for r in answers]
126
+ spf = [t for t in txts if t.lower().startswith('v=spf1')]
127
+ return {"ok": bool(spf), "records": txts}
128
+ except Exception as e:
129
+ return {"ok": False, "error": str(e)}
130
+
131
+ def dkim_check(raw_bytes: bytes) -> Dict[str, Any]:
132
+ """Attempt DKIM verification using dkimpy; returns result dict."""
133
+ try:
134
+ # dkim.verify expects full message bytes
135
+ res = dkim.verify(raw_bytes)
136
+ return {"ok": bool(res)}
137
+ except Exception as e:
138
+ return {"ok": False, "error": str(e)}
139
+
140
+ # ----------------------------
141
+ # Heuristics
142
+ # ----------------------------
143
+ def analyze_heuristics(raw_text: str, from_header: str = "") -> Dict[str, Any]:
144
+ links = extract_links(raw_text)
145
+ reasons = []
146
+ score = 0
147
+ # domain mismatch
148
+ from_dom = ""
149
+ if from_header:
150
+ m = re.search(r"@([\w\.-]+)", from_header)
151
+ from_dom = m.group(1).lower() if m else ""
152
+ for u in links:
153
+ host = url_hostname(u)
154
+ if from_dom and host and from_dom not in host:
155
+ reasons.append("Dominio de enlaces distinto al dominio del remitente")
156
+ score += 20
157
+ break
158
+ if any(contains_ip(u) for u in links):
159
+ reasons.append("Enlaces con IP en vez de dominio")
160
+ score += 20
161
+ if any(is_shortener(u) for u in links):
162
+ reasons.append("Enlace acortado sospechoso")
163
+ score += 15
164
+ if contains_urgent_language(raw_text):
165
+ reasons.append("Lenguaje de urgencia / presión")
166
+ score += 15
167
+ if re.search(r'\.(exe|scr|bat|cmd|msi|zip)\b', raw_text, re.I):
168
+ reasons.append("Adjunto ejecutable o extensión peligrosa detectada")
169
+ score += 15
170
+ # reply-to different
171
+ m_reply = re.search(r"Reply-To:\s*(.+)", raw_text, re.I)
172
+ m_from = re.search(r"From:\s*(.+)", raw_text, re.I)
173
+ if m_reply and m_from:
174
+ reply = m_reply.group(1).strip()
175
+ frm = m_from.group(1).strip()
176
+ if reply and frm and (reply.lower() not in frm.lower()):
177
+ reasons.append("Reply-To diferente al From")
178
+ score += 10
179
+ # normalize
180
+ score = max(0, min(100, score))
181
+ return {"score": score, "reasons": reasons, "links": links, "from_domain": from_dom}
182
+
183
+ # ----------------------------
184
+ # OpenAI helper with fallbacks & robust error messages
185
+ # ----------------------------
186
+ def call_openai(prompt_text: str, api_key: str, models=None, timeout=20):
187
+ if models is None:
188
+ models = OPENAI_MODEL_FALLBACK
189
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
190
+ for model in models:
191
+ payload = {"model": model, "input": prompt_text}
192
+ try:
193
+ resp = requests.post(OPENAI_API_URL, headers=headers, json=payload, timeout=timeout)
194
+ except Exception as e:
195
+ print("AI CALL ERROR (connection):", repr(e))
196
+ traceback.print_exc()
197
+ return False, f"Error de conexión: {e}"
198
+ if resp.status_code == 200:
199
+ try:
200
+ j = resp.json()
201
+ # extract output text from Responses API
202
+ out = ""
203
+ if "output" in j:
204
+ if isinstance(j["output"], list):
205
+ parts = []
206
+ for item in j["output"]:
207
+ if isinstance(item, dict):
208
+ c = item.get("content") or item.get("text") or item.get("output_text")
209
+ if isinstance(c, str):
210
+ parts.append(c)
211
+ elif isinstance(c, list):
212
+ for el in c:
213
+ if isinstance(el, dict):
214
+ txt = el.get("text") or el.get("output_text") or el.get("content")
215
+ if txt:
216
+ parts.append(str(txt))
217
+ else:
218
+ parts.append(str(el))
219
+ out = "\n\n".join(parts).strip()
220
+ elif isinstance(j["output"], str):
221
+ out = j["output"].strip()
222
+ if not out and "choices" in j and isinstance(j.get("choices"), list) and j["choices"]:
223
+ ch = j["choices"][0]
224
+ out = ch.get("text") or ch.get("message", {}).get("content", {}).get("text") or ""
225
+ if not out:
226
+ out = json.dumps(j, ensure_ascii=False)[:4000]
227
+ return True, out
228
+ except Exception as e:
229
+ print("AI CALL ERROR (parse):", repr(e))
230
+ traceback.print_exc()
231
+ return False, f"Error al parsear respuesta de OpenAI: {e}"
232
+ else:
233
+ try:
234
+ err_json = resp.json()
235
+ except Exception:
236
+ err_json = {"status_code": resp.status_code, "text": resp.text}
237
+ print(f"AI CALL HTTP ERROR model={model}: status={resp.status_code} body={str(err_json)[:1000]}")
238
+ if resp.status_code == 401:
239
+ return False, "AuthenticationError (401): clave inválida o revocada. Revoca y crea una nueva en platform.openai.com"
240
+ if resp.status_code == 429:
241
+ return False, "RateLimitError (429): cuota superada o límite de velocidad en OpenAI."
242
+ # model not found? try next
243
+ msg = ""
244
+ if isinstance(err_json, dict):
245
+ msg = err_json.get("error", {}).get("message") or err_json.get("message") or str(err_json)
246
+ if msg and "model" in msg.lower():
247
+ # try next model
248
+ continue
249
+ return False, f"Error HTTP {resp.status_code} al llamar a OpenAI: {msg or resp.text}"
250
+ return False, "Ningún modelo disponible o permitido en la cuenta de OpenAI."
251
+
252
+ # ----------------------------
253
+ # Main analyze function
254
+ # ----------------------------
255
+ def analyze_email(raw_text: str, use_ai: bool = False, do_spf: bool = False, do_dkim: bool = False) -> Dict[str, Any]:
256
+ result = {"heuristic": None, "spf": None, "dkim": None, "ai": None}
257
+ try:
258
+ parsed = parse_email_raw(raw_text or "")
259
+ heur = analyze_heuristics(parsed.get('body', raw_text), parsed.get('from') or parsed.get('reply_to') or "")
260
+ result['heuristic'] = heur
261
+ # technical checks
262
+ # SPF: try to extract an IP from Received headers (simple heuristic)
263
+ if do_spf:
264
+ # find first Received header IP
265
+ m = re.search(r"Received: .*\[?(\d{1,3}(?:\.\d{1,3}){3})\]?", raw_text or "", re.I)
266
+ ip = m.group(1) if m else None
267
+ domain = heur.get('from_domain') or (parsed.get('from') and re.search(r"@([\w\.-]+)", parsed.get('from')) and re.search(r"@([\w\.-]+)", parsed.get('from')).group(1))
268
+ if domain and ip:
269
+ result['spf'] = spf_check(ip, domain)
270
+ else:
271
+ result['spf'] = {"ok": False, "error": "No se pudo extraer IP o dominio para SPF"}
272
+ if do_dkim:
273
+ raw_bytes = parsed.get('raw_bytes')
274
+ if raw_bytes:
275
+ result['dkim'] = dkim_check(raw_bytes)
276
+ else:
277
+ result['dkim'] = {"ok": False, "error": "No raw bytes disponibles para DKIM"}
278
+ # AI
279
+ if use_ai:
280
+ key = os.environ.get('OPENAI_API_KEY')
281
+ if not key:
282
+ result['ai'] = {"error": "OPENAI_API_KEY no configurada en Settings → Variables and secrets."}
283
+ else:
284
+ prompt = (
285
+ "Eres un detector de phishing. Recibiste este correo (incluye cabeceras y cuerpo):\n\n" +
286
+ (raw_text or "") +
287
+ "\n\nResponde con JSON válido con campos: verdict ('phishing'|'suspicious'|'legitimate'), score (float 0-1), reasons (lista de strings). SOLO devuelve JSON puro."
288
+ )
289
+ ok, out = call_openai(prompt, key)
290
+ if not ok:
291
+ result['ai'] = {"error": out}
292
+ else:
293
+ # try to parse json
294
+ parsed_ai = None
295
+ try:
296
+ parsed_ai = json.loads(out)
297
+ except Exception:
298
+ # try to find JSON substring
299
+ s = out.find('{')
300
+ e = out.rfind('}')
301
+ if s != -1 and e != -1 and e > s:
302
+ try:
303
+ parsed_ai = json.loads(out[s:e+1])
304
+ except Exception:
305
+ parsed_ai = {"raw": out}
306
+ else:
307
+ parsed_ai = {"raw": out}
308
+ result['ai'] = parsed_ai
309
+ return result
310
+ except Exception as e:
311
+ print("ANALYZE ERROR:", repr(e))
312
+ traceback.print_exc()
313
+ return {"error": True, "message": str(e)}
314
+
315
+ # ----------------------------
316
+ # UI
317
+ # ----------------------------
318
+ def format_result_html(res: Dict[str, Any]) -> str:
319
+ if res.get('error'):
320
+ return f"<b>Error:</b> {res.get('message')}"
321
+ parts = []
322
+ heur = res.get('heuristic') or {}
323
+ parts.append(f"<h3>Resultado del análisis</h3>")
324
+ parts.append(f"<b>Riesgo heurístico:</b> {heur.get('score',0)}%")
325
+ parts.append("<h4>Heurísticas</h4>")
326
+ if heur.get('reasons'):
327
+ parts.append("<ul>")
328
+ for r in heur.get('reasons'):
329
+ parts.append(f"<li>{r}</li>")
330
+ parts.append("</ul>")
331
+ else:
332
+ parts.append("<p>No se detectaron heurísticas sospechosas.</p>")
333
+ parts.append("<h4>Enlaces detectados</h4>")
334
+ links = heur.get('links') or []
335
+ if links:
336
+ parts.append("<ul>")
337
+ for u in links:
338
+ parts.append(f"<li><a href=\"{u}\" target=\"_blank\">{u}</a></li>")
339
+ parts.append("</ul>")
340
+ else:
341
+ parts.append("<p>-</p>")
342
+ parts.append("<h4>Comprobaciones técnicas</h4>")
343
+ if res.get('spf') is not None:
344
+ spf = res['spf']
345
+ if spf.get('ok'):
346
+ parts.append(f"<p>SPF: <b>Encontrado</b> (registros: {len(spf.get('records',[]))})</p>")
347
+ else:
348
+ parts.append(f"<p>SPF: <b>No verificado</b> - {spf.get('error') or ''}</p>")
349
+ if res.get('dkim') is not None:
350
+ d = res['dkim']
351
+ if d.get('ok'):
352
+ parts.append("<p>DKIM: <b>Firma válida</b></p>")
353
+ else:
354
+ parts.append(f"<p>DKIM: <b>No válido</b> - {d.get('error') or ''}</p>")
355
+ parts.append("<h4>Veredicto IA</h4>")
356
+ if res.get('ai') is None:
357
+ parts.append("<p>IA no activada.</p>")
358
+ elif isinstance(res.get('ai'), dict) and res.get('ai').get('error'):
359
+ parts.append(f"<p style='color:crimson;'><b>Error IA:</b> {res['ai'].get('error')}</p>")
360
+ else:
361
+ parts.append("<pre style='white-space:pre-wrap;background:#111;padding:10px;border-radius:6px;color:#d6d6d6;'>")
362
+ parts.append(json.dumps(res.get('ai'), ensure_ascii=False, indent=2))
363
+ parts.append("</pre>")
364
+ return '\\n'.join(parts)
365
+
366
+ with gr.Blocks(css=".gradio-container .output_html { color: #ddd; }", analytics_enabled=False) as demo:
367
+ gr.Markdown("## 🔎 Detector de Phishing — Mejorado (heurísticas + SPF/DKIM + OpenAI opcional)")
368
+ with gr.Row():
369
+ with gr.Column(scale=7):
370
+ inp = gr.Textbox(label="Correo (RAW o contenido)", lines=20, placeholder="Pega aquí el correo (ideal: RAW con cabeceras)")
371
+ use_ai = gr.Checkbox(label="Usar IA (OpenAI)", value=False)
372
+ do_spf = gr.Checkbox(label="Comprobar SPF (intentará extraer IP desde Received)", value=False)
373
+ do_dkim = gr.Checkbox(label="Comprobar DKIM (si pegas el RAW completo)", value=False)
374
+ btn = gr.Button("Analizar")
375
+ with gr.Column(scale=5):
376
+ out_html = gr.HTML("<i>Resultado aparecerá aquí</i>")
377
+ def run(raw, use_ai_flag, spf_flag, dkim_flag):
378
+ res = analyze_email(raw or "", use_ai=bool(use_ai_flag), do_spf=bool(spf_flag), do_dkim=bool(dkim_flag))
379
+ return format_result_html(res)
380
+ btn.click(run, inputs=[inp, use_ai, do_spf, do_dkim], outputs=[out_html])
381
+
382
+ if __name__ == '__main__':
383
+ demo.launch(server_name='0.0.0.0', server_port=7860)
redteam_simulator.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # redteam_simulator.py
2
+ # Simulador Red Team (alto nivel, defensivo) - Gradio app
3
+ # Requisitos: gradio, requests
4
+ # Instrucciones: sube este archivo a tu Space y configura OPENAI_API_KEY en Settings -> Variables and secrets
5
+ import os
6
+ import json
7
+ import re
8
+ import requests
9
+ import gradio as gr
10
+ from typing import Tuple
11
+
12
+ # ------------------ Config ------------------
13
+ OPENAI_API_URL = "https://api.openai.com/v1/responses"
14
+ OPENAI_MODEL_FALLBACK = ["gpt-4o-mini", "gpt-4o", "gpt-5-mini"]
15
+
16
+ # Prompt template (defensivo, no instrucciones de explotación)
17
+ PROMPT_TEMPLATE = """Eres un asistente de ciberseguridad (red-team simulator) que debe simular a alto nivel cómo un atacante podría aprovechar el contenido que se le entrega (email, URL o fragmento). NORMAS (obligatorias):
18
+ 1. Proporciona sólo un resumen del objetivo del atacante y del vector. NO entregues instrucciones técnicas, comandos, código de exploit, payloads, o pasos paso-a-paso para cometer un ataque.
19
+ 2. Devuelve 3 secciones en JSON:
20
+ - \"simulation\": breve párrafo (1-3 frases) explicando la estrategia del atacante (alto nivel).
21
+ - \"iocs\": lista de indicadores accionables para detección (dominios, patrones de URL, encabezados sospechosos, extensiones).
22
+ - \"mitigations\": lista de contramedidas operativas (bloqueos, políticas, educación, verificación técnica).
23
+ 3. Si el material es insuficiente, indica qué faltaría.
24
+ 4. Limita la respuesta a lenguaje defensivo y educacional. NO ofrezcas código ni tácticas para explotar vulnerabilidades.
25
+ 5. Devuelve SOLO JSON válido (objetivo: {\"simulation\":..., \"iocs\":[...], \"mitigations\":[...]})
26
+
27
+ Contenido a analizar:
28
+ {input}
29
+ """
30
+
31
+ # Palabras/prototipos prohibidos en la salida (si aparecen, se bloqueará la respuesta por seguridad)
32
+ FORBIDDEN_PATTERNS = [
33
+ r"\bexploit\b", r"\bpayload\b", r"\bmeterpreter\b", r"\bmsfconsole\b",
34
+ r"curl\b", r"wget\b", r"sudo\b", r"rm\s+-rf\b", r"reverse shell\b",
35
+ r"exec\b", r"bash -i\b", r"nc\b", r"ncat\b", r"chmod\b", r"chown\b",
36
+ r"\bsqlmap\b", r"\\x", r"0x[0-9a-fA-F]{2,}", r"base64 -d", r"\\b\\$\\(", r"\\$\\{"
37
+ ]
38
+ FORBIDDEN_REGEX = re.compile("|".join(FORBIDDEN_PATTERNS), re.I)
39
+
40
+ # ------------------ Helpers ------------------
41
+ def call_openai_responses(prompt: str, api_key: str, models=None, timeout: int = 20) -> Tuple[bool, str]:
42
+ if models is None:
43
+ models = OPENAI_MODEL_FALLBACK
44
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
45
+ for model in models:
46
+ payload = {"model": model, "input": prompt}
47
+ try:
48
+ r = requests.post(OPENAI_API_URL, headers=headers, json=payload, timeout=timeout)
49
+ except Exception as e:
50
+ return False, f"Error de conexión al llamar a la API: {e}"
51
+ if r.status_code == 200:
52
+ try:
53
+ j = r.json()
54
+ # Extraer texto de la Responses API
55
+ out = ""
56
+ if "output" in j:
57
+ if isinstance(j["output"], list):
58
+ parts = []
59
+ for item in j["output"]:
60
+ if isinstance(item, dict):
61
+ c = item.get("content") or item.get("text") or item.get("output_text")
62
+ if isinstance(c, str):
63
+ parts.append(c)
64
+ elif isinstance(c, list):
65
+ for el in c:
66
+ if isinstance(el, dict):
67
+ txt = el.get("text") or el.get("output_text") or el.get("content")
68
+ if txt:
69
+ parts.append(str(txt))
70
+ else:
71
+ parts.append(str(el))
72
+ out = "\\n".join(parts).strip()
73
+ elif isinstance(j["output"], str):
74
+ out = j["output"].strip()
75
+ # Fallback a choices
76
+ if not out and "choices" in j and isinstance(j.get("choices"), list) and j["choices"]:
77
+ ch = j["choices"][0]
78
+ out = ch.get("text") or ch.get("message", {}).get("content", {}).get("text") or ""
79
+ if not out:
80
+ out = json.dumps(j, ensure_ascii=False)[:4000]
81
+ return True, out
82
+ except Exception as e:
83
+ return False, f"Error parseando respuesta de la API: {e}"
84
+ else:
85
+ # error http: intentar siguiente modelo o devolver mensaje claro
86
+ try:
87
+ ej = r.json()
88
+ msg = ej.get("error", {}).get("message") or ej.get("message") or r.text
89
+ except Exception:
90
+ msg = r.text
91
+ if r.status_code == 401:
92
+ return False, "AuthenticationError (401): OPENAI_API_KEY inválida o revocada."
93
+ if r.status_code == 429:
94
+ return False, "RateLimitError (429): límite superado en OpenAI."
95
+ # si el mensaje menciona el modelo, intentar siguiente
96
+ if isinstance(msg, str) and "model" in msg.lower():
97
+ continue
98
+ return False, f"HTTP {r.status_code}: {msg}"
99
+ return False, "Ningún modelo disponible o permitido en la cuenta de OpenAI."
100
+
101
+ def contains_forbidden(text: str) -> bool:
102
+ if not text:
103
+ return False
104
+ return bool(FORBIDDEN_REGEX.search(text))
105
+
106
+ def safe_parse_json_from_model(text: str):
107
+ # Intenta parsear JSON; si falla, extrae el primer bloque JSON encontrado
108
+ try:
109
+ return json.loads(text)
110
+ except Exception:
111
+ s = text.find('{')
112
+ e = text.rfind('}')
113
+ if s != -1 and e != -1 and e > s:
114
+ try:
115
+ return json.loads(text[s:e+1])
116
+ except Exception:
117
+ return {"raw": text}
118
+ return {"raw": text}
119
+
120
+ # ------------------ Generador ------------------
121
+ def generate_simulation(user_input: str, include_iocs: bool, include_mitigation: bool):
122
+ api_key = os.environ.get("OPENAI_API_KEY")
123
+ if not api_key:
124
+ return "<p style='color:crimson'><b>Error:</b> OPENAI_API_KEY no configurada en Settings → Variables and secrets.</p>"
125
+
126
+ # construir prompt
127
+ prompt = PROMPT_TEMPLATE.format(input=user_input)
128
+
129
+ ok, out = call_openai_responses(prompt, api_key)
130
+ if not ok:
131
+ return f"<p style='color:crimson'><b>Error IA:</b> {out}</p>"
132
+
133
+ # filtro de seguridad: si la respuesta contiene patrones peligrosos, no se muestra
134
+ if contains_forbidden(out):
135
+ # devolver aviso y una versión segura: pedir al modelo reescribir de forma defensiva
136
+ safe_msg = ("La respuesta original fue bloqueada por contener contenido sensible que podría ser instructivo para ataques. "
137
+ "He realizado un bloqueo por seguridad. Intenta proporcionar más contexto defensivo o limpia el contenido y vuelve a intentarlo.")
138
+ return f"<p style='color:crimson'><b>Contenido bloqueado por seguridad:</b></p><p>{safe_msg}</p>"
139
+
140
+ parsed = safe_parse_json_from_model(out)
141
+
142
+ # Construir HTML de salida
143
+ html = []
144
+ html.append("<h3>Simulación Red Team (alto nivel)</h3>")
145
+ if isinstance(parsed, dict) and parsed.get("simulation"):
146
+ html.append(f"<p><b>Simulación:</b> {parsed['simulation']}</p>")
147
+ else:
148
+ sim = parsed.get("simulation") if isinstance(parsed, dict) else None
149
+ html.append(f"<p><b>Simulación:</b> {json.dumps(sim, ensure_ascii=False)}</p>")
150
+
151
+ if include_iocs:
152
+ html.append("<h4>Indicadores (IoCs) sugeridos</h4>")
153
+ iocs = parsed.get("iocs") if isinstance(parsed, dict) else None
154
+ if isinstance(iocs, list) and iocs:
155
+ html.append("<ul>")
156
+ for i in iocs:
157
+ html.append(f"<li>{i}</li>")
158
+ html.append("</ul>")
159
+ else:
160
+ html.append(f"<p>{json.dumps(iocs, ensure_ascii=False)}</p>")
161
+
162
+ if include_mitigation:
163
+ html.append("<h4>Contramedidas y mitigación</h4>")
164
+ mit = parsed.get("mitigations") if isinstance(parsed, dict) else None
165
+ if isinstance(mit, list) and mit:
166
+ html.append("<ul>")
167
+ for m in mit:
168
+ html.append(f"<li>{m}</li>")
169
+ html.append("</ul>")
170
+ else:
171
+ html.append(f"<p>{json.dumps(mit, ensure_ascii=False)}</p>")
172
+
173
+ html.append("<p style='font-size:0.9em;color:#bbb'>Nota: esta simulación es de alto nivel y educativa. No proporciona instrucciones de ataque. Use para mejorar defensas y detección.</p>")
174
+ return "\\n".join(html)
175
+
176
+ # ------------------ UI ------------------
177
+ with gr.Blocks(analytics_enabled=False) as demo:
178
+ gr.Markdown(\"## 🧯 Simulador Red Team (alto nivel) — Defender con IA\")
179
+ with gr.Row():
180
+ with gr.Column(scale=7):
181
+ inp = gr.Textbox(label=\"Pega aquí el correo RAW, URL o fragmento a analizar\", lines=20, placeholder=\"Pega cabeceras, cuerpo o URL completa\")
182
+ cb_iocs = gr.Checkbox(label=\"Incluir IoCs (indicadores) en la salida\", value=True)
183
+ cb_mit = gr.Checkbox(label=\"Incluir mitigaciones\", value=True)
184
+ btn = gr.Button(\"Simular ataque (alto nivel)\")
185
+ with gr.Column(scale=5):
186
+ out_html = gr.HTML(\"<i>Resultado aparecerá aquí</i>\")
187
+ btn.click(generate_simulation, inputs=[inp, cb_iocs, cb_mit], outputs=[out_html])
188
+
189
+ if __name__ == '__main__':
190
+ demo.launch(server_name='0.0.0.0', server_port=7860)
redteam_simulator_with_download.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # redteam_simulator_with_download.py
2
+ # Simulador Red Team (alto nivel, defensivo) - Gradio app + generación de reporte TXT
3
+ # Requisitos: gradio, requests
4
+ # Instrucciones: sube este archivo a tu Space y configura OPENAI_API_KEY en Settings -> Variables and secrets
5
+ import os
6
+ import json
7
+ import re
8
+ import time
9
+ import requests
10
+ import gradio as gr
11
+ from typing import Tuple
12
+
13
+ # ------------------ Config ------------------
14
+ OPENAI_API_URL = "https://api.openai.com/v1/responses"
15
+ OPENAI_MODEL_FALLBACK = ["gpt-4o-mini", "gpt-4o", "gpt-5-mini"]
16
+
17
+ PROMPT_TEMPLATE = \"\"\"Eres un asistente de ciberseguridad (red-team simulator) que debe simular a alto nivel cómo un atacante podría aprovechar el contenido que se le entrega (email, URL o fragmento). NORMAS (obligatorias):
18
+ 1. Proporciona sólo un resumen del objetivo del atacante y del vector. NO entregues instrucciones técnicas, comandos, código de exploit, payloads, o pasos paso-a-paso para cometer un ataque.
19
+ 2. Devuelve 3 secciones en JSON:
20
+ - \"simulation\": breve párrafo (1-3 frases) explicando la estrategia del atacante (alto nivel).
21
+ - \"iocs\": lista de indicadores accionables para detección (dominios, patrones de URL, encabezados sospechosos, extensiones).
22
+ - \"mitigations\": lista de contramedidas operativas (bloqueos, políticas, educación, verificación técnica).
23
+ 3. Si el material es insuficiente, indica qué faltaría.
24
+ 4. Limita la respuesta a lenguaje defensivo y educacional. NO ofrezcas código ni tácticas para explotar vulnerabilidades.
25
+ 5. Devuelve SOLO JSON válido (objetivo: {\"simulation\":..., \"iocs\":[...], \"mitigations\":[...]})
26
+
27
+ Contenido a analizar:
28
+ {input}
29
+ \"\"\"
30
+
31
+ FORBIDDEN_PATTERNS = [
32
+ r\"\\bexploit\\b\", r\"\\bpayload\\b\", r\"\\bmeterpreter\\b\", r\"\\bmsfconsole\\b\",
33
+ r\"curl\\b\", r\"wget\\b\", r\"sudo\\b\", r\"rm\\s+-rf\\b\", r\"reverse shell\\b\",
34
+ r\"exec\\b\", r\"bash -i\\b\", r\"nc\\b\", r\"ncat\\b\", r\"chmod\\b\", r\"chown\\b\",
35
+ r\"\\bsqlmap\\b\", r\"\\\\x\", r\"0x[0-9a-fA-F]{2,}\", r\"base64 -d\", r\"\\\\b\\\\$\\\\(\", r\"\\\\$\\\\{\"
36
+ ]
37
+ FORBIDDEN_REGEX = re.compile(\"|\".join(FORBIDDEN_PATTERNS), re.I)
38
+
39
+ def call_openai_responses(prompt: str, api_key: str, models=None, timeout: int = 20) -> Tuple[bool, str]:
40
+ if models is None:
41
+ models = OPENAI_MODEL_FALLBACK
42
+ headers = {\"Authorization\": f\"Bearer {api_key}\", \"Content-Type\": \"application/json\"}
43
+ for model in models:
44
+ payload = {\"model\": model, \"input\": prompt}
45
+ try:
46
+ r = requests.post(OPENAI_API_URL, headers=headers, json=payload, timeout=timeout)
47
+ except Exception as e:
48
+ return False, f\"Error de conexión al llamar a la API: {e}\"
49
+ if r.status_code == 200:
50
+ try:
51
+ j = r.json()
52
+ out = \"\"
53
+ if \"output\" in j:
54
+ if isinstance(j[\"output\"], list):
55
+ parts = []
56
+ for item in j[\"output\"]:
57
+ if isinstance(item, dict):
58
+ c = item.get(\"content\") or item.get(\"text\") or item.get(\"output_text\")
59
+ if isinstance(c, str):
60
+ parts.append(c)
61
+ elif isinstance(c, list):
62
+ for el in c:
63
+ if isinstance(el, dict):
64
+ txt = el.get(\"text\") or el.get(\"output_text\") or el.get(\"content\")
65
+ if txt:
66
+ parts.append(str(txt))
67
+ else:
68
+ parts.append(str(el))
69
+ out = \"\\n\".join(parts).strip()
70
+ elif isinstance(j[\"output\"], str):
71
+ out = j[\"output\"].strip()
72
+ if not out and \"choices\" in j and isinstance(j.get(\"choices\"), list) and j[\"choices\"]:
73
+ ch = j[\"choices\"][0]
74
+ out = ch.get(\"text\") or ch.get(\"message\", {}).get(\"content\", {}).get(\"text\") or \"\"
75
+ if not out:
76
+ out = json.dumps(j, ensure_ascii=False)[:4000]
77
+ return True, out
78
+ except Exception as e:
79
+ return False, f\"Error parseando respuesta de la API: {e}\"
80
+ else:
81
+ try:
82
+ ej = r.json()
83
+ msg = ej.get(\"error\", {}).get(\"message\") or ej.get(\"message\") or r.text
84
+ except Exception:
85
+ msg = r.text
86
+ if r.status_code == 401:
87
+ return False, \"AuthenticationError (401): OPENAI_API_KEY inválida o revocada.\"
88
+ if r.status_code == 429:
89
+ return False, \"RateLimitError (429): límite superado en OpenAI.\"
90
+ if isinstance(msg, str) and \"model\" in msg.lower():
91
+ continue
92
+ return False, f\"HTTP {r.status_code}: {msg}\"
93
+ return False, \"Ningún modelo disponible o permitido en la cuenta de OpenAI.\"
94
+
95
+ def contains_forbidden(text: str) -> bool:
96
+ if not text:
97
+ return False
98
+ return bool(FORBIDDEN_REGEX.search(text))
99
+
100
+ def safe_parse_json_from_model(text: str):
101
+ try:
102
+ return json.loads(text)
103
+ except Exception:
104
+ s = text.find('{')
105
+ e = text.rfind('}')
106
+ if s != -1 and e != -1 and e > s:
107
+ try:
108
+ return json.loads(text[s:e+1])
109
+ except Exception:
110
+ return {\"raw\": text}
111
+ return {\"raw\": text}
112
+
113
+ def generate_simulation(user_input: str, include_iocs: bool, include_mitigation: bool):
114
+ api_key = os.environ.get(\"OPENAI_API_KEY\")
115
+ if not api_key:
116
+ return \"<p style='color:crimson'><b>Error:</b> OPENAI_API_KEY no configurada en Settings → Variables and secrets.</p>\", \"\"
117
+
118
+ prompt = PROMPT_TEMPLATE.format(input=user_input)
119
+ ok, out = call_openai_responses(prompt, api_key)
120
+ if not ok:
121
+ return f\"<p style='color:crimson'><b>Error IA:</b> {out}</p>\", \"\"
122
+
123
+ if contains_forbidden(out):
124
+ safe_msg = (\"La respuesta original fue bloqueada por contener contenido sensible que podría ser instructivo para ataques. "
125
+ "He realizado un bloqueo por seguridad. Intenta proporcionar más contexto defensivo o limpia el contenido y vuelve a intentarlo.\")
126
+ return f\"<p style='color:crimson'><b>Contenido bloqueado por seguridad:</b></p><p>{safe_msg}</p>\", \"\"
127
+
128
+ parsed = safe_parse_json_from_model(out)
129
+
130
+ html = []
131
+ html.append(\"<h3>Simulación Red Team (alto nivel)</h3>\")
132
+ if isinstance(parsed, dict) and parsed.get(\"simulation\"):
133
+ html.append(f\"<p><b>Simulación:</b> {parsed['simulation']}</p>\")
134
+ else:
135
+ sim = parsed.get(\"simulation\") if isinstance(parsed, dict) else None
136
+ html.append(f\"<p><b>Simulación:</b> {json.dumps(sim, ensure_ascii=False)}</p>\")
137
+
138
+ if include_iocs:
139
+ html.append(\"<h4>Indicadores (IoCs) sugeridos</h4>\")
140
+ iocs = parsed.get(\"iocs\") if isinstance(parsed, dict) else None
141
+ if isinstance(iocs, list) and iocs:
142
+ html.append(\"<ul>\")
143
+ for i in iocs:
144
+ html.append(f\"<li>{i}</li>\")
145
+ html.append(\"</ul>\")
146
+ else:
147
+ html.append(f\"<p>{json.dumps(iocs, ensure_ascii=False)}</p>\")
148
+
149
+ if include_mitigation:
150
+ html.append(\"<h4>Contramedidas y mitigación</h4>\")
151
+ mit = parsed.get(\"mitigations\") if isinstance(parsed, dict) else None
152
+ if isinstance(mit, list) and mit:
153
+ html.append(\"<ul>\")
154
+ for m in mit:
155
+ html.append(f\"<li>{m}</li>\")
156
+ html.append(\"</ul>\")
157
+ else:
158
+ html.append(f\"<p>{json.dumps(mit, ensure_ascii=False)}</p>\")
159
+
160
+ html.append(\"<p style='font-size:0.9em;color:#bbb'>Nota: esta simulación es de alto nivel y educativa. No proporciona instrucciones de ataque. Use para mejorar defensas y detección.</p>\")
161
+ # devolvemos tambien el JSON parseado como string para uso en reporte
162
+ return \"\\n\".join(html), json.dumps(parsed, ensure_ascii=False, indent=2)
163
+
164
+ def generate_report(json_str: str, title: str = \"Reporte Red Team\") -> Tuple[str, str]:
165
+ \"\"\"Crea un archivo TXT con la simulación y mitigaciones y devuelve la ruta lista para descargar.\"\"\"
166
+ if not json_str:
167
+ return \"\", \"\"
168
+ try:
169
+ parsed = json.loads(json_str) if isinstance(json_str, str) else json_str
170
+ except Exception:
171
+ parsed = {\"raw\": str(json_str)}
172
+
173
+ timestamp = time.strftime(\"%Y%m%d_%H%M%S\")
174
+ filename = f\"/mnt/data/redteam_report_{timestamp}.txt\"
175
+ with open(filename, 'w', encoding='utf-8') as f:
176
+ f.write(f\"{title}\\nGenerated: {time.ctime()}\\n\\n\")
177
+ f.write(\"SIMULATION:\\n\")
178
+ sim = parsed.get(\"simulation\") if isinstance(parsed, dict) else None
179
+ f.write((sim or \"(no simulation)\") + \"\\n\\n\")
180
+ f.write(\"IOCS:\\n\")
181
+ for i in (parsed.get(\"iocs\") if isinstance(parsed, dict) and parsed.get(\"iocs\") else []):
182
+ f.write(f\"- {i}\\n\")
183
+ f.write(\"\\nMITIGATIONS:\\n\")
184
+ for m in (parsed.get(\"mitigations\") if isinstance(parsed, dict) and parsed.get(\"mitigations\") else []):
185
+ f.write(f\"- {m}\\n\")
186
+ f.write(\"\\nRAW:\\n\")
187
+ f.write(json.dumps(parsed, ensure_ascii=False, indent=2))
188
+ return filename, filename # return as two values (path, path) for compatibility
189
+
190
+ # ------------------ UI ------------------
191
+ with gr.Blocks(analytics_enabled=False) as demo:
192
+ gr.Markdown(\"## 🧯 Simulador Red Team (alto nivel) — Defender con IA\")
193
+ with gr.Row():
194
+ with gr.Column(scale=7):
195
+ inp = gr.Textbox(label=\"Pega aquí el correo RAW, URL o fragmento a analizar\", lines=20, placeholder=\"Pega cabeceras, cuerpo o URL completa\")
196
+ cb_iocs = gr.Checkbox(label=\"Incluir IoCs (indicadores) en la salida\", value=True)
197
+ cb_mit = gr.Checkbox(label=\"Incluir mitigaciones\", value=True)
198
+ btn = gr.Button(\"Simular ataque (alto nivel)\")
199
+ download_btn = gr.Button(\"Generar reporte (.txt)\")
200
+ with gr.Column(scale=5):
201
+ out_html = gr.HTML(\"<i>Resultado aparecerá aquí</i>\")
202
+ # componente invisible para guardar el JSON parseado
203
+ last_json = gr.Textbox(visible=False)
204
+ file_out = gr.File(label=\"Descargar reporte (.txt)\", visible=False)
205
+
206
+ # Al hacer click en Simular -> actualiza out_html y last_json (json string)
207
+ btn.click(generate_simulation, inputs=[inp, cb_iocs, cb_mit], outputs=[out_html, last_json])
208
+ # Al hacer click en Generar reporte -> crea archivo y lo muestra en file_out
209
+ download_btn.click(generate_report, inputs=[last_json, gr.Textbox(value=\"Reporte Red Team\", visible=False)], outputs=[file_out, file_out])
210
+
211
+ if __name__ == '__main__':
212
+ demo.launch(server_name='0.0.0.0', server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.44.1
2
+ requests
3
+ tldextract
4
+ dnspython
5
+ dkimpy
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.10