Tefifi commited on
Commit
7adf02c
·
0 Parent(s):

deploy inicial

Browse files
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── AIM Dashboard — Dockerfile ──────────────────────────────────────────────
2
+ # Imagen base: Python 3.11 slim para menor tamaño
3
+ FROM python:3.11-slim
4
+
5
+ # Metadatos
6
+ LABEL maintainer="tu-equipo@empresa.cl"
7
+ LABEL description="AIM Dashboard — Perfil de ciberseguridad para PyMEs"
8
+
9
+ # Variables de entorno
10
+ ENV PYTHONDONTWRITEBYTECODE=1 \
11
+ PYTHONUNBUFFERED=1 \
12
+ PORT=8050
13
+
14
+ # Directorio de trabajo dentro del contenedor
15
+ WORKDIR /app
16
+
17
+ # Instalar dependencias del sistema (necesarias para torch y lxml)
18
+ RUN apt-get update && apt-get install -y --no-install-recommends \
19
+ gcc \
20
+ g++ \
21
+ libxml2-dev \
22
+ libxslt-dev \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ # Copiar solo el requirements primero (aprovecha cache de Docker)
26
+ COPY requirements.txt .
27
+
28
+ # Instalar dependencias Python
29
+ # --no-cache-dir reduce el tamaño de imagen
30
+ RUN pip install --upgrade pip && \
31
+ pip install --no-cache-dir -r requirements.txt && \
32
+ pip install --no-cache-dir gunicorn
33
+
34
+ # Copiar el código del proyecto
35
+ COPY . .
36
+
37
+ # Descargar recursos NLTK necesarios durante el build
38
+ RUN python -c "import nltk; nltk.download('punkt', quiet=True); nltk.download('punkt_tab', quiet=True)"
39
+
40
+ # Puerto que expone la app
41
+ EXPOSE 8050
42
+
43
+ # Comando de inicio con Gunicorn
44
+ # - 2 workers (ajustar según CPU disponibles: 2 * num_cpus + 1)
45
+ # - timeout 300s porque el análisis NLP puede tardar varios minutos
46
+ # - El objeto WSGI de Dash se llama "server" dentro de app.py
47
+ CMD ["gunicorn", \
48
+ "--workers", "2", \
49
+ "--timeout", "300", \
50
+ "--bind", "0.0.0.0:8050", \
51
+ "--log-level", "info", \
52
+ "--access-logfile", "-", \
53
+ "--error-logfile", "-", \
54
+ "app:server"]
Modelo_Pymes.pkl ADDED
Binary file (1.81 kB). View file
 
README.md ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AIM Dashboard
2
+
3
+ **Herramienta de clasificación de perfil de ciberseguridad para PyMEs** basada en la
4
+ Tríada AIM (Awareness, Infrastructure, Management).
5
+
6
+ ---
7
+
8
+ ## Cómo verlo en localhost
9
+
10
+ ### Opción A — Python directo (más rápido para probar)
11
+
12
+ **1. Descomprime el proyecto y entra a la carpeta**
13
+ ```bash
14
+ cd aim_dashboard
15
+ ```
16
+
17
+ **2. Crea un entorno virtual**
18
+ ```bash
19
+ python -m venv venv
20
+
21
+ # macOS / Linux:
22
+ source venv/bin/activate
23
+
24
+ # Windows:
25
+ venv\Scripts\activate
26
+ ```
27
+
28
+ **3. Instala las dependencias**
29
+ ```bash
30
+ pip install -r requirements.txt
31
+ ```
32
+ La primera vez tarda varios minutos porque descarga modelos de ~1.5 GB (PyTorch, BART, DeBERTa, MPNet).
33
+
34
+ **4. Verifica que el modelo esté en la carpeta raíz**
35
+ ```
36
+ aim_dashboard/
37
+ ├── app.py
38
+ ├── Modelo_Pymes.pkl ← debe estar aquí
39
+ └── ...
40
+ ```
41
+
42
+ **5. Ejecuta**
43
+ ```bash
44
+ python app.py
45
+ ```
46
+
47
+ **6. Abre en el navegador:**
48
+ ```
49
+ http://localhost:8050
50
+ ```
51
+
52
+ ---
53
+
54
+ ### Opción B — Docker (recomendado para producción)
55
+
56
+ Requisitos: Docker Desktop instalado.
57
+
58
+ ```bash
59
+ cd aim_dashboard
60
+ docker-compose up --build
61
+ ```
62
+ La primera vez tarda ~5-10 minutos. Luego abre `http://localhost:8050`.
63
+
64
+ Para detener: `docker-compose down`
65
+
66
+ ---
67
+
68
+ ### Opción C — Gunicorn (producción sin Docker)
69
+
70
+ ```bash
71
+ pip install gunicorn
72
+ gunicorn --workers 2 --timeout 300 --bind 0.0.0.0:8050 app:server
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Estructura del proyecto
78
+
79
+ ```
80
+ aim_dashboard/
81
+
82
+ ├── app.py <- Punto de entrada principal
83
+ ├── Dockerfile <- Imagen Docker
84
+ ├── docker-compose.yml <- Orquestación
85
+ ├── requirements.txt
86
+ ├── Modelo_Pymes.pkl <- Modelo K-Means (NO subir a repositorios públicos)
87
+
88
+ ├── assets/
89
+ │ └── style.css <- Estilos (Dash los carga automáticamente)
90
+
91
+ ├── data/
92
+ │ └── definitions.py <- Constantes y textos estáticos
93
+
94
+ ├── layouts/
95
+ │ └── pages.py <- Pantallas de home y perfiles
96
+
97
+ ├── callbacks/
98
+ │ └── navegacion.py <- Análisis en background, barra de progreso, routing
99
+
100
+ └── logic/
101
+ ├── extractor.py <- Scraping y clasificación semántica
102
+ ├── modelo.py <- Vectorización NLP + predicción K-Means
103
+ └── venn.py <- Diagrama de Venn
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Flujo de la aplicación
109
+
110
+ ```
111
+ URL ingresada
112
+ -> ExtractorMVD: scraping de páginas relevantes (Paso 1)
113
+ -> clasificar_inteligente: clasifica en MISION/VISION/DESCRIPCION
114
+ -> traducción ES->EN (Paso 2)
115
+ -> vectorización NLP: MPNet + DeBERTa + BART zero-shot (Paso 3)
116
+ -> KMeans.predict: cluster 0-4 -> Perfil 1-5
117
+ -> Layout del perfil con fortalezas, debilidades y Venn
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Problemas comunes
123
+
124
+ | Problema | Solución |
125
+ |---|---|
126
+ | `FileNotFoundError: Modelo_Pymes.pkl` | Pon el `.pkl` junto a `app.py` |
127
+ | La app tarda en arrancar | Normal: los modelos NLP se cargan al primer análisis |
128
+ | Error de red al analizar sitio | Verifica que la URL incluya `https://` |
129
+ | `Port 8050 already in use` | Cambia el puerto en `app.py` o mata el proceso: `lsof -ti:8050 \| xargs kill` |
130
+ | Docker con poca memoria | Asigna al menos 4 GB de RAM en Docker Desktop > Settings > Resources |
131
+
132
+ ---
133
+
134
+ ## Notas para producción
135
+
136
+ - El análisis NLP corre en un **thread separado** para no bloquear la UI. Con múltiples
137
+ usuarios simultáneos considera usar **Celery + Redis** para una cola de trabajos.
138
+ - Los modelos de HuggingFace se cachean en `~/.cache/huggingface/`. En Docker se
139
+ descargan al construir la imagen.
140
+ - Para exponer con dominio real, descomenta el servicio `nginx` en `docker-compose.yml`
141
+ y configura tu dominio.
app.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AIM Dashboard — Punto de entrada principal.
3
+ Ejecutar con: python app.py
4
+ """
5
+
6
+ import logging
7
+ import dash
8
+ import dash_bootstrap_components as dbc
9
+ from dash import dcc, html
10
+
11
+ from layouts.pages import layout_home, all_layouts
12
+ from callbacks.navegacion import registrar_callbacks
13
+
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
17
+ )
18
+
19
+ app = dash.Dash(
20
+ __name__,
21
+ suppress_callback_exceptions=True,
22
+ external_stylesheets=[dbc.themes.BOOTSTRAP],
23
+ meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}],
24
+ )
25
+ app.title = "AIM Dashboard"
26
+
27
+ # ── Panel de progreso en el layout raíz ──────────────────────────────────────
28
+ # Debe estar aquí (no en page-content) para que los callbacks siempre
29
+ # puedan escribir en él sin importar qué página está cargada.
30
+ # Se muestra como barra fija en la parte inferior de la pantalla.
31
+ panel_progreso = html.Div(
32
+ id="panel-progreso",
33
+ style={"display": "none"},
34
+ children=[
35
+ html.Div(
36
+ style={
37
+ "position": "fixed",
38
+ "bottom": "0", "left": "0", "right": "0",
39
+ "zIndex": "1000",
40
+ "background": "#ffffff",
41
+ "borderTop": "3px solid #1c3160",
42
+ "boxShadow": "0 -4px 24px rgba(28,49,96,0.15)",
43
+ "padding": "18px 36px 20px",
44
+ },
45
+ children=[
46
+ html.Div(
47
+ style={"maxWidth": "860px", "margin": "0 auto"},
48
+ children=[
49
+ html.Div(
50
+ style={"display": "flex", "alignItems": "center",
51
+ "justifyContent": "space-between", "marginBottom": "10px"},
52
+ children=[
53
+ html.Span("Analizando perfil AIM", style={
54
+ "fontSize": "0.78rem", "fontWeight": "700",
55
+ "letterSpacing": "1.2px", "textTransform": "uppercase",
56
+ "color": "#1c3160",
57
+ }),
58
+ html.Span(id="progreso-pct", style={
59
+ "fontSize": "1rem", "fontWeight": "700",
60
+ "color": "#b87a2a",
61
+ }),
62
+ ],
63
+ ),
64
+ html.Div(
65
+ style={
66
+ "background": "#e8ecf2",
67
+ "borderRadius": "999px",
68
+ "height": "14px",
69
+ "marginBottom": "14px",
70
+ "overflow": "hidden",
71
+ "border": "2px solid #b0bacb",
72
+ "boxShadow": "inset 0 1px 3px rgba(0,0,0,0.1)",
73
+ },
74
+ children=[
75
+ html.Div(
76
+ id="progreso-bar",
77
+ style={
78
+ "height": "100%", "borderRadius": "999px",
79
+ "background": "linear-gradient(90deg, #b87a2a, #d4a843)",
80
+ "width": "0%",
81
+ "transition": "width 0.8s ease",
82
+ "boxShadow": "0 1px 4px rgba(184,122,42,0.4)",
83
+ },
84
+ )
85
+ ],
86
+ ),
87
+ html.Div(id="progreso-container"),
88
+ html.Div(id="error-msg", children=""),
89
+ ],
90
+ )
91
+ ],
92
+ )
93
+ ],
94
+ )
95
+
96
+ # ── Layout raíz ───────────────────────────────────────────────────────────────
97
+ app.layout = html.Div([
98
+ dcc.Location(id="url", refresh=False),
99
+ dcc.Store(id="store_name", storage_type="session"),
100
+ dcc.Store(id="store_link", storage_type="session"),
101
+ dcc.Interval(id="interval_progress", interval=600, n_intervals=0, disabled=True),
102
+ html.Div(id="page-content"),
103
+ panel_progreso,
104
+ ])
105
+
106
+ # ── Routing ───────────────────────────────────────────────────────────────────
107
+ @app.callback(
108
+ dash.Output("page-content", "children"),
109
+ dash.Input("url", "pathname"),
110
+ )
111
+ def mostrar_pagina(pathname: str):
112
+ rutas = {
113
+ "/profile_1": all_layouts[0],
114
+ "/profile_2": all_layouts[1],
115
+ "/profile_3": all_layouts[2],
116
+ "/profile_4": all_layouts[3],
117
+ "/profile_5": all_layouts[4],
118
+ }
119
+ return rutas.get(pathname, layout_home)
120
+
121
+ registrar_callbacks(app)
122
+
123
+ server = app.server # para Gunicorn: gunicorn app:server
124
+
125
+ if __name__ == "__main__":
126
+ app.run(debug=False, host="0.0.0.0", port=8050)
assets/style.css ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ AIM Dashboard — Rediseño accesible
3
+ Principios: alto contraste, tipografía generosa, espaciado amplio,
4
+ paleta sobria institucional (azul profundo + blanco cálido + ámbar)
5
+ ============================================================ */
6
+
7
+ /* ---------- Fuente legible y serif elegante para encabezados ---------- */
8
+ @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;600;700&family=Source+Sans+3:wght@400;500;600;700&display=swap');
9
+
10
+ *, *::before, *::after { box-sizing: border-box; }
11
+
12
+ body {
13
+ margin: 0;
14
+ font-family: 'Source Sans 3', 'Segoe UI', Georgia, sans-serif;
15
+ background-color: #f4f1eb; /* blanco cálido, no agresivo */
16
+ color: #1c2233; /* azul muy oscuro, no negro puro */
17
+ min-height: 100vh;
18
+ font-size: 17px; /* base más grande que el estándar */
19
+ line-height: 1.7;
20
+ }
21
+
22
+ /* ---------- Variables de paleta institucional ---------- */
23
+ :root {
24
+ --bg: #f4f1eb; /* fondo principal: crema cálida */
25
+ --bg-card: #ffffff; /* tarjetas blancas */
26
+ --bg-card-alt: #eef2f7; /* tarjetas secundarias: azul muy pálido */
27
+ --border: #c8d0df; /* bordes suaves */
28
+ --border-dark: #8a96aa; /* bordes énfasis */
29
+
30
+ --navy: #1c3160; /* azul marino profundo — color primario */
31
+ --navy-mid: #2d4f8a; /* azul medio para hover */
32
+ --navy-light: #dde6f5; /* azul muy claro para fondos */
33
+
34
+ --amber: #b87a2a; /* ámbar cálido — acento positivo */
35
+ --amber-light: #fdf3e3; /* fondo ámbar suave */
36
+
37
+ --success: #1e6b45; /* verde bosque oscuro */
38
+ --success-bg: #e6f4ed;
39
+ --success-border: #7dc4a0;
40
+
41
+ --danger: #8b1c1c; /* rojo ladrillo oscuro */
42
+ --danger-bg: #fdeaea;
43
+ --danger-border: #d48585;
44
+
45
+ --text-primary: #1c2233;
46
+ --text-secondary:#3d4a60;
47
+ --text-muted: #6b7a96;
48
+ --text-light: #9aa3b5;
49
+
50
+ --radius: 10px;
51
+ --radius-lg: 14px;
52
+ --shadow-sm: 0 1px 4px rgba(28,49,96,0.08);
53
+ --shadow: 0 3px 16px rgba(28,49,96,0.12);
54
+ --shadow-lg: 0 8px 32px rgba(28,49,96,0.16);
55
+ }
56
+
57
+ /* ---------- Contenedor principal ---------- */
58
+ .aim-container {
59
+ max-width: 1340px;
60
+ margin: 0 auto;
61
+ padding: 28px 36px;
62
+ }
63
+
64
+ /* ---------- Header ---------- */
65
+ .aim-header {
66
+ text-align: center;
67
+ padding: 48px 0 40px;
68
+ border-bottom: 2px solid var(--border);
69
+ margin-bottom: 36px;
70
+ }
71
+
72
+ .aim-header h1 {
73
+ font-family: 'Source Serif 4', Georgia, serif;
74
+ font-size: 2.6rem;
75
+ font-weight: 700;
76
+ color: var(--navy);
77
+ letter-spacing: -0.3px;
78
+ margin: 0 0 10px;
79
+ }
80
+
81
+ .aim-header .subtitle {
82
+ color: var(--text-secondary);
83
+ font-size: 1.15rem;
84
+ margin: 0;
85
+ font-weight: 400;
86
+ }
87
+
88
+ /* ---------- Layout de dos columnas ---------- */
89
+ .aim-two-col {
90
+ display: flex;
91
+ gap: 28px;
92
+ align-items: flex-start;
93
+ }
94
+
95
+ .aim-two-col .col-left { flex: 1; min-width: 0; }
96
+ .aim-two-col .col-right { flex: 1; min-width: 0; text-align: center; }
97
+
98
+ @media (max-width: 960px) {
99
+ .aim-two-col { flex-direction: column; }
100
+ .aim-container { padding: 20px 18px; }
101
+ }
102
+
103
+ /* ---------- Cards ---------- */
104
+ .aim-card {
105
+ background: var(--bg-card);
106
+ border: 1.5px solid var(--border);
107
+ border-radius: var(--radius-lg);
108
+ padding: 28px 32px;
109
+ margin-bottom: 20px;
110
+ box-shadow: var(--shadow-sm);
111
+ }
112
+
113
+ .aim-card-label {
114
+ font-size: 0.78rem;
115
+ font-weight: 700;
116
+ letter-spacing: 1.4px;
117
+ text-transform: uppercase;
118
+ color: var(--navy);
119
+ margin-bottom: 10px;
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 8px;
123
+ }
124
+
125
+ .aim-card-label::before {
126
+ content: '';
127
+ display: inline-block;
128
+ width: 4px;
129
+ height: 16px;
130
+ background: var(--amber);
131
+ border-radius: 2px;
132
+ }
133
+
134
+ /* ---------- Inputs ---------- */
135
+ .aim-input {
136
+ width: 100%;
137
+ padding: 14px 18px;
138
+ background: #fafbfd;
139
+ border: 2px solid var(--border);
140
+ border-radius: var(--radius);
141
+ color: var(--text-primary);
142
+ font-size: 1.05rem;
143
+ font-family: inherit;
144
+ transition: border-color 0.2s, box-shadow 0.2s;
145
+ margin-bottom: 18px;
146
+ }
147
+
148
+ .aim-input:focus {
149
+ outline: none;
150
+ border-color: var(--navy-mid);
151
+ box-shadow: 0 0 0 3px rgba(45,79,138,0.15);
152
+ background: #fff;
153
+ }
154
+
155
+ .aim-input::placeholder { color: var(--text-light); }
156
+
157
+ /* Labels de formulario */
158
+ label {
159
+ display: block;
160
+ font-weight: 600;
161
+ font-size: 1rem;
162
+ color: var(--text-secondary);
163
+ margin-bottom: 6px;
164
+ }
165
+
166
+ /* ---------- Botón principal ---------- */
167
+ .aim-btn-primary {
168
+ width: 100%;
169
+ padding: 16px 28px;
170
+ background: var(--navy);
171
+ color: #ffffff;
172
+ border: none;
173
+ border-radius: var(--radius);
174
+ font-size: 1.08rem;
175
+ font-weight: 700;
176
+ font-family: inherit;
177
+ letter-spacing: 0.3px;
178
+ cursor: pointer;
179
+ transition: background 0.2s, box-shadow 0.2s, transform 0.1s;
180
+ margin-top: 8px;
181
+ }
182
+
183
+ .aim-btn-primary:hover {
184
+ background: var(--navy-mid);
185
+ box-shadow: 0 4px 18px rgba(28,49,96,0.28);
186
+ transform: translateY(-1px);
187
+ }
188
+
189
+ .aim-btn-primary:active { transform: translateY(0); }
190
+
191
+ /* Botón secundario (volver) */
192
+ .aim-btn-secondary {
193
+ padding: 10px 22px;
194
+ background: var(--bg-card);
195
+ color: var(--navy);
196
+ border: 2px solid var(--navy);
197
+ border-radius: var(--radius);
198
+ font-size: 0.97rem;
199
+ font-weight: 700;
200
+ font-family: inherit;
201
+ cursor: pointer;
202
+ transition: background 0.2s;
203
+ }
204
+
205
+ .aim-btn-secondary:hover {
206
+ background: var(--navy-light);
207
+ }
208
+
209
+ /* ---------- Tags de dominio ---------- */
210
+ .domain-tag {
211
+ display: inline-block;
212
+ padding: 5px 13px;
213
+ border-radius: 999px;
214
+ font-size: 0.85rem;
215
+ font-weight: 600;
216
+ margin: 3px 3px;
217
+ }
218
+
219
+ .domain-tag.strength {
220
+ background: var(--success-bg);
221
+ color: var(--success);
222
+ border: 1.5px solid var(--success-border);
223
+ }
224
+
225
+ .domain-tag.weakness {
226
+ background: var(--danger-bg);
227
+ color: var(--danger);
228
+ border: 1.5px solid var(--danger-border);
229
+ }
230
+
231
+ /* ---------- Texto de definición ---------- */
232
+ .aim-definition {
233
+ color: var(--text-secondary);
234
+ font-size: 1rem;
235
+ line-height: 1.8;
236
+ }
237
+
238
+ .aim-definition p { margin: 0 0 12px; }
239
+ .aim-definition strong { color: var(--navy); }
240
+
241
+ /* ---------- Imagen Venn ---------- */
242
+ .venn-img {
243
+ width: 100%;
244
+ max-width: 100%;
245
+ height: auto;
246
+ border-radius: var(--radius);
247
+ box-shadow: var(--shadow);
248
+ border: 1.5px solid var(--border);
249
+ display: block;
250
+ }
251
+
252
+ /* ---------- Alerta de error ---------- */
253
+ .aim-error {
254
+ background: var(--danger-bg);
255
+ border: 1.5px solid var(--danger-border);
256
+ border-radius: var(--radius);
257
+ padding: 14px 18px;
258
+ color: var(--danger);
259
+ font-size: 0.95rem;
260
+ margin-top: 12px;
261
+ font-weight: 500;
262
+ }
263
+
264
+ /* ---------- Navegación flotante (botón volver) ---------- */
265
+ .aim-nav-home {
266
+ position: fixed;
267
+ top: 18px;
268
+ right: 24px;
269
+ z-index: 999;
270
+ }
271
+
272
+ /* ---------- Panel de progreso ---------- */
273
+ #panel-progreso .aim-card {
274
+ border-left: 4px solid var(--navy);
275
+ background: var(--bg-card);
276
+ }
277
+
278
+ /* ---------- Acordeón Details/Summary ---------- */
279
+ details {
280
+ border-radius: var(--radius);
281
+ margin-bottom: 10px;
282
+ overflow: hidden;
283
+ border: 1.5px solid var(--border);
284
+ transition: box-shadow 0.2s;
285
+ }
286
+
287
+ details:hover {
288
+ box-shadow: var(--shadow-sm);
289
+ }
290
+
291
+ details[open] {
292
+ box-shadow: var(--shadow);
293
+ }
294
+
295
+ details summary {
296
+ outline: none;
297
+ list-style: none;
298
+ padding: 14px 18px;
299
+ cursor: pointer;
300
+ font-weight: 600;
301
+ font-size: 0.98rem;
302
+ user-select: none;
303
+ transition: background 0.15s;
304
+ }
305
+
306
+ details summary::-webkit-details-marker { display: none; }
307
+ details summary::marker { display: none; }
308
+
309
+ details summary:hover {
310
+ filter: brightness(0.97);
311
+ }
312
+
313
+ details[open] > div {
314
+ animation: fadeSlide 0.22s ease;
315
+ }
316
+
317
+ @keyframes fadeSlide {
318
+ from { opacity: 0; transform: translateY(-5px); }
319
+ to { opacity: 1; transform: translateY(0); }
320
+ }
321
+
322
+ /* ---------- Cabecera de columna de dominios ---------- */
323
+ .aim-domain-col-header {
324
+ font-size: 0.78rem;
325
+ font-weight: 700;
326
+ letter-spacing: 1.3px;
327
+ text-transform: uppercase;
328
+ margin-bottom: 14px;
329
+ padding-bottom: 12px;
330
+ border-bottom: 2px solid;
331
+ display: flex;
332
+ align-items: center;
333
+ gap: 9px;
334
+ }
335
+
336
+ /* ---------- Grid 3 columnas responsive ---------- */
337
+ @media (max-width: 1100px) {
338
+ .aim-profile-grid { grid-template-columns: 1fr 1fr !important; }
339
+ }
340
+ @media (max-width: 680px) {
341
+ .aim-profile-grid { grid-template-columns: 1fr !important; }
342
+ .aim-header h1 { font-size: 1.9rem; }
343
+ }
344
+
345
+ /* ---------- Scrollbar suavizado ---------- */
346
+ ::-webkit-scrollbar { width: 8px; }
347
+ ::-webkit-scrollbar-track { background: var(--bg); }
348
+ ::-webkit-scrollbar-thumb {
349
+ background: var(--border-dark);
350
+ border-radius: 4px;
351
+ }
352
+ ::-webkit-scrollbar-thumb:hover { background: var(--navy-mid); }
353
+
354
+ /* ---------- Selección de texto ---------- */
355
+ ::selection {
356
+ background: var(--navy-light);
357
+ color: var(--navy);
358
+ }
359
+
360
+ /* ---------- Zoom modal del diagrama Venn ---------- */
361
+ .venn-clickable {
362
+ transition: transform 0.2s, box-shadow 0.2s;
363
+ }
364
+ .venn-clickable:hover {
365
+ transform: scale(1.02);
366
+ box-shadow: 0 6px 24px rgba(28,49,96,0.2);
367
+ }
368
+
369
+ .venn-modal-overlay {
370
+ position: fixed;
371
+ inset: 0;
372
+ background: rgba(10, 15, 30, 0.75);
373
+ z-index: 9999;
374
+ display: flex !important;
375
+ align-items: center;
376
+ justify-content: center;
377
+ padding: 24px;
378
+ backdrop-filter: blur(3px);
379
+ animation: fadeIn 0.2s ease;
380
+ }
381
+
382
+ .venn-modal-overlay[style*="display: none"] {
383
+ display: none !important;
384
+ }
385
+
386
+ @keyframes fadeIn {
387
+ from { opacity: 0; }
388
+ to { opacity: 1; }
389
+ }
390
+
391
+ .venn-modal-box {
392
+ background: #fff;
393
+ border-radius: 14px;
394
+ padding: 24px;
395
+ max-width: 820px;
396
+ width: 100%;
397
+ box-shadow: 0 20px 60px rgba(0,0,0,0.4);
398
+ position: relative;
399
+ animation: scaleIn 0.2s ease;
400
+ }
401
+
402
+ @keyframes scaleIn {
403
+ from { transform: scale(0.92); opacity: 0; }
404
+ to { transform: scale(1); opacity: 1; }
405
+ }
406
+
407
+ .venn-modal-img {
408
+ width: 100%;
409
+ height: auto;
410
+ border-radius: 8px;
411
+ display: block;
412
+ }
413
+
414
+ .venn-modal-close {
415
+ position: absolute;
416
+ top: 12px;
417
+ right: 12px;
418
+ background: var(--navy);
419
+ color: #fff;
420
+ border: none;
421
+ border-radius: 8px;
422
+ padding: 8px 16px;
423
+ font-size: 0.9rem;
424
+ font-weight: 700;
425
+ font-family: inherit;
426
+ cursor: pointer;
427
+ transition: background 0.2s;
428
+ }
429
+ .venn-modal-close:hover {
430
+ background: var(--navy-mid);
431
+ }
callbacks/__init__.py ADDED
File without changes
callbacks/navegacion.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Callbacks del dashboard AIM.
3
+ - error-msg, progreso-container, progreso-bar, progreso-pct viven en pages.py
4
+ dentro del panel inline bajo el botón.
5
+ - El panel se muestra/oculta via Output("panel-progreso", "style").
6
+ """
7
+
8
+ import logging
9
+ import threading
10
+ import time
11
+
12
+ import dash
13
+ from dash import ctx, html
14
+ from dash.dependencies import Input, Output, State, ALL
15
+
16
+ from logic.extractor import ExtractorMVD
17
+ from logic.modelo import obtener_perfil
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # ── Estado compartido ─────────────────────────────────────────────────────────
22
+ _estado: dict = {"paso": 0, "pct": 0, "perfil": None, "error": None}
23
+ _lock = threading.Lock()
24
+
25
+ # Porcentaje base al inicio de cada paso
26
+ _PCT_BASE = {1: 0, 2: 30, 3: 50, 4: 100}
27
+ # Porcentaje máximo al que puede llegar solo con el timer (sin avanzar de paso)
28
+ _PCT_TECHO = {1: 28, 2: 48, 3: 95, 4: 100}
29
+
30
+ PASOS_LABELS = ["Extrayendo", "Procesando", "Clasificando", "Listo"]
31
+ PASOS_DETALLE = [
32
+ "Extrayendo contenido del sitio web...",
33
+ "Traduciendo y procesando texto...",
34
+ "Clasificando perfil con el modelo NLP...",
35
+ "¡Análisis completado!",
36
+ ]
37
+
38
+ _PANEL_VISIBLE = {"display": "block"}
39
+ _PANEL_OCULTO = {"display": "none"}
40
+
41
+
42
+ # ── Thread principal de análisis ──────────────────────────────────────────────
43
+
44
+ def _correr_analisis(link: str, nombre: str) -> None:
45
+ global _estado
46
+ try:
47
+ # Paso 1 — Extracción web
48
+ with _lock:
49
+ _estado = {"paso": 1, "pct": 0, "perfil": None, "error": None}
50
+
51
+ extractor = ExtractorMVD(url=link, nombre=nombre)
52
+ extractor.navegar_y_extraer()
53
+ texto = extractor.clasificar_inteligente()
54
+
55
+ if not texto:
56
+ with _lock:
57
+ _estado["error"] = {
58
+ "tipo": "extraccion",
59
+ "titulo": "No se encontró contenido relevante",
60
+ "detalle": (
61
+ "El sitio web no contiene texto relacionado con misión, visión "
62
+ "o descripción organizacional que el modelo pueda analizar."
63
+ ),
64
+ "sugerencia": "Prueba con la URL de la página 'Quiénes somos' o 'Acerca de' de la empresa.",
65
+ }
66
+ _estado["paso"] = -1
67
+ return
68
+
69
+ # Paso 2 — Traducción
70
+ with _lock:
71
+ _estado["paso"] = 2
72
+ _estado["pct"] = _PCT_BASE[2]
73
+
74
+ # Paso 3 — Clasificación NLP + K-Means
75
+ with _lock:
76
+ _estado["paso"] = 3
77
+ _estado["pct"] = _PCT_BASE[3]
78
+
79
+ perfil = obtener_perfil(texto) + 1 # 0-4 → 1-5
80
+
81
+ # Paso 4 — Listo
82
+ with _lock:
83
+ _estado["paso"] = 4
84
+ _estado["pct"] = 100
85
+ _estado["perfil"] = perfil
86
+
87
+ logger.info("Perfil: %d | '%s'", perfil, link)
88
+
89
+ except FileNotFoundError as e:
90
+ logger.error("Modelo no encontrado: %s", e)
91
+ with _lock:
92
+ _estado["error"] = {
93
+ "tipo": "modelo",
94
+ "titulo": "Modelo no encontrado",
95
+ "detalle": "El archivo Modelo_Pymes.pkl no está en la carpeta del proyecto.",
96
+ "sugerencia": "Verifica que Modelo_Pymes.pkl esté en la carpeta raíz junto a app.py.",
97
+ }
98
+ _estado["paso"] = -1
99
+ except Exception as e:
100
+ logger.exception("Error procesando '%s'", link)
101
+ err_str = str(e)
102
+ # Clasificar el error según su contenido
103
+ if "connection" in err_str.lower() or "timeout" in err_str.lower() or "urlopen" in err_str.lower():
104
+ tipo = "red"
105
+ titulo = "Error de conexión"
106
+ detalle = f"No se pudo conectar al sitio: {err_str}"
107
+ sugiere = "Verifica que la URL sea correcta y que el sitio esté accesible desde tu red."
108
+ elif "ssl" in err_str.lower() or "certificate" in err_str.lower():
109
+ tipo = "ssl"
110
+ titulo = "Error de certificado SSL"
111
+ detalle = "El sitio tiene un certificado de seguridad inválido o expirado."
112
+ sugiere = "Intenta con la versión http:// en lugar de https://, o prueba con otro sitio."
113
+ elif "403" in err_str or "401" in err_str or "forbidden" in err_str.lower():
114
+ tipo = "acceso"
115
+ titulo = "Acceso denegado por el sitio"
116
+ detalle = "El servidor rechazó la solicitud (error 403/401)."
117
+ sugiere = "Este sitio bloquea el acceso automatizado. Prueba con otro sitio o con la URL de una subpágina."
118
+ elif "404" in err_str or "not found" in err_str.lower():
119
+ tipo = "url"
120
+ titulo = "Página no encontrada"
121
+ detalle = f"La URL ingresada no existe o fue movida: {err_str}"
122
+ sugiere = "Verifica que la URL sea correcta e incluya https://"
123
+ elif "runtime" in err_str.lower() or "tensor" in err_str.lower():
124
+ tipo = "modelo"
125
+ titulo = "Error en el modelo NLP"
126
+ detalle = "Ocurrió un error interno al procesar el texto con los modelos de lenguaje."
127
+ sugiere = "Intenta de nuevo. Si el error persiste, el texto extraído puede ser demasiado corto o inusual."
128
+ else:
129
+ tipo = "desconocido"
130
+ titulo = "Error inesperado"
131
+ detalle = err_str
132
+ sugiere = "Intenta de nuevo o prueba con un sitio web diferente."
133
+
134
+ with _lock:
135
+ _estado["error"] = {
136
+ "tipo": tipo, "titulo": titulo,
137
+ "detalle": detalle, "sugerencia": sugiere,
138
+ }
139
+ _estado["paso"] = -1
140
+
141
+
142
+ # ── Thread de animación de porcentaje ─────────────────────────────────────────
143
+
144
+ def _animar_porcentaje() -> None:
145
+ """Incrementa el % gradualmente dentro del techo de cada paso."""
146
+ global _estado
147
+ while True:
148
+ time.sleep(0.8)
149
+ with _lock:
150
+ paso = _estado["paso"]
151
+ if paso <= 0 or paso == 4:
152
+ break
153
+ techo = _PCT_TECHO.get(paso, 95)
154
+ pct = _estado["pct"]
155
+ if pct < techo:
156
+ # Avance más rápido al principio, más lento cerca del techo
157
+ incremento = max(1, int((techo - pct) * 0.07))
158
+ _estado["pct"] = min(pct + incremento, techo)
159
+
160
+
161
+ # ── Componente visual de pasos ────────────────────────────────────────────────
162
+
163
+ def _pasos_html(paso: int) -> html.Div:
164
+ items = []
165
+ for i, label in enumerate(PASOS_LABELS, start=1):
166
+ if paso == -1:
167
+ estado = "error" if i == 1 else "pendiente"
168
+ elif i < paso:
169
+ estado = "completado"
170
+ elif i == paso:
171
+ estado = "activo"
172
+ else:
173
+ estado = "pendiente"
174
+
175
+ color = {
176
+ "completado": "#1e6b45", # verde bosque
177
+ "activo": "#1c3160", # azul marino
178
+ "pendiente": "#6b7a96", # gris medio visible en claro
179
+ "error": "#8b1c1c", # rojo ladrillo
180
+ }[estado]
181
+ icono = "✔" if estado == "completado" else ("✖" if estado == "error" else str(i))
182
+
183
+ items.append(html.Div(
184
+ style={"display": "flex", "flexDirection": "column",
185
+ "alignItems": "center", "gap": "5px", "flex": "1"},
186
+ children=[
187
+ html.Div(icono, style={
188
+ "width": "32px", "height": "32px", "borderRadius": "50%",
189
+ "border": f"2px solid {color}", "color": color,
190
+ "display": "flex", "alignItems": "center", "justifyContent": "center",
191
+ "fontWeight": "700", "fontSize": "0.8rem",
192
+ "background": "rgba(28,49,96,0.1)" if estado == "activo" else "transparent",
193
+ "boxShadow": "0 0 0 4px rgba(28,49,96,0.12)" if estado == "activo" else "none",
194
+ }),
195
+ html.Span(label, style={
196
+ "fontSize": "0.68rem", "color": color, "fontWeight": "600",
197
+ }),
198
+ ],
199
+ ))
200
+ if i < len(PASOS_LABELS):
201
+ items.append(html.Div(style={
202
+ "flex": "2", "height": "2px", "marginTop": "-17px",
203
+ "background": "#1e6b45" if i < paso else "#b0bacb",
204
+ "transition": "background 0.5s",
205
+ }))
206
+
207
+ desc_color = "#f87171" if paso == -1 else "#8892a4"
208
+ desc = ("⚠ Error durante el análisis" if paso == -1
209
+ else PASOS_DETALLE[paso - 1] if 1 <= paso <= 4
210
+ else "Iniciando...")
211
+
212
+ return html.Div([
213
+ html.Div(items, style={
214
+ "display": "flex", "alignItems": "center",
215
+ "padding": "4px 0 10px", "gap": "2px",
216
+ }),
217
+ html.P(desc, style={
218
+ "color": desc_color, "fontSize": "0.75rem",
219
+ "margin": "0", "textAlign": "center",
220
+ }),
221
+ ])
222
+
223
+
224
+ # ── Registro de callbacks ─────────────────────────────────────────────────────
225
+
226
+ def registrar_callbacks(app: dash.Dash) -> None:
227
+
228
+ # 1. Botón Analizar → lanza threads, muestra panel
229
+ @app.callback(
230
+ Output("interval_progress", "disabled"),
231
+ Output("panel-progreso", "style"),
232
+ Output("error-msg", "children"),
233
+ Input({"type": "btn", "index": ALL}, "n_clicks"),
234
+ State("store_name", "data"),
235
+ State("store_link", "data"),
236
+ prevent_initial_call=True,
237
+ )
238
+ def iniciar_analisis(n_clicks, nombre, link):
239
+ global _estado
240
+ if not ctx.triggered_id or not ctx.triggered[0]["value"]:
241
+ return dash.no_update, dash.no_update, ""
242
+ tid = ctx.triggered_id
243
+ if not (isinstance(tid, dict) and tid.get("type") == "btn"):
244
+ return dash.no_update, dash.no_update, ""
245
+ if tid.get("index") != 0:
246
+ return True, _PANEL_OCULTO, ""
247
+ if not link:
248
+ return True, _PANEL_VISIBLE, html.Div(
249
+ "⚠ Por favor ingresa el link del sitio web.",
250
+ style={"color": "#f87171", "fontSize": "0.82rem", "marginTop": "8px"},
251
+ )
252
+ with _lock:
253
+ _estado = {"paso": 0, "pct": 0, "perfil": None, "error": None}
254
+
255
+ threading.Thread(target=_correr_analisis,
256
+ args=(link, nombre or "Organización"), daemon=True).start()
257
+ threading.Thread(target=_animar_porcentaje, daemon=True).start()
258
+
259
+ return False, _PANEL_VISIBLE, ""
260
+
261
+ # 2. Botones volver al inicio
262
+ @app.callback(
263
+ Output("url", "pathname"),
264
+ Input({"type": "btn", "index": ALL}, "n_clicks"),
265
+ prevent_initial_call=True,
266
+ )
267
+ def volver_inicio(n_clicks):
268
+ if not ctx.triggered_id or not ctx.triggered[0]["value"]:
269
+ return dash.no_update
270
+ tid = ctx.triggered_id
271
+ if isinstance(tid, dict) and tid.get("type") == "btn" and tid.get("index") != 0:
272
+ return "/"
273
+ return dash.no_update
274
+
275
+ # 3. Polling → actualiza barra %, pasos, y redirige al terminar
276
+ @app.callback(
277
+ Output("progreso-container", "children"),
278
+ Output("progreso-bar", "style"),
279
+ Output("progreso-pct", "children"),
280
+ Output("url", "pathname", allow_duplicate=True),
281
+ Output("interval_progress", "disabled", allow_duplicate=True),
282
+ Output("panel-progreso", "style", allow_duplicate=True),
283
+ Input("interval_progress", "n_intervals"),
284
+ prevent_initial_call=True,
285
+ )
286
+ def actualizar_progreso(n):
287
+ with _lock:
288
+ e = dict(_estado)
289
+
290
+ paso = e["paso"]
291
+ pct = e.get("pct", 0)
292
+
293
+ bar_style = {
294
+ "height": "100%", "borderRadius": "999px",
295
+ "background": "linear-gradient(90deg, #b87a2a, #d4a843)",
296
+ "width": f"{pct}%",
297
+ "transition": "width 0.8s ease",
298
+ }
299
+ pct_txt = f"{pct}%"
300
+
301
+ # Error — mostrar panel descriptivo
302
+ if paso == -1:
303
+ err = e.get("error", {})
304
+ if isinstance(err, dict):
305
+ titulo = err.get("titulo", "Error durante el análisis")
306
+ detalle = err.get("detalle", "Ocurrió un problema inesperado.")
307
+ sugiere = err.get("sugerencia", "")
308
+ tipo = err.get("tipo", "desconocido")
309
+ else:
310
+ titulo, detalle, sugiere, tipo = str(err), "", "", "desconocido"
311
+
312
+ iconos = {
313
+ "red": "🌐", "ssl": "🔒", "acceso": "🚫",
314
+ "url": "🔗", "modelo": "🤖", "extraccion": "📄",
315
+ "desconocido": "⚠",
316
+ }
317
+ icono = iconos.get(tipo, "⚠")
318
+
319
+ panel_error = html.Div(
320
+ style={
321
+ "marginTop": "12px",
322
+ "background": "#fdeaea",
323
+ "border": "1.5px solid #d48585",
324
+ "borderRadius": "8px",
325
+ "padding": "14px 16px",
326
+ },
327
+ children=[
328
+ html.Div(
329
+ style={"display": "flex", "alignItems": "center",
330
+ "gap": "8px", "marginBottom": "6px"},
331
+ children=[
332
+ html.Span(icono, style={"fontSize": "1.1rem"}),
333
+ html.Span(titulo, style={
334
+ "fontWeight": "700", "color": "#8b1c1c",
335
+ "fontSize": "0.95rem",
336
+ }),
337
+ ],
338
+ ),
339
+ html.P(detalle, style={
340
+ "color": "#6b2020", "fontSize": "0.9rem",
341
+ "margin": "0 0 6px", "lineHeight": "1.5",
342
+ }),
343
+ html.P(f"💡 {sugiere}", style={
344
+ "color": "#3d4a60", "fontSize": "0.88rem",
345
+ "margin": "0", "fontStyle": "italic",
346
+ }) if sugiere else None,
347
+ html.Button(
348
+ "Intentar de nuevo",
349
+ style={
350
+ "marginTop": "12px", "padding": "10px 20px",
351
+ "background": "#fff", "border": "2px solid #8b1c1c",
352
+ "borderRadius": "8px", "color": "#8b1c1c",
353
+ "cursor": "pointer", "fontSize": "0.95rem",
354
+ "fontWeight": "700", "fontFamily": "inherit",
355
+ },
356
+ id={"type": "btn", "index": 0},
357
+ n_clicks=0,
358
+ ),
359
+ ],
360
+ )
361
+ return (
362
+ html.Div([_pasos_html(-1), panel_error]),
363
+ {**bar_style, "background": "#8b1c1c", "width": "100%", "boxShadow": "0 1px 4px rgba(139,28,28,0.4)"},
364
+ "Error",
365
+ dash.no_update, True, _PANEL_VISIBLE,
366
+ )
367
+
368
+ # Completado
369
+ if paso == 4 and e["perfil"]:
370
+ return (
371
+ _pasos_html(4),
372
+ {**bar_style, "width": "100%"},
373
+ "100%",
374
+ f"/profile_{e['perfil']}", True, _PANEL_OCULTO,
375
+ )
376
+
377
+ # En progreso
378
+ return _pasos_html(paso), bar_style, pct_txt, dash.no_update, dash.no_update, dash.no_update
379
+
380
+ # 4. Guardar datos en sesión
381
+ @app.callback(
382
+ Output("store_name", "data"),
383
+ Output("store_link", "data"),
384
+ Input("Nombre_org", "value"),
385
+ Input("Link_org", "value"),
386
+ prevent_initial_call=True,
387
+ )
388
+ def guardar_datos(nombre, link):
389
+ return nombre, link
390
+
391
+ # 5. Abrir modal Venn al hacer clic en la imagen
392
+ @app.callback(
393
+ Output({"type": "venn-modal", "index": ALL}, "style"),
394
+ Input({"type": "venn-thumb", "index": ALL}, "n_clicks"),
395
+ Input({"type": "venn-close", "index": ALL}, "n_clicks"),
396
+ prevent_initial_call=True,
397
+ )
398
+ def toggle_venn_modal(thumb_clicks, close_clicks):
399
+ _ABIERTO = {"display": "flex"}
400
+ _CERRADO = {"display": "none"}
401
+
402
+ if not ctx.triggered_id:
403
+ return [_CERRADO] * len(thumb_clicks)
404
+
405
+ tid = ctx.triggered_id
406
+ tipo = tid.get("type")
407
+ idx = tid.get("index")
408
+
409
+ # Obtener cuántos perfiles hay
410
+ n = len(thumb_clicks)
411
+ result = [_CERRADO] * n
412
+
413
+ if tipo == "venn-thumb":
414
+ # Abrir el modal del perfil clicado (índice = número de perfil 1-5)
415
+ for j in range(n):
416
+ # Los índices son 1..5, la lista es 0..4
417
+ if j + 1 == idx:
418
+ result[j] = _ABIERTO
419
+ # Si es venn-close, todos quedan cerrados (result ya es todo _CERRADO)
420
+
421
+ return result
data/__init__.py ADDED
File without changes
data/definitions.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Definiciones, constantes y datos estáticos de la aplicación AIM Dashboard.
3
+ Separado del código lógico para facilitar mantenimiento y localización.
4
+ """
5
+
6
+ DEFINICION_TRIADA_AIM = """
7
+ **La Tríada AIM (Awareness, Infrastructure, Management)** no es un nuevo modelo de madurez
8
+ en ciberseguridad, sino una estrategia de priorización diseñada para guiar a las organizaciones.
9
+
10
+ A través de nuestro servicio obtenemos el perfil correspondiente a su empresa a partir del
11
+ link de su sitio web, para dar indicaciones acerca de qué estrategias de ciberseguridad
12
+ serían adecuadas para su organización.
13
+ """
14
+
15
+ DEFINICIONES_PERFILES = [
16
+ # ── Perfil 1 ── Gestión formalizada, visibilidad técnica limitada
17
+ """
18
+ **Perfil 1 — El Gestor Formalizado**
19
+
20
+ Su organización ha construido una base sólida en la dimensión de **Gestión**: cuenta con
21
+ políticas documentadas, conciencia sobre obligaciones legales y una estructura básica para
22
+ administrar activos y personal de seguridad. Entiende *qué* debe proteger y *quién* es
23
+ responsable de hacerlo.
24
+
25
+ Sin embargo, la brecha crítica está en la **visibilidad técnica**: carece de capacidad para
26
+ detectar amenazas en tiempo real, su arquitectura de red no está diseñada pensando en
27
+ seguridad y los vectores de ataque técnicos permanecen sin monitoreo activo.
28
+
29
+ **¿Qué significa esto en la práctica?**
30
+ Su empresa sabe que existe el riesgo, tiene los papeles en orden, pero no vería un ataque
31
+ en curso hasta que ya causara daño. Es como tener un buen seguro pero ninguna alarma.
32
+
33
+ **Próximos pasos recomendados:**
34
+ Priorizar la implementación de herramientas de monitoreo (SIEM básico o EDR), revisar la
35
+ segmentación de red y realizar un primer ejercicio de análisis de vulnerabilidades.
36
+ """,
37
+ # ── Perfil 2 ── Alta madurez técnica y de conciencia, gestión en desarrollo
38
+ """
39
+ **Perfil 2 — El Técnico Avanzado**
40
+
41
+ Su organización demuestra un nivel de madurez **excepcionalmente alto** en las dimensiones
42
+ técnicas: tiene tecnología de seguridad bien implementada, arquitectura defensiva estructurada
43
+ y una visibilidad del entorno de amenazas muy por encima del promedio para una PyME.
44
+
45
+ Además posee una cultura de seguridad activa y capacidad de detección y respuesta ante
46
+ incidentes. En términos del modelo AIM, cubre prácticamente todo el espectro de
47
+ **Awareness** e **Infrastructure**.
48
+
49
+ La oportunidad de mejora está en la **formalización de la gestión**: los procesos existen
50
+ pero dependen de personas clave, la gestión del talento de seguridad no está sistematizada
51
+ y el cumplimiento normativo puede estar rezagado frente al nivel técnico alcanzado.
52
+
53
+ **Próximos pasos recomendados:**
54
+ Documentar y transferir el conocimiento tácito a procesos formales, revisar brechas de
55
+ cumplimiento regulatorio y estructurar un plan de sucesión para roles críticos de seguridad.
56
+ """,
57
+ # ── Perfil 3 ── Operaciones robustas, estrategia y cultura por desarrollar
58
+ """
59
+ **Perfil 3 — El Operador Robusto**
60
+
61
+ Su organización ha madurado en la ejecución operativa de la seguridad: administra
62
+ correctamente sus activos, mantiene su fuerza laboral alineada con las responsabilidades
63
+ de seguridad y ha desarrollado capacidades para identificar y mitigar vulnerabilidades técnicas.
64
+
65
+ Existe un programa de seguridad que funciona en el día a día. La gestión del riesgo tiene
66
+ presencia y la gestión del conocimiento es un punto diferenciador positivo.
67
+
68
+ Las brechas se concentran en la **dirección estratégica y la cultura**: no existe una
69
+ política de seguridad que unifique los esfuerzos, la seguridad no está integrada como
70
+ valor organizacional y el cumplimiento normativo no ha sido abordado formalmente.
71
+
72
+ **Próximos pasos recomendados:**
73
+ Desarrollar una política de seguridad corporativa aprobada por la dirección, iniciar un
74
+ programa de cultura de seguridad para el personal no técnico y mapear los requisitos
75
+ regulatorios aplicables al sector.
76
+ """,
77
+ # ── Perfil 4 ── Cultura y conciencia desarrolladas, infraestructura rezagada
78
+ """
79
+ **Perfil 4 — El Consciente Estratégico**
80
+
81
+ Su organización tiene algo valioso y difícil de construir: una **cultura de seguridad genuina**
82
+ y capacidad para detectar y responder a incidentes. El personal entiende los riesgos,
83
+ existe conciencia situacional y la gestión del conocimiento en seguridad es un activo real.
84
+
85
+ Esto la ubica por delante de la mayoría de las PyMEs, donde la mayor vulnerabilidad
86
+ es precisamente el factor humano.
87
+
88
+ La brecha está en la **infraestructura técnica**: la arquitectura no refleja el nivel de
89
+ madurez cultural alcanzado, los vectores de ataque técnico no están completamente
90
+ mitigados y el marco normativo-legal no ha sido formalizado.
91
+
92
+ **Próximos pasos recomendados:**
93
+ Traducir la cultura de seguridad existente en controles técnicos concretos: segmentación
94
+ de red, gestión de vulnerabilidades y hardening de sistemas. Aprovechar la conciencia
95
+ del equipo para acelerar la adopción de nuevas herramientas.
96
+ """,
97
+ # ── Perfil 5 ── Infraestructura y gestión sólidas, detección y cultura incipientes
98
+ """
99
+ **Perfil 5 — El Arquitecto Estructurado**
100
+
101
+ Su organización ha invertido en construir una **base técnica y de gestión coherente**:
102
+ la arquitectura de seguridad está diseñada defensivamente, los activos están bajo control,
103
+ existe una estrategia de seguridad formal y se gestiona el riesgo con criterios definidos.
104
+
105
+ Es una organización que "construyó bien": su infraestructura digital refleja decisiones
106
+ de diseño seguro y los procesos de gestión respaldan esas decisiones.
107
+
108
+ Las áreas de mejora están en la **capacidad de detección activa y el factor humano**:
109
+ aún no se ha desarrollado plenamente la capacidad para detectar y responder a incidentes
110
+ en tiempo real, y la seguridad como valor cultural todavía no está arraigada en el
111
+ comportamiento cotidiano del personal.
112
+
113
+ **Próximos pasos recomendados:**
114
+ Implementar capacidades de detección y respuesta (SOC básico o servicio MDR), desarrollar
115
+ un programa de concientización continua para el personal y establecer ejercicios de
116
+ simulación de incidentes (tabletop exercises).
117
+ """]
118
+
119
+ DOMINIOS_DEFINICIONES = {
120
+ "Cultura y Sociedad": (
121
+ "Refleja los valores, creencias y comportamientos del equipo frente a la seguridad. "
122
+ "Una cultura madura convierte a cada persona en un sensor activo de riesgos, donde "
123
+ "reportar incidentes se ve como una responsabilidad compartida y no como una amenaza."
124
+ ),
125
+ "Conciencia Situacional": (
126
+ "Capacidad de detectar, correlacionar y comprender eventos de seguridad en tiempo real. "
127
+ "Incluye monitoreo centralizado (SIEM/XDR), inteligencia de amenazas y dashboards que "
128
+ "traducen datos técnicos en información útil para la toma de decisiones."
129
+ ),
130
+ "Estándares y Tecnología": (
131
+ "Adopción y operación disciplinada de estándares (NIST, ISO 27001, CIS) y herramientas "
132
+ "de seguridad. No basta con comprar tecnología: este dominio mide si está correctamente "
133
+ "configurada, integrada y mantenida."
134
+ ),
135
+ "Arquitectura": (
136
+ "Diseño estructural de la infraestructura digital pensado para minimizar el impacto de "
137
+ "una brecha. Incluye segmentación de red, modelos Zero Trust, zonas desmilitarizadas (DMZ) "
138
+ "y principios de mínimo privilegio aplicados desde el diseño."
139
+ ),
140
+ "Amenazas y Vulnerabilidades": (
141
+ "Ciclo de vida completo de identificación y mitigación de vulnerabilidades: desde "
142
+ "escaneos automáticos y pruebas de penetración hasta la gestión priorizada de parches "
143
+ "según el riesgo real para el negocio."
144
+ ),
145
+ "Programa": (
146
+ "Existencia de un programa formal de ciberseguridad con objetivos, presupuesto, "
147
+ "métricas y hoja de ruta. Asegura que las iniciativas de seguridad sean planificadas "
148
+ "y ejecutadas como un esfuerzo coherente y sostenido."
149
+ ),
150
+ "Capital Humano": (
151
+ "Gestión del talento de seguridad: roles definidos, responsabilidades claras, "
152
+ "capacitación continua y planificación de sucesión. Cubre tanto al equipo técnico "
153
+ "especializado como al personal general con obligaciones de seguridad."
154
+ ),
155
+ "Activos y Configuración": (
156
+ "Inventario actualizado de todos los activos digitales y físicos, con control de "
157
+ "cambios que previene modificaciones no autorizadas o inseguras. Sin saber qué "
158
+ "activos existen, es imposible protegerlos."
159
+ ),
160
+ "Marco Legal y Regulatorio": (
161
+ "Cumplimiento de leyes, regulaciones sectoriales y contratos que imponen obligaciones "
162
+ "de seguridad (ej. Ley 21.663 en Chile, GDPR si hay datos europeos). Implica mapear "
163
+ "los requisitos aplicables y traducirlos en controles operacionales."
164
+ ),
165
+ "Marco Legal y Regulatorio": (
166
+ "Cumplimiento de leyes, regulaciones sectoriales y contratos que imponen obligaciones "
167
+ "de seguridad. Implica mapear los requisitos aplicables y traducirlos en controles "
168
+ "operacionales concretos y auditables."
169
+ ),
170
+ "Detección y Respuesta": (
171
+ "Capacidad de detectar, contener, erradicar y recuperarse de incidentes de seguridad "
172
+ "de forma estructurada. Incluye playbooks de respuesta, equipos con roles definidos "
173
+ "y ejercicios de simulación que validan la preparación real."
174
+ ),
175
+ "Política y Estrategia": (
176
+ "Marco de políticas formales aprobadas por la dirección que definen el comportamiento "
177
+ "esperado, los controles requeridos y la estrategia de seguridad a mediano y largo "
178
+ "plazo. Proporciona el 'norte' que orienta todas las demás decisiones."
179
+ ),
180
+ "Conocimiento y Capacidades": (
181
+ "Base de conocimiento institucional en ciberseguridad: inteligencia de amenazas, "
182
+ "lecciones aprendidas, competencias técnicas especializadas y capacidad de investigación. "
183
+ "Permite anticipar tendencias y no solo reaccionar a ellas."
184
+ ),
185
+ "Riesgo": (
186
+ "Proceso sistemático para identificar, evaluar y priorizar riesgos según su probabilidad "
187
+ "e impacto en el negocio. Un registro de riesgos activo permite tomar decisiones de "
188
+ "inversión en seguridad basadas en evidencia, no en intuición."
189
+ ),
190
+ }
191
+
192
+ # Dominios débiles (áreas a mejorar) por perfil (índice 0 = Perfil 1)
193
+ CONSEJOS_PERFILES_NEGATIVOS = [
194
+ ["Conciencia Situacional", "Arquitectura", "Amenazas y Vulnerabilidades"],
195
+ ["Marco Legal y Regulatorio", "Capital Humano", "Activos y Configuración"],
196
+ ["Política y Estrategia", "Cultura y Sociedad", "Marco Legal y Regulatorio"],
197
+ ["Arquitectura", "Amenazas y Vulnerabilidades", "Marco Legal y Regulatorio"],
198
+ ["Detección y Respuesta", "Cultura y Sociedad", "Conciencia Situacional"],
199
+ ]
200
+
201
+ # Dominios fuertes (fortalezas) por perfil
202
+ CONSEJOS_PERFILES_POSITIVO = [
203
+ ["Marco Legal y Regulatorio", "Capital Humano", "Activos y Configuración"],
204
+ ["Estándares y Tecnología", "Conciencia Situacional", "Arquitectura"],
205
+ ["Programa", "Amenazas y Vulnerabilidades", "Capital Humano"],
206
+ ["Detección y Respuesta", "Cultura y Sociedad", "Conciencia Situacional"],
207
+ ["Arquitectura", "Amenazas y Vulnerabilidades", "Activos y Configuración"],
208
+ ]
209
+
210
+ # Fortalezas completas por perfil (para el diagrama Venn)
211
+ FORTALEZAS_PERFIL = [
212
+ ["Política y Estrategia", "Riesgo", "Programa", "Detección y Respuesta",
213
+ "Marco Legal y Regulatorio", "Activos y Configuración", "Capital Humano"],
214
+ ["Política y Estrategia", "Conocimiento y Capacidades", "Detección y Respuesta",
215
+ "Estándares y Tecnología", "Arquitectura", "Amenazas y Vulnerabilidades",
216
+ "Cultura y Sociedad", "Conciencia Situacional"],
217
+ ["Conocimiento y Capacidades", "Riesgo", "Programa", "Activos y Configuración",
218
+ "Capital Humano", "Amenazas y Vulnerabilidades"],
219
+ ["Conocimiento y Capacidades", "Riesgo", "Detección y Respuesta",
220
+ "Cultura y Sociedad", "Conciencia Situacional"],
221
+ ["Política y Estrategia", "Riesgo", "Programa", "Marco Legal y Regulatorio",
222
+ "Activos y Configuración", "Arquitectura", "Amenazas y Vulnerabilidades"],
223
+ ]
224
+
225
+ # --- Diagrama Venn ---
226
+ COLORES_BASE = {
227
+ "Concientizacion": "#e06060",
228
+ "Infraestructura": "#4a8c5c",
229
+ "Gestion": "#3a6ab0",
230
+ "Bridge": "#c87e30",
231
+ "Core": "#8a6d3b",
232
+ "Desactivado": "#c8d0df",
233
+ }
234
+
235
+ VENN_SECTIONS = {
236
+ "100": "Concientizacion",
237
+ "010": "Infraestructura",
238
+ "001": "Gestion",
239
+ "110": "Bridge",
240
+ "101": "Bridge",
241
+ "011": "Bridge",
242
+ "111": "Core",
243
+ }
244
+
245
+ SUBCATEGORIAS = {
246
+ "A": ["Cultura y Sociedad", "Conciencia Situacional"],
247
+ "I": ["Arquitectura", "Amenazas y Vulnerabilidades"],
248
+ "M": ["Marco Legal y Regulatorio", "Activos y Configuración", "Capital Humano"],
249
+ "A-I": ["Estándares y Tecnología"],
250
+ "A-M": ["Detección y Respuesta"],
251
+ "I-M": ["Programa"],
252
+ "A-I-M": ["Política y Estrategia", "Conocimiento y Capacidades", "Riesgo"],
253
+ }
254
+
255
+ DOMINIOS_POR_ETAPA = {
256
+ "Core": SUBCATEGORIAS["A-I-M"],
257
+ "Bridge": SUBCATEGORIAS["A-I"] + SUBCATEGORIAS["A-M"] + SUBCATEGORIAS["I-M"],
258
+ "Concientización": SUBCATEGORIAS["A"],
259
+ "Infraestructura": SUBCATEGORIAS["I"],
260
+ "Gestión": SUBCATEGORIAS["M"],
261
+ }
262
+
263
+ # Mapeo de zonas Venn a subcategorías
264
+ VENN_ID_TO_SUBCATEGORIA = {
265
+ "100": SUBCATEGORIAS["A"],
266
+ "010": SUBCATEGORIAS["I"],
267
+ "001": SUBCATEGORIAS["M"],
268
+ "110": SUBCATEGORIAS["A-I"],
269
+ "101": SUBCATEGORIAS["A-M"],
270
+ "011": SUBCATEGORIAS["I-M"],
271
+ "111": SUBCATEGORIAS["A-I-M"],
272
+ }
docker-compose.yml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.9"
2
+
3
+ services:
4
+ aim-dashboard:
5
+ build: .
6
+ container_name: aim_dashboard
7
+ ports:
8
+ - "8050:8050" # Acceso en http://localhost:8050
9
+ volumes:
10
+ # Monta el modelo externo para no tener que reconstruir la imagen
11
+ # si actualizas el modelo K-Means
12
+ - ./Modelo_Pymes.pkl:/app/Modelo_Pymes.pkl:ro
13
+ environment:
14
+ - PYTHONUNBUFFERED=1
15
+ restart: unless-stopped
16
+ # Límite de memoria recomendado (los modelos NLP son pesados)
17
+ mem_limit: 4g
18
+
19
+ # ── Opcional: Nginx como proxy reverso (descomentar para producción) ──────
20
+ # nginx:
21
+ # image: nginx:alpine
22
+ # container_name: aim_nginx
23
+ # ports:
24
+ # - "80:80"
25
+ # - "443:443"
26
+ # volumes:
27
+ # - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
28
+ # depends_on:
29
+ # - aim-dashboard
30
+ # restart: unless-stopped
layouts/__init__.py ADDED
File without changes
layouts/pages.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de layouts del dashboard AIM.
3
+ """
4
+
5
+ from dash import dcc, html
6
+
7
+ from data.definitions import (
8
+ DEFINICION_TRIADA_AIM,
9
+ DEFINICIONES_PERFILES,
10
+ DOMINIOS_DEFINICIONES,
11
+ CONSEJOS_PERFILES_NEGATIVOS,
12
+ CONSEJOS_PERFILES_POSITIVO,
13
+ FORTALEZAS_PERFIL,
14
+ )
15
+ from logic.venn import VENN_IMG_COMPLETO, generar_venn_base
16
+
17
+
18
+
19
+ # ---------- Pantalla de inicio ------------------------------------------------
20
+
21
+ def crear_layout_home() -> html.Div:
22
+ return html.Div(
23
+ className="aim-container",
24
+ children=[
25
+ html.Div(className="aim-header", children=[
26
+ html.H1("Tríada AIM"),
27
+ html.P("Perfil de ciberseguridad para PyMEs", className="subtitle"),
28
+ ]),
29
+
30
+ html.Div(className="aim-two-col", children=[
31
+
32
+ # ---- Columna izquierda ----
33
+ html.Div(className="col-left", children=[
34
+ html.Div(className="aim-card", children=[
35
+ html.Div("Ingrese sus datos", className="aim-card-label"),
36
+ html.Label("Nombre de la organización",
37
+ style={"fontWeight": "600", "marginBottom": "4px"}),
38
+ dcc.Input(
39
+ id="Nombre_org", type="text",
40
+ placeholder="Ej: Mi Empresa S.A.",
41
+ className="aim-input",
42
+ ),
43
+ html.Label("Sitio web de la organización",
44
+ style={"fontWeight": "600", "marginBottom": "4px"}),
45
+ dcc.Input(
46
+ id="Link_org", type="text",
47
+ placeholder="https://www.miempresa.cl",
48
+ className="aim-input",
49
+ ),
50
+ html.Button(
51
+ "Analizar perfil →",
52
+ id={"type": "btn", "index": 0},
53
+ n_clicks=0,
54
+ className="aim-btn-primary",
55
+ ),
56
+ ]),
57
+
58
+ # Descripción tríada
59
+ html.Div(className="aim-card", children=[
60
+ html.Div("¿Qué es la Tríada AIM?", className="aim-card-label"),
61
+ dcc.Markdown(DEFINICION_TRIADA_AIM, className="aim-definition"),
62
+ ]),
63
+ ]),
64
+
65
+ # ---- Columna derecha ----
66
+ html.Div(className="col-right", children=[
67
+ html.Div(className="aim-card", children=[
68
+ html.Div("Modelo de referencia", className="aim-card-label"),
69
+ html.Img(src=VENN_IMG_COMPLETO, className="venn-img"),
70
+ ]),
71
+ ]),
72
+ ]),
73
+ ],
74
+ )
75
+
76
+
77
+ # ---------- Pantalla de perfil ------------------------------------------------
78
+
79
+ def _tarjeta_dominio(nombre: str, tipo: str) -> html.Div:
80
+ """Tarjeta acordeón con descripción completa al expandir."""
81
+ is_strength = tipo == "strength"
82
+ color = "var(--success)" if is_strength else "var(--danger)"
83
+ bg_color = "var(--success-bg)" if is_strength else "var(--danger-bg)"
84
+ border_col = "var(--success-border)" if is_strength else "var(--danger-border)"
85
+ icono = "✦" if is_strength else "◈"
86
+ desc = DOMINIOS_DEFINICIONES.get(nombre, "")
87
+
88
+ return html.Details(
89
+ style={
90
+ "background": bg_color,
91
+ "border": f"1.5px solid {border_col}",
92
+ "borderRadius": "8px",
93
+ "marginBottom": "10px",
94
+ "overflow": "hidden",
95
+ },
96
+ children=[
97
+ html.Summary(
98
+ style={
99
+ "padding": "14px 18px",
100
+ "cursor": "pointer",
101
+ "display": "flex",
102
+ "alignItems": "center",
103
+ "gap": "10px",
104
+ "fontWeight": "700",
105
+ "fontSize": "1rem",
106
+ "color": color,
107
+ "listStyle": "none",
108
+ "userSelect": "none",
109
+ },
110
+ children=[
111
+ html.Span(icono, style={"fontSize": "0.9rem", "flexShrink": "0"}),
112
+ html.Span(nombre, style={"flex": "1"}),
113
+ html.Span("▾ ver más", style={
114
+ "fontSize": "0.78rem",
115
+ "color": "var(--text-muted)",
116
+ "fontWeight": "500",
117
+ "whiteSpace": "nowrap",
118
+ }),
119
+ ],
120
+ ),
121
+ html.Div(
122
+ style={
123
+ "padding": "12px 18px 16px",
124
+ "borderTop": f"1.5px solid {border_col}",
125
+ "background": "#ffffff",
126
+ },
127
+ children=[
128
+ html.P(
129
+ desc,
130
+ style={
131
+ "margin": "0",
132
+ "fontSize": "0.98rem",
133
+ "color": "var(--text-secondary)",
134
+ "lineHeight": "1.75",
135
+ },
136
+ )
137
+ ],
138
+ ),
139
+ ],
140
+ )
141
+
142
+
143
+ def _columna_dominios(titulo: str, icono: str, color: str,
144
+ dominios: list, tipo: str) -> html.Div:
145
+ return html.Div([
146
+ html.Div(
147
+ className="aim-domain-col-header",
148
+ style={"color": color, "borderBottomColor": color, "fontSize": "0.82rem"},
149
+ children=[
150
+ html.Span(icono, style={"fontSize": "1rem"}),
151
+ html.Span(titulo),
152
+ ],
153
+ ),
154
+ *[_tarjeta_dominio(d, tipo) for d in dominios],
155
+ ])
156
+
157
+
158
+ def crear_layout_perfil(i: int) -> html.Div:
159
+ idx = i - 1
160
+ venn_img = f"data:image/png;base64,{generar_venn_base('Reporte', FORTALEZAS_PERFIL[idx])}"
161
+ fortalezas = CONSEJOS_PERFILES_POSITIVO[idx]
162
+ debilidades = CONSEJOS_PERFILES_NEGATIVOS[idx]
163
+
164
+ return html.Div(
165
+ className="aim-container",
166
+ children=[
167
+ # Botón volver
168
+ html.Div(className="aim-nav-home", children=[
169
+ html.Button(
170
+ "← Inicio",
171
+ id={"type": "btn", "index": i},
172
+ n_clicks=0,
173
+ className="aim-btn-secondary",
174
+ style={"width": "auto", "padding": "8px 18px"},
175
+ )
176
+ ]),
177
+
178
+ # Header
179
+ html.Div(className="aim-header", children=[
180
+ html.H1(f"Perfil {i}"),
181
+ html.P("Resultado del análisis de ciberseguridad AIM", className="subtitle"),
182
+ ]),
183
+
184
+ # Descripción del perfil (ancho completo)
185
+ html.Div(className="aim-card", style={"marginBottom": "20px"}, children=[
186
+ html.Div(f"Perfil {i} — Descripción", className="aim-card-label"),
187
+ dcc.Markdown(DEFINICIONES_PERFILES[idx], className="aim-definition"),
188
+ ]),
189
+
190
+ # Grid: fortalezas | debilidades | Venn (Venn más ancho)
191
+ html.Div(
192
+ style={
193
+ "display": "grid",
194
+ "gridTemplateColumns": "1fr 1fr 1.4fr",
195
+ "gap": "20px",
196
+ "alignItems": "start",
197
+ },
198
+ children=[
199
+ # Fortalezas
200
+ html.Div(className="aim-card", children=[
201
+ _columna_dominios(
202
+ "Fortalezas identificadas", "✦", "#34d399",
203
+ fortalezas, "strength",
204
+ ),
205
+ ]),
206
+ # Áreas de mejora
207
+ html.Div(className="aim-card", children=[
208
+ _columna_dominios(
209
+ "Áreas de mejora", "◈", "#f87171",
210
+ debilidades, "weakness",
211
+ ),
212
+ ]),
213
+ # Diagrama Venn con zoom modal
214
+ html.Div(className="aim-card", style={"textAlign": "center"}, children=[
215
+ html.Div("Cobertura AIM", className="aim-card-label"),
216
+ # Imagen clickeable
217
+ html.Div(
218
+ style={"position": "relative", "cursor": "zoom-in"},
219
+ children=[
220
+ html.Img(
221
+ src=venn_img,
222
+ className="venn-img venn-clickable",
223
+ id={"type": "venn-thumb", "index": i},
224
+ n_clicks=0,
225
+ ),
226
+ html.Div(
227
+ "🔍 Clic para ampliar",
228
+ style={
229
+ "position": "absolute", "bottom": "10px",
230
+ "right": "10px", "background": "rgba(28,49,96,0.75)",
231
+ "color": "#fff", "fontSize": "0.75rem",
232
+ "padding": "4px 10px", "borderRadius": "999px",
233
+ "fontWeight": "600", "pointerEvents": "none",
234
+ }
235
+ ),
236
+ ],
237
+ ),
238
+ html.P(
239
+ "✔ Dominios cubiertos ✗ Por desarrollar",
240
+ style={"color": "var(--text-muted)", "fontSize": "0.92rem",
241
+ "marginTop": "12px", "fontWeight": "500"},
242
+ ),
243
+ # Modal overlay
244
+ html.Div(
245
+ id={"type": "venn-modal", "index": i},
246
+ style={"display": "none"},
247
+ className="venn-modal-overlay",
248
+ children=[
249
+ html.Div(className="venn-modal-box", children=[
250
+ html.Button(
251
+ "✕ Cerrar",
252
+ id={"type": "venn-close", "index": i},
253
+ className="venn-modal-close",
254
+ n_clicks=0,
255
+ ),
256
+ html.Img(src=venn_img, className="venn-modal-img"),
257
+ ]),
258
+ ],
259
+ ),
260
+ ]),
261
+ ],
262
+ ),
263
+ ],
264
+ )
265
+
266
+
267
+ layout_home = crear_layout_home()
268
+ all_layouts = [crear_layout_perfil(i) for i in range(1, 6)]
logic/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # logic package
logic/extractor.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de extracción y clasificación de texto desde sitios web.
3
+ Clase ExtractorMVD: navega el sitio, extrae contenido relevante y
4
+ clasifica oraciones en categorías MISIÓN, VISIÓN y DESCRIPCIÓN.
5
+ """
6
+
7
+ import re
8
+ import logging
9
+
10
+ import requests
11
+ from bs4 import BeautifulSoup
12
+ import trafilatura
13
+ import nltk
14
+ from urllib.parse import urljoin
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Descarga de recursos NLTK solo si no están disponibles
19
+ def _asegurar_nltk():
20
+ for recurso in ("punkt", "punkt_tab"):
21
+ try:
22
+ nltk.data.find(f"tokenizers/{recurso}")
23
+ except LookupError:
24
+ nltk.download(recurso, quiet=True)
25
+
26
+ _asegurar_nltk()
27
+
28
+
29
+ class ExtractorMVD:
30
+ """
31
+ Extrae y clasifica el contenido de texto de un sitio web corporativo.
32
+
33
+ Uso:
34
+ extractor = ExtractorMVD(url="https://empresa.cl", nombre="Empresa S.A.")
35
+ extractor.navegar_y_extraer()
36
+ resultado = extractor.clasificar_inteligente()
37
+ """
38
+
39
+ # Rutas de fallback cuando no se encuentran links de navegación
40
+ RUTAS_FALLBACK = [
41
+ "/nosotros", "/pages/nosotros", "/quienes-somos", "/somos",
42
+ "/about", "/mision", "/conocenos", "/about-us",
43
+ "/empresa.htm", "/nosotros-2", "/nuestra-empresa",
44
+ ]
45
+
46
+ def __init__(self, url: str, nombre: str):
47
+ self.url_base = url
48
+ self.nombre = nombre
49
+ self.paginas_candidatas: list[str] = []
50
+ self.datos_crudos: list[dict] = []
51
+
52
+ # Keywords para detectar páginas "Acerca de" en la navegación
53
+ self._nav_keywords = [
54
+ "desarrollo", "ubicada", "nosotros", "nosotras", "quienes",
55
+ "misión", "vision", "historia", "origen", "equipo", "about",
56
+ "propósito", "manifiesto", "impacto", "sobre",
57
+ "we", "us", "who", "mission", "vision", "history", "origin",
58
+ "team", "purpose", "manifesto", "impact",
59
+ ]
60
+
61
+ # Patrones de clasificación semántica
62
+ self._patrones = {
63
+ "MISION": {
64
+ "fuerte": [
65
+ r"nos enfocamos", r"buscamos hacer", r"nuestra misión",
66
+ r"nuestro propósito", r"nuestro objetivo", r"la misión es",
67
+ r"razón de ser", r"por qué existimos", r"nos dedicamos a",
68
+ r"nuestro compromiso es", r"our mision", r"our purpose",
69
+ r"our objective", r"mision is", r"we exist",
70
+ r"we are dedicated to", r"our commitment is",
71
+ ],
72
+ "debil": [
73
+ r"trabajamos para", r"buscamos", r"ayudar a", r"solucionar",
74
+ r"dar una alternativa", r"reemplazar", r"permitir", r"entregar",
75
+ r"proveer", r"facilitar", r"impulsar", r"fomentar", r"promover",
76
+ r"asegurar", r"garantizar", r"contribuir", r"aportar",
77
+ r"generar valor", r"we work for", r"we search", r"help",
78
+ r"solve", r"give an alternative", r"replace", r"allow",
79
+ r"deliver", r"provide", r"facilitate", r"encourage",
80
+ r"assure", r"guarantee", r"contribute", r"create value",
81
+ ],
82
+ },
83
+ "VISION": {
84
+ "fuerte": [
85
+ r"nuestra visión", r"queremos ser", r"seremos", r"proyectamos",
86
+ r"hacia el futuro", r"nuestro sueño", r"soñamos con",
87
+ r"aspiramos a", r"horizonte", r"queremos llevar",
88
+ r"our vision", r"we want to be", r"we will be", r"we project",
89
+ r"into the future", r"our dream", r"we dream of",
90
+ r"we aspire to", r"horizon", r"we want to take",
91
+ ],
92
+ "debil": [
93
+ r"convertirnos en", r"liderar", r"referente", r"mundo",
94
+ r"global", r"internacional", r"latinoamérica", r"impacto real",
95
+ r"cambio es necesario", r"otra forma de vida", r"revolucionar",
96
+ r"transformar", r"redefinir", r"innovación constante",
97
+ r"vanguardia", r"consolidarnos", r"reconocidos por",
98
+ r"largo plazo", r"transform", r"become", r"lead", r"world",
99
+ r"international", r"latin america", r"real impact",
100
+ r"necessary change", r"another way of life", r"revolutionize",
101
+ r"redefine", r"constant innovation", r"vanguard",
102
+ r"consolidate", r"recognized by", r"long term",
103
+ ],
104
+ },
105
+ "DESCRIPCION": {
106
+ "fuerte": [
107
+ r"somos", r"fundada en", r"experiencia", r"historia comienza",
108
+ r"nació en", r"comenzó como", r"trayectoria", r"nuestros inicios",
109
+ r"quienes somos", r"equipo de",
110
+ r"as in our name", r"we are", r"founded in", r"experience",
111
+ r"history start", r"born in", r"started as", r"trajectory",
112
+ r"our begginings", r"who we are", r"team of",
113
+ ],
114
+ "debil": [
115
+ r"empresa", r"compañía", r"consultora", r"organización",
116
+ r"startup", r"agencia", r"firma", r"ofrecemos", r"servicios",
117
+ r"productos", r"plataforma", r"soluciones", r"herramientas",
118
+ r"ubicados en", r"especialistas en", r"expertos en",
119
+ r"más de \d+ años", r"experiencia en", r"presencia en",
120
+ r"company", r"consultant", r"organization", r"agency",
121
+ r"sign", r"we offer", r"services", r"products", r"platform",
122
+ r"solutions", r"tools", r"located in", r"specialists in",
123
+ r"experts in", r"more than \d+ years", r"experience in",
124
+ r"presence in",
125
+ ],
126
+ },
127
+ }
128
+
129
+ self._blacklisted_words = [
130
+ "leer más", "read more", "ver más", "cookies", "derechos reservados",
131
+ "copyright", "iniciar sesión", "carrito", "despachos", "envíos",
132
+ "vacaciones", "feriado", "horario de atención", "subscribe", "boletín",
133
+ "plastic free july", "sumate", "síguenos", "formulario", "censura",
134
+ "see more", "all rights reserved", "fifa", "concurso", "incumplimiento",
135
+ "teléfono", "link", "horario", "log in", "shopping cart", "shipping",
136
+ "deliveries", "holidays", "opening hours", "newsletter", "join us",
137
+ "follow us", "cookie",
138
+ ]
139
+
140
+ # ------------------------------------------------------------------
141
+ # Navegación y extracción
142
+ # ------------------------------------------------------------------
143
+
144
+ def navegar_y_extraer(self) -> None:
145
+ """Recorre el sitio web y almacena el texto extraído de cada página."""
146
+ try:
147
+ headers = {"User-Agent": "Mozilla/5.0"}
148
+ response = requests.get(self.url_base, headers=headers, timeout=20)
149
+ response.raise_for_status()
150
+ soup = BeautifulSoup(response.content, "html.parser")
151
+ self._encontrar_paginas_candidatas(soup)
152
+ except requests.RequestException as e:
153
+ logger.warning("Error al acceder a %s: %s", self.url_base, e)
154
+
155
+ # Siempre incluir la página raíz
156
+ if self.url_base not in self.paginas_candidatas:
157
+ self.paginas_candidatas.append(self.url_base)
158
+
159
+ self._extraer_textos()
160
+
161
+ def _encontrar_paginas_candidatas(self, soup: BeautifulSoup) -> None:
162
+ found_links: set[str] = set()
163
+ for link in soup.find_all("a", href=True):
164
+ href = link["href"]
165
+ texto_link = link.get_text().lower()
166
+ es_relevante = any(kw in texto_link for kw in self._nav_keywords) or \
167
+ any(kw in href.lower() for kw in self._nav_keywords)
168
+ if es_relevante:
169
+ full_url = urljoin(self.url_base, href)
170
+ if self.url_base in full_url and full_url not in found_links:
171
+ found_links.add(full_url)
172
+ self.paginas_candidatas.append(full_url)
173
+
174
+ if not self.paginas_candidatas:
175
+ for ruta in self.RUTAS_FALLBACK:
176
+ self.paginas_candidatas.append(urljoin(self.url_base, ruta))
177
+
178
+ def _extraer_textos(self) -> None:
179
+ for url in self.paginas_candidatas:
180
+ try:
181
+ downloaded = trafilatura.fetch_url(url)
182
+ if downloaded:
183
+ texto = trafilatura.extract(
184
+ downloaded,
185
+ include_comments=False,
186
+ include_tables=False,
187
+ include_links=False,
188
+ favor_precision=True,
189
+ )
190
+ if texto:
191
+ self.datos_crudos.append({"url": url, "texto": texto})
192
+ except Exception as e:
193
+ logger.debug("No se pudo extraer texto de %s: %s", url, e)
194
+
195
+ # ------------------------------------------------------------------
196
+ # Limpieza y validación
197
+ # ------------------------------------------------------------------
198
+
199
+ def _limpiar_texto(self, oracion: str) -> str:
200
+ oracion = re.sub(r"(?i)(leer\s+m[áa]s|read\s+more|ver\s+m[áa]s|ver\s+detalle)\.*", "", oracion)
201
+ oracion = re.sub(r"\.{2,}", "", oracion)
202
+ return oracion.strip()
203
+
204
+ def _validar_integridad(self, oracion: str) -> bool:
205
+ if not re.search(r'[.!?"]$', oracion):
206
+ return False
207
+ letras = [c for c in oracion if c.isalpha()]
208
+ if len(letras) > 10:
209
+ mayusculas = [c for c in letras if c.isupper()]
210
+ if len(mayusculas) / len(letras) > 0.6:
211
+ return False
212
+ return True
213
+
214
+ # ------------------------------------------------------------------
215
+ # Clasificación semántica
216
+ # ------------------------------------------------------------------
217
+
218
+ def clasificar_inteligente(self) -> dict | None:
219
+ """
220
+ Clasifica el texto extraído en categorías MISIÓN, VISIÓN y DESCRIPCIÓN.
221
+
222
+ Returns:
223
+ Dict {nombre_empresa: {"MISION": [...], "VISION": [...], "DESCRIPCION": [...]}}
224
+ o None si no hay datos.
225
+ """
226
+ if not self.datos_crudos:
227
+ logger.warning("No se encontraron datos para clasificar en %s", self.url_base)
228
+ return None
229
+
230
+ resultados: dict[str, list] = {"MISION": [], "VISION": [], "DESCRIPCION": []}
231
+ oraciones_procesadas: set[str] = set()
232
+
233
+ for fuente in self.datos_crudos:
234
+ texto_raw = fuente["texto"]
235
+ texto_limpio = re.sub(r"\s+", " ", texto_raw)
236
+
237
+ oraciones = nltk.sent_tokenize(texto_limpio)
238
+
239
+ for oracion in oraciones:
240
+ oracion = self._limpiar_texto(oracion)
241
+ if len(oracion) < 25 or oracion in oraciones_procesadas:
242
+ continue
243
+
244
+ oracion_lower = oracion.lower()
245
+ if any(bad in oracion_lower for bad in self._blacklisted_words):
246
+ continue
247
+ if not self._validar_integridad(oracion):
248
+ continue
249
+
250
+ mejor_cat = None
251
+ max_puntaje = 0
252
+
253
+ for categoria, tipos in self._patrones.items():
254
+ puntaje = sum(
255
+ 10 for p in tipos["fuerte"] if re.search(p, oracion_lower)
256
+ ) + sum(
257
+ 3 for p in tipos["debil"] if re.search(p, oracion_lower)
258
+ )
259
+ if puntaje > max_puntaje:
260
+ max_puntaje = puntaje
261
+ mejor_cat = categoria
262
+
263
+ if mejor_cat and max_puntaje >= 3:
264
+ resultados[mejor_cat].append(oracion)
265
+ oraciones_procesadas.add(oracion)
266
+
267
+ return {self.nombre: resultados}
logic/modelo.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de clasificación mediante modelos de NLP y K-Means.
3
+ Contiene la lógica de vectorización semántica y predicción de perfil AIM.
4
+ """
5
+
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import torch
12
+ import joblib
13
+ from sentence_transformers import SentenceTransformer, CrossEncoder, util
14
+ from transformers import pipeline
15
+ from deep_translator import GoogleTranslator
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Ruta del modelo K-Means (relativa al directorio de este archivo)
20
+ MODEL_PATH = Path(__file__).parent.parent / "Modelo_Pymes.pkl"
21
+
22
+ # Pesos del comité de expertos
23
+ W_BART = 0.50
24
+ W_MPNET = 0.30
25
+ W_NLI = 0.20
26
+
27
+ # Umbrales de corte
28
+ SCORE_MINIMO = 0.15
29
+ UMBRAL_CORTE = 0.08
30
+ DOMINIOS_NUCLEO = ["Risk", "Policy and Strategy", "Knowledge and Capabilities"]
31
+
32
+ # Herencia AIM: qué pilares influyen a cada dominio
33
+ HERENCIA_AIM = {
34
+ "Risk": ["AWARENESS", "INFRASTRUCTURE", "MANAGEMENT"],
35
+ "Policy and Strategy": ["AWARENESS", "INFRASTRUCTURE", "MANAGEMENT"],
36
+ "Knowledge and Capabilities": ["AWARENESS", "INFRASTRUCTURE", "MANAGEMENT"],
37
+ "Incident Detection and Response": ["AWARENESS", "MANAGEMENT"],
38
+ "Program": ["MANAGEMENT", "INFRASTRUCTURE"],
39
+ "Standards and Technology": ["AWARENESS", "INFRASTRUCTURE"],
40
+ "Culture and Society": ["AWARENESS"],
41
+ "Situational Awareness": ["AWARENESS"],
42
+ "Architecture": ["INFRASTRUCTURE"],
43
+ "Threat and Vulnerability": ["INFRASTRUCTURE"],
44
+ "Legal and regulatory Framework": ["MANAGEMENT"],
45
+ "Workforce": ["MANAGEMENT"],
46
+ "Asset, Change, and Configuration": ["MANAGEMENT"],
47
+ }
48
+
49
+ # ----------------------------------------------------------------------------------
50
+ # Definiciones de dominios y pilares (textos largos de referencia para embeddings)
51
+ # Extraídos del archivo original sin modificación de contenido.
52
+ # ----------------------------------------------------------------------------------
53
+
54
+ BASE_DOMINIOS_AMPLIADOS = {
55
+ "Culture and Society": """
56
+ ### DOMAIN: CULTURE AND SOCIETY
57
+ This domain encapsulates the collective set of values, beliefs, perceptions, and behavioral norms
58
+ that determine how an institution and its stakeholders approach the protection of information assets.
59
+ It functions as the organization's informal operating system, governing the unwritten rules of conduct
60
+ that dictate whether official security directives are internalized as a shared responsibility or viewed
61
+ as bureaucratic impediments. Unlike technical controls that enforce limitations, this dimension focuses
62
+ on the willingness of human actors to adhere to safe practices even in the absence of direct supervision.
63
+ """,
64
+ "Situational Awareness": """
65
+ ### DOMAIN: SITUATIONAL AWARENESS
66
+ This domain defines the organization's dynamic capacity to perceive, synthesize, and interpret the
67
+ status of its security environment in real-time. It bridges the semantic gap between technical anomalies
68
+ and business context, aggregating fragmented telemetry from disparate sources to construct a unified
69
+ Common Operating Picture. It answers: What is happening now? Who is the adversary? Which critical
70
+ functions are implicated?
71
+ """,
72
+ "Standards and Technology": """
73
+ ### DOMAIN: STANDARDS AND TECHNOLOGY
74
+ This domain constitutes the technical realization of cybersecurity: the rigorous selection, implementation,
75
+ and maintenance of the hardware, software, and configuration frameworks that enforce protection.
76
+ Standards refer to externally validated frameworks (NIST CSF, ISO 27001, CIS Benchmarks).
77
+ Technology refers to the specific operational tools deployed to execute those standards.
78
+ """,
79
+ "Architecture": """
80
+ ### DOMAIN: ARCHITECTURE
81
+ This domain defines the structural design, organization, and interconnection of an institution's digital
82
+ ecosystem. It translates abstract security principles such as defense-in-depth, least privilege, and
83
+ resilience into concrete, enforceable topologies. The fundamental objective is to limit the blast radius
84
+ of a potential compromise through network segmentation, Zero Trust models, and cloud landing zones.
85
+ """,
86
+ "Threat and Vulnerability": """
87
+ ### DOMAIN: THREAT AND VULNERABILITY
88
+ This domain encapsulates the organization's dynamic capability to proactively identify, evaluate, and
89
+ mitigate security weaknesses before they can be exploited. It governs the operational lifecycle of a flaw:
90
+ from detection (scanning/reporting) to assessment (scoring based on exploitability and asset criticality)
91
+ and finally to remediation or compensating controls.
92
+ """,
93
+ "Program": """
94
+ ### DOMAIN: PROGRAM
95
+ This domain refers to the strategic planning and execution of cybersecurity as a formal organizational
96
+ program. It ensures that security initiatives are funded, staffed, sequenced, and tracked as a coherent
97
+ portfolio of work aligned with business objectives and risk tolerance.
98
+ """,
99
+ "Workforce": """
100
+ ### DOMAIN: WORKFORCE
101
+ This domain encompasses the people dimension of cybersecurity: recruiting, retaining, and developing
102
+ security talent; defining roles and responsibilities; and ensuring that all staff have the skills and
103
+ authority required to execute their security functions effectively.
104
+ """,
105
+ "Asset, Change and Configuration": """
106
+ ### DOMAIN: ASSET, CHANGE AND CONFIGURATION
107
+ This domain refers to the governance and control of the organization's digital and physical assets,
108
+ including inventory management, configuration baselines, and change control processes that prevent
109
+ unauthorized or insecure modifications to the technology estate.
110
+ """,
111
+ "Legal and Regulatory Framework": """
112
+ ### DOMAIN: LEGAL AND REGULATORY FRAMEWORK
113
+ This domain refers to the laws, regulations, contractual obligations, and industry standards that govern
114
+ the organization's security posture. It ensures that the organization meets its compliance obligations
115
+ while translating external mandates into internal controls and policies.
116
+ """,
117
+ "Incident Detection and Response": """
118
+ ### DOMAIN: INCIDENT DETECTION AND RESPONSE
119
+ This domain refers to the organization's capability to detect, analyze, contain, eradicate, and recover
120
+ from security incidents in a timely and effective manner. It encompasses the people, processes, and
121
+ technology that form the incident lifecycle, from initial alert triage to post-incident review.
122
+ """,
123
+ "Policy and Strategy": """
124
+ ### DOMAIN: POLICY AND STRATEGY
125
+ This domain refers to the capacity of an organization to establish formal policies, standards, and a
126
+ coherent security strategy that aligns protection investments with business objectives and risk appetite.
127
+ It provides the governing framework within which all other security activities operate.
128
+ """,
129
+ "Knowledge and Capabilities": """
130
+ ### DOMAIN: KNOWLEDGE AND CAPABILITIES
131
+ This domain refers to the organization's institutional knowledge base and the specialized competencies
132
+ required to execute its security strategy. It encompasses threat intelligence, security research, and
133
+ the continuous development of skills that keep the organization ahead of the evolving threat landscape.
134
+ """,
135
+ "Risk": """
136
+ ### DOMAIN: RISK
137
+ This domain refers to the systematic process of identifying, assessing, prioritizing, and managing
138
+ threats to the organization's information assets. It provides the analytical framework for converting
139
+ technical vulnerabilities and threat intelligence into business impact language, enabling
140
+ defensible resource allocation decisions.
141
+ """,
142
+ }
143
+
144
+ BASE_PILARES = {
145
+ "AWARENESS": """
146
+ ### PILLAR: AWARENESS
147
+ Awareness constitutes the cognitive and behavioral layer of the organization's cybersecurity posture.
148
+ It represents the internalization of risk management into the daily heuristics of the workforce,
149
+ transforming the human element from a potential vulnerability into a sophisticated sensor network.
150
+ It includes security champions, phishing simulations, role-based training, and reporting mechanisms.
151
+ """,
152
+ "INFRASTRUCTURE": """
153
+ ### PILLAR: INFRASTRUCTURE
154
+ Infrastructure represents the tangible, operative reality of cybersecurity: the collection of hardware,
155
+ software, networks, and architectural mechanisms that materially enforce protection. It encompasses
156
+ network segmentation, endpoint detection, hardening baselines, encryption, and resilience testing.
157
+ It ensures that Defense in Depth is an operational fact rather than a theoretical concept.
158
+ """,
159
+ "MANAGEMENT": """
160
+ ### PILLAR: MANAGEMENT
161
+ Management constitutes the executive and strategic brain of the cybersecurity ecosystem. It encompasses
162
+ governance structures, risk registers, security budgets, policy frameworks, and executive accountability
163
+ mechanisms that ensure security is managed as a critical business function aligned with fiduciary duties.
164
+ """,
165
+ }
166
+
167
+
168
+ # ----------------------------------------------------------------------------------
169
+ # Funciones de traducción y vectorización
170
+ # ----------------------------------------------------------------------------------
171
+
172
+ def _traducir_texto_largo(texto: dict) -> str:
173
+ """Traduce el diccionario de texto clasificado al inglés, en chunks si es necesario."""
174
+ translator = GoogleTranslator(source="es", target="en")
175
+ limite = 4_000
176
+ partes_traducidas = []
177
+
178
+ for llave, texto_original in texto.items():
179
+ texto_original = str(texto_original)
180
+ if len(texto_original) <= limite:
181
+ try:
182
+ partes_traducidas.append(translator.translate(texto_original))
183
+ except Exception:
184
+ partes_traducidas.append(texto_original)
185
+ else:
186
+ for i in range(0, len(texto_original), limite):
187
+ chunk = texto_original[i : i + limite]
188
+ try:
189
+ partes_traducidas.append(translator.translate(chunk))
190
+ except Exception:
191
+ partes_traducidas.append(chunk)
192
+
193
+ return " ".join(partes_traducidas).strip()
194
+
195
+
196
+ def _calcular_similitud(
197
+ texto: str,
198
+ nombres: list,
199
+ definiciones: list,
200
+ embeddings_ref,
201
+ model_A,
202
+ model_B,
203
+ model_C,
204
+ ) -> dict:
205
+ """Calcula similitud fusionada usando MPNet + DeBERTa + BART."""
206
+
207
+ # MPNet (semántico)
208
+ emb_texto = model_A.encode(texto, convert_to_tensor=True)
209
+ scores_A = util.cos_sim(emb_texto, embeddings_ref)[0].cpu().numpy()
210
+ if scores_A.max() > scores_A.min():
211
+ scores_A = (scores_A - scores_A.min()) / (scores_A.max() - scores_A.min())
212
+
213
+ # DeBERTa (lógico / NLI)
214
+ pares = [[texto, d] for d in definiciones]
215
+ scores_B_logits = model_B.predict(pares)
216
+ scores_B = np.max(scores_B_logits, axis=1) if len(scores_B_logits.shape) > 1 else scores_B_logits
217
+ if scores_B.max() > scores_B.min():
218
+ scores_B = (scores_B - scores_B.min()) / (scores_B.max() - scores_B.min())
219
+
220
+ # BART (zero-shot contextual)
221
+ res_C = model_C(texto, nombres, multi_label=True)
222
+ mapa_C = dict(zip(res_C["labels"], res_C["scores"]))
223
+ scores_C = np.array([mapa_C[n] for n in nombres])
224
+
225
+ finales = (scores_C * W_BART) + (scores_A * W_MPNET) + (scores_B * W_NLI)
226
+
227
+ return {
228
+ nombre: {
229
+ "final": finales[i],
230
+ "bart": scores_C[i],
231
+ "mpnet": scores_A[i],
232
+ "nli": scores_B[i],
233
+ }
234
+ for i, nombre in enumerate(nombres)
235
+ }
236
+
237
+
238
+ def _vectorizar(texto: dict) -> pd.DataFrame:
239
+ """
240
+ Vectoriza el texto clasificado y devuelve un DataFrame con scores por dominio.
241
+ Carga los modelos NLP bajo demanda (solo cuando se llama).
242
+ """
243
+ device = 0 if torch.cuda.is_available() else -1
244
+
245
+ logger.info("Cargando modelos NLP...")
246
+ model_A = SentenceTransformer("all-mpnet-base-v2")
247
+ model_B = CrossEncoder("cross-encoder/nli-deberta-v3-base")
248
+ model_C = pipeline("zero-shot-classification", model="facebook/bart-large-mnli", device=device)
249
+
250
+ nombres_dominios = list(BASE_DOMINIOS_AMPLIADOS.keys())
251
+ defs_dominios = list(BASE_DOMINIOS_AMPLIADOS.values())
252
+ emb_dominios = model_A.encode(defs_dominios, convert_to_tensor=True)
253
+
254
+ nombres_pilares = list(BASE_PILARES.keys())
255
+ defs_pilares = list(BASE_PILARES.values())
256
+ emb_pilares = model_A.encode(defs_pilares, convert_to_tensor=True)
257
+
258
+ texto_clean = _traducir_texto_largo(texto)
259
+
260
+ scores_dominios = _calcular_similitud(
261
+ texto_clean, nombres_dominios, defs_dominios, emb_dominios,
262
+ model_A, model_B, model_C,
263
+ )
264
+ scores_pilares = _calcular_similitud(
265
+ texto_clean, nombres_pilares, defs_pilares, emb_pilares,
266
+ model_A, model_B, model_C,
267
+ )
268
+ scores_pilares_simple = {k: v["final"] for k, v in scores_pilares.items()}
269
+
270
+ P_BASE = 0.60
271
+ P_HERENCIA = 0.40
272
+ datos_tabla = []
273
+
274
+ for dominio, detalle in scores_dominios.items():
275
+ padres = HERENCIA_AIM.get(dominio, [])
276
+ score_herencia = (
277
+ sum(scores_pilares_simple[p] for p in padres) / len(padres)
278
+ if padres else 0.0
279
+ )
280
+ bono = 0.10 if len(padres) == 3 else 0.0
281
+ score_final = (detalle["final"] * P_BASE) + (score_herencia * P_HERENCIA) + bono
282
+
283
+ datos_tabla.append({
284
+ "Categoría": dominio,
285
+ "Final": score_final,
286
+ "BART": detalle["bart"],
287
+ "MPNet": detalle["mpnet"],
288
+ "NLI": detalle["nli"],
289
+ "Base": detalle["final"],
290
+ "Herencia": score_herencia,
291
+ })
292
+
293
+ df = (
294
+ pd.DataFrame(datos_tabla)
295
+ .sort_values(by="Final", ascending=False)
296
+ .reset_index(drop=True)
297
+ )
298
+
299
+ # Calcular saltos para el criterio de corte
300
+ df["Salto"] = df["Final"].diff(periods=-1).fillna(0)
301
+
302
+ indice_corte = len(df)
303
+ for idx, row in df.iterrows():
304
+ siguiente_dominio = df.iloc[idx + 1]["Categoría"] if idx + 1 < len(df) else ""
305
+ score_siguiente = df.iloc[idx + 1]["Final"] if idx + 1 < len(df) else 0
306
+
307
+ if row["Final"] < SCORE_MINIMO:
308
+ indice_corte = idx
309
+ break
310
+
311
+ if row["Salto"] > UMBRAL_CORTE:
312
+ nucleo_rescatable = (
313
+ siguiente_dominio in DOMINIOS_NUCLEO and score_siguiente >= SCORE_MINIMO
314
+ )
315
+ if not nucleo_rescatable:
316
+ indice_corte = idx + 1
317
+ break
318
+
319
+ return df * 100 # Convertir a porcentajes
320
+
321
+
322
+ def obtener_perfil(texto: dict) -> int:
323
+ """
324
+ Clasifica el texto extraído y retorna el índice del perfil (0-4).
325
+
326
+ Args:
327
+ texto: Diccionario {nombre_empresa: {MISION: [...], VISION: [...], DESCRIPCION: [...]}}.
328
+
329
+ Returns:
330
+ Entero entre 0 y 4 (índice de cluster K-Means).
331
+
332
+ Raises:
333
+ FileNotFoundError: Si no se encuentra el archivo del modelo.
334
+ ValueError: Si el texto es None o vacío.
335
+ """
336
+ if not texto:
337
+ raise ValueError("El texto de entrada está vacío o es None.")
338
+
339
+ if not MODEL_PATH.exists():
340
+ raise FileNotFoundError(
341
+ f"No se encontró el modelo en: {MODEL_PATH}\n"
342
+ "Asegúrate de que 'Modelo_Pymes.pkl' esté en la carpeta raíz del proyecto."
343
+ )
344
+
345
+ vector = _vectorizar(texto)
346
+ kmeans = joblib.load(MODEL_PATH)
347
+ perfil = kmeans.predict(vector.iloc[:, 1].values.reshape(1, -1))
348
+ return int(perfil[0])
logic/venn.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de generación del diagrama de Venn para la Tríada AIM.
3
+ Genera imágenes base64 a partir de matplotlib + matplotlib_venn.
4
+ """
5
+
6
+ import io
7
+ import base64
8
+ import matplotlib
9
+ matplotlib.use("Agg")
10
+ import matplotlib.pyplot as plt
11
+ from matplotlib_venn import venn3
12
+
13
+ from data.definitions import (
14
+ COLORES_BASE,
15
+ VENN_SECTIONS,
16
+ VENN_ID_TO_SUBCATEGORIA,
17
+ )
18
+
19
+ VENN_IDS = ["100", "010", "001", "110", "101", "011", "111"]
20
+
21
+
22
+ def generar_venn_base(foco=None, seleccionados_usuario=None) -> str:
23
+ """
24
+ Genera el diagrama de Venn y retorna la imagen codificada en base64.
25
+
26
+ Args:
27
+ foco: Modo de visualización. Opciones:
28
+ "Reporte" → muestra fortalezas/debilidades por perfil.
29
+ "Completo" → muestra todos los dominios con color.
30
+ "Simple" → solo color, sin texto.
31
+ str → destaca únicamente la zona con ese nombre (Core, Bridge, ...).
32
+ seleccionados_usuario: Lista de dominios activos (solo relevante en modo "Reporte").
33
+
34
+ Returns:
35
+ String base64 PNG de la imagen generada.
36
+ """
37
+ if seleccionados_usuario is None:
38
+ seleccionados_usuario = []
39
+
40
+ plt.figure(figsize=(11, 11))
41
+ venn = venn3(
42
+ subsets=(3, 2, 3, 1, 1, 1, 3),
43
+ set_labels=("Concientizacion", "Infraestructura", "Gestion"),
44
+ )
45
+
46
+ # Limpiar etiquetas por defecto
47
+ for vid in VENN_IDS:
48
+ label = venn.get_label_by_id(vid)
49
+ if label:
50
+ label.set_text("")
51
+
52
+ for vid in VENN_IDS:
53
+ patch = venn.get_patch_by_id(vid)
54
+ if not patch:
55
+ continue
56
+
57
+ section_name = VENN_SECTIONS[vid]
58
+ dominios_zona = VENN_ID_TO_SUBCATEGORIA[vid]
59
+
60
+ if foco == "Reporte":
61
+ hay_seleccion = any(d in seleccionados_usuario for d in dominios_zona)
62
+ color = COLORES_BASE[section_name] if hay_seleccion else COLORES_BASE["Desactivado"]
63
+ alpha = 0.8 if hay_seleccion else 0.3
64
+
65
+ texto_etiqueta = [
66
+ f"✔ {d}" if d in seleccionados_usuario else f"✗ {d}"
67
+ for d in dominios_zona
68
+ ]
69
+ label = venn.get_label_by_id(vid)
70
+ if label:
71
+ label.set_text("\n".join(texto_etiqueta))
72
+ label.set_fontsize(8)
73
+
74
+ elif foco == "Completo":
75
+ color = COLORES_BASE[section_name]
76
+ alpha = 0.8
77
+ label = venn.get_label_by_id(vid)
78
+ if label:
79
+ label.set_text("\n".join(dominios_zona))
80
+
81
+ elif foco == "Simple":
82
+ color = COLORES_BASE[section_name]
83
+ alpha = 0.8
84
+
85
+ else:
86
+ # Foco específico: destaca solo la zona indicada
87
+ es_foco = section_name == foco
88
+ color = COLORES_BASE[section_name] if es_foco else COLORES_BASE["Desactivado"]
89
+ alpha = 0.9 if es_foco else 0.3
90
+ if es_foco:
91
+ label = venn.get_label_by_id(vid)
92
+ if label:
93
+ label.set_text("\n".join(dominios_zona))
94
+
95
+ patch.set_facecolor(color)
96
+ patch.set_edgecolor("black")
97
+ patch.set_linewidth(1.5)
98
+ patch.set_alpha(alpha)
99
+
100
+ if foco == "Reporte":
101
+ titulo = "Tríada AIM — Perfil de cobertura"
102
+ elif foco in ("Completo", "Simple"):
103
+ titulo = "Tríada AIM"
104
+ else:
105
+ titulo = f"Zona: {foco.upper()}" if foco else "Tríada AIM"
106
+
107
+ plt.title(titulo, fontsize=13, fontweight="bold")
108
+
109
+ buf = io.BytesIO()
110
+ plt.savefig(buf, format="png", bbox_inches="tight", dpi=150)
111
+ plt.close()
112
+ buf.seek(0)
113
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
114
+
115
+
116
+ # Pre-renderizado del Venn completo para la pantalla de inicio (se genera una sola vez)
117
+ VENN_IMG_COMPLETO = f"data:image/png;base64,{generar_venn_base('Completo')}"
render.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: aim-dashboard
4
+ runtime: python
5
+ buildCommand: pip install -r requirements.txt
6
+ startCommand: gunicorn app:server
7
+ envVars:
8
+ - key: PYTHON_VERSION
9
+ value: 3.13.2
requirements.txt ADDED
File without changes