# Auditoria de seguridad interna — SiPi (uso interno, NO publicar)

Fecha: 2026-08-20. Este documento es para Mateo/NovaLab, no para el
repositorio publico. Esta listado en `.gitignore` y `proteger_codigo.py`
no lo copia a `publicacion/`.

## 1. Busqueda de secretos hardcodeados

Se corrio grep contra todo `.py`, `.md` y `.json` del repo buscando:
API keys (`sk-...`, `AKIA...`, `ghp_...`), tokens, passwords, credenciales,
rutas absolutas de tu maquina (`/home/...`, `C:\Users\...`), e IPs/URLs
internas (`192.168.*`, `*.internal`, `*.local`, `*.staging`).

**Resultado: nada encontrado.** Lo unico que matcheaba los patrones era
codigo legitimo:
- `sipi_ia.py`: la API key de Anthropic se lee de `ANTHROPIC_API_KEY`
  (variable de entorno) o de un archivo de configuracion local del
  usuario final — nunca esta en el codigo.
- `sipi_ia_servidor.py`: usa `secrets.token_hex()` para generar un
  token de sesion en runtime, no un token fijo.
- Las menciones a "token"/"secreto" en `docs/CHANGELOG.md` y
  `claude/errores_comunes.md` son sobre tokens de lexer o ejemplos de
  la sintaxis del lenguaje (`hash_texto "clave secreta"`), no
  credenciales reales.

No hay nada que rotar ni que sacar del historial de git por ahora.
Igual, agregue `.env`, `.env.local`, `*.key`, `*.pem` y los config
locales de SiPi IA al `.gitignore` para que si en algun momento
aparecen, no lleguen a un commit.

## 2. `proteger_codigo.py` — que protege realmente y que no

**Antes** de este cambio, el loader generado tenia comentarios que
explicaban paso a paso el mecanismo completo (marshal + base64,
por que `globals()` y no un dict nuevo, etc.) — informacion que no
ayuda al usuario final y si le ahorra trabajo a cualquiera que quiera
revertir la proteccion. Se acorto todo eso: los comentarios que
quedan explican que hace una funcion, no como esta armada la
proteccion por dentro.

**Cambio funcional:** se agrego una capa de XOR + zlib antes del
base64 (antes era `base64(marshal.dumps(codigo))` a secas). Esto
**no es una mejora criptografica real** — la clave se deriva de datos
que estan en el propio archivo (ver `herramientas/romper_proteccion.py`,
que revierte todo el pipeline en ~15 lineas sin ningun secreto externo).
Lo que si logra:
- Rompe el patron trivial de "es un base64, lo pego en un decoder
  online y ya" — ahora hace falta correr codigo Python, no una
  herramienta generica.
- Cada archivo protegido usa una clave derivada distinta (no hay una
  unica clave reusada en todos los `_protegido.py`).

**Lo que definitivamente NO protege**, y no hay que asumir lo
contrario: alguien con conocimiento de Python medio, con el propio
loader a la vista (que es publico, viene con la distribucion), puede
reconstruir el pipeline exacto en minutos — el propio archivo dice
literalmente `marshal`, `zlib`, `hashlib`. Con un decompilador de
bytecode (`decompyle3`, `uncompyle6`, `pycdc`) el resultado es codigo
fuente Python aproximado y casi completo, con nombres de funciones,
variables y docstrings intactos (el bytecode de CPython no los
elimina). `herramientas/romper_proteccion.py` demuestra esto
corriendolo contra cualquier archivo protegido.

**Conclusion honesta:** esto sirve para lo que dice el propio
docstring del script — evitar la copia/edicion casual del motor por
un usuario final que abre el .py en un editor de texto. No es
proteccion de propiedad intelectual real contra alguien decidido. Si
en algun momento eso importa (por ejemplo, si SiPi empieza a incluir
algo genuinamente sensible), la opcion real es compilar con Cython/
Nuitka a codigo nativo, no bytecode de Python interpretado.

## 3. Que archivos deberian ser publicos vs internos

Separacion aplicada:

**Publico** (va a `publicacion/` via `preparar_publicacion()`):
`docs/`, `examples/`, `pkg-assets/`, `claude/` (patrones y guia para
IA, es documentacion util para quien use SiPi con un asistente),
`LICENSE`, `README.md`, `requirements.txt`, y el codigo protegido de
`src/` (bytecode, no texto plano) + `sipi_celular.html`.

**Interno, NUNCA se publica** (ahora en `.gitignore` y fuera de la
lista de `recursos` en `preparar_publicacion()`):
- Este mismo archivo (`AUDITORIA_SEGURIDAD_INTERNA.md`).
- `herramientas/romper_proteccion.py` — es una herramienta de
  auditoria que documenta como romper la proteccion; tiene sentido
  para vos, no para distribuir junto al producto.
- `publicacion/` en si (es un artefacto generado, no fuente).
- Caches (`__pycache__`, `.sipic`), builds (`build/`, `dist/`, `*.spec`,
  `*.exe`), entornos virtuales, config de IDE.

`herramientas/proteger_codigo.py` y `herramientas/proteger_codigo.bat`
si viajan en el repo fuente (no en `publicacion/`, que no incluye
`herramientas/`), lo cual ya es correcto: son parte del proceso de
build, no del producto final.

## 5. Mejoras aplicadas en esta segunda pasada (endurecer contra ingenieria inversa)

Sobre la base del punto 2:
- **Nombres de variable aleatorios por build.** Antes el loader siempre
  usaba `_r, _s, _n, _k, _p, _c` — una firma fija que cualquier script
  automatizado podia buscar con un solo patron para detectar todos los
  `_protegido.py` de golpe. Ahora cada build genera nombres al azar
  (`_uam`, `_gtr`, etc.), distintos en cada corrida.
- **Rotacion de bits (ROL) encadenada con el XOR.** Antes del XOR+zlib
  ya existente, se agrega una rotacion de bits por byte con una
  cantidad tambien aleatoria por build. Sigue sin ser criptografia real
  (la cantidad de rotacion queda en el propio archivo, igual que la
  clave), pero rompe la firma de bytes repetitiva que un volcado hex
  simple mostraria con un XOR de clave corta sin rotacion.
- **Payload partido en varios trozos** en vez de un unico string base64
  larguisimo — para que no sea reconocible ni greppeable de un vistazo
  como "un blob base64 gigante" que delata el patron a simple vista.
- **Chequeo automatico de secretos antes de publicar.** `preparar_publicacion()`
  ahora escanea toda la carpeta generada buscando patrones de API keys,
  tokens de AWS/GitHub y rutas locales absolutas, y avisa si encuentra
  algo antes de que llegues a subirlo.
- **Limpieza de `__pycache__`** que quedaba en la carpeta de publicacion
  como residuo del paso de verificacion (no exponia nada sensible —
  era el `.pyc` del wrapper/loader, no del codigo real descifrado en
  runtime — pero no tenia sentido distribuirlo).
- **Generador de `.exe` de la IDE completa** (`herramientas/generar_exe_ide.py`),
  separado del generador de `.exe` para programas puntuales de usuarios
  finales. Empaqueta el codigo ya protegido con PyInstaller
  (`--onefile --windowed --strip`, UPX si esta disponible), listo para
  subir a un hosting como Mediafire.

**Esto sigue sin ser proteccion real contra un atacante decidido** — la
conclusion del punto 2 no cambia. Lo que si logra: sube el piso de
esfuerzo de "un script generico que busca `_r/_s/_n` y un blob base64"
a "alguien que tiene que leer el loader especifico de ese build para
entender su estructura puntual". `herramientas/romper_proteccion.py`
sigue funcionando igual (extrae los literales del propio archivo por
regex, adaptado a que ahora son varios trozos) — eso es intencional:
la herramienta de auditoria interna tiene que poder romperlo siempre,
para que el analisis siga siendo honesto.

## 6. Tests automaticos agregados (antes no existian)

`sipi_cli.py` ya tenia un comando `sipi test` que buscaba
`src/tests/test_suite.py`, pero ese archivo no existia — el comando
estaba roto. Se creo:

- `src/tests/test_suite.py` — 25 tests que corren contra el interprete
  real (no mocks): lexer/parser basico, variables, condicionales,
  bucles, funciones con recursion real, manejo de errores
  (`intentar`/`capturar`), listas/diccionarios, y el cache de bytecode.
  **Al escribir estos tests aparecieron 2 bugs reales del motor**, ya
  corregidos:
  - `para_cada n en [1, 2, 3]` (con una lista literal, no una variable)
    no ejecutaba nada — el regex de `para_cada` solo reconocia un
    identificador simple despues de `en`, no una expresion.
  - `longitud` sobre una lista devolvia el largo de su representacion
    como texto (`longitud([10, 20, 30])` -> 12, el largo de la string
    `"[10, 20, 30]"`) en vez de la cantidad de elementos (3).
- `src/tests/test_robustez.py` — 8 tests sobre manejo de casos raros
  (bloques sin cerrar, variables inexistentes, recursion sin caso base,
  texto unicode, archivos casi vacios, division por cero) — confirman
  que el interprete falla con `SiPiError` o una excepcion clara, no con
  un traceback crudo de Python ni colgandose.
- `.github/workflows/ci.yml` — corre ambas suites en Ubuntu/Windows/
  macOS con Python 3.10/3.11/3.12 en cada Pull Request.

Nota: estos tests SI viajan en el repositorio fuente normal, pero NO se
copian a la carpeta `publicacion/` (esa carpeta es solo lo necesario
para correr SiPi, no las herramientas de desarrollo).

## 7. Separacion dev/produccion (logging)

Se agrego `src/sipi_debug.py`: interruptor central via la variable de
entorno `SIPI_DEBUG`. Con `SIPI_DEBUG` apagado (default, produccion),
`sipi_ia_servidor.py` ya no imprime la IP ni la ruta de cada pedido, y
los errores no controlados le devuelven al cliente un mensaje limpio
("ocurrio un error inesperado") en vez del tipo de excepcion de Python.
Con `SIPI_DEBUG=1` se ve todo el detalle, para cuando estas
desarrollando. Todavia falta aplicar el mismo criterio de forma
consistente en `editor_sipi.py` y en `sipi.py` — quedo pendiente, no
alcanzo en esta pasada.

## 8. El .exe no andaba — causa real y arreglo (no era cosmetico)

Se probo el generador de punta a punta (compilando con PyInstaller de
verdad y corriendo el binario resultante bajo Xvfb, no solo leyendo el
codigo) y aparecio el motivo real de la falla:

**El problema:** PyInstaller arma la lista de que empaquetar analizando
el *texto* del script de entrada. Nuestro punto de entrada real
(`editor_protegido.py`) no tiene ni un solo `import tkinter` visible —
todo esta comprimido dentro del bytecode que recien se ejecuta con
`exec()` en runtime. Resultado: PyInstaller no detectaba automaticamente
tkinter, sus submodulos (`tkinter.filedialog`, `tkinter.messagebox`,
`tkinter.ttk`, etc.), ni los modulos propios de SiPi (`sipi_ia`,
`sipi_debug`, etc.), y el `.exe` reventaba en cuanto el codigo real
(ya corriendo) intentaba importarlos.

**El arreglo:** `generar_exe_ide.py` ahora escanea con `ast` (no con
regex) todos los `.py` originales de `src/` antes de protegerlos, arma
la lista real de dependencias externas Y de submodulos puntuales
(`tkinter.filedialog` no se detecta solo — hubo que pedirlo aparte), y
se los pasa a PyInstaller de forma explicita via `--hidden-import` +
`--paths`. Las dependencias opcionales que no esten instaladas en la
maquina donde se compila (`pygame`, `pymysql`, `pg8000`,
`customtkinter` si falta) se avisan y se omiten en vez de romper el
build — son features opcionales que el propio codigo ya maneja con
`try/except`.

**Verificado de verdad:** se instalo PyInstaller + tkinter + Xvfb en
este mismo entorno, se compilo el `.exe` (ELF de Linux, pero el mismo
pipeline aplica en Windows) y se corrio el binario resultante: arranca,
llega al `mainloop` de Tkinter, y hasta lanza subprocesos del motor de
SiPi para ejecutar programas de prueba — sin ningun `ModuleNotFoundError`
ni traceback. Antes de este arreglo, el mismo build fallaba con
`ImportError: cannot import name 'filedialog' from 'tkinter'` en
cuanto arrancaba.

## 9. Endurecido adicional del .exe contra ingenieria inversa

- **UPX**, si esta instalado en la maquina que compila, comprime el
  binario final — PyInstaller lo deshabilita solo en Linux por
  incompatibilidades conocidas, pero en Windows (el target real) se
  aplica normalmente.
- **Cifrado AES de PyInstaller** (`--key`, via el paquete `tinyaes`):
  disponible solo en PyInstaller <6.0 — la version 6+ le saco esa
  opcion (la propia PyInstaller la considero seguridad-por-oscuridad
  de bajo valor real). El generador detecta la version instalada y
  avisa con claridad si no aplica, en vez de fallar en silencio o
  mentir sobre que esta activo.
- `--strip` sigue activo (quita simbolos de debug de los binarios
  nativos empaquetados).
- Sigue aplicando todo el endurecimiento de `proteger_codigo.py` de la
  seccion 5 (nombres al azar, rotacion de bits, trozos) sobre el codigo
  que va DENTRO del `.exe` — el `.exe` en si es una capa mas encima de
  eso, no un reemplazo.

**Conclusion honesta, otra vez:** un `.exe` de PyInstaller sigue siendo,
en el fondo, un interprete de Python empaquetado + los mismos `.pyc`
protegidos adentro. Herramientas como `pyinstxtractor` los extraen sin
mayor dificultad — lo que cambia es que ahora hay una capa mas de
compresion/empaquetado que analizar antes de llegar al mismo bytecode
protegido de siempre.

## 10. Recomendaciones que quedan pendientes

- Aplicar `sipi_debug.py` de forma consistente en `editor_sipi.py` y
  `sipi.py` (por ahora solo esta en `sipi_ia_servidor.py`).
- Tests especificos de manipulacion de bytecode a mano (mas alla de
  los de robustez ya agregados en la seccion 6).
- Unificar estilo/nombres del codigo (item 6 del pedido original) —
  no se ataco en esta pasada, el foco fue seguridad + que el .exe
  funcione de verdad.

## 11. Inyeccion SQL en `postgres_*`/`mysql_*` y XSS en `pagina_web` (corregidas)

Dos huecos reales encontrados revisando los puntos donde SiPi mete un
valor de variable directo en un lenguaje ajeno (SQL, HTML) sin pasar
por un mecanismo de escape/parametros:

- **`postgres_ejecutar`/`mysql_ejecutar`/`postgres_consultar`/`mysql_consultar`
  no tenian version parametrizada** (a diferencia de `sqlite_ejecutar_parametrizado`,
  que ya existia desde antes). Cualquier programa que armara el SQL
  pegando una variable con `+` o `{interpolacion}` quedaba expuesto a
  inyeccion SQL real contra Postgres/MySQL. Se agregaron
  `postgres_ejecutar_parametrizado`, `postgres_consultar_parametrizado`,
  `mysql_ejecutar_parametrizado`, `mysql_consultar_parametrizado`, con
  la misma sintaxis `"SQL con ?" con [valores]` que ya se usaba en
  SQLite (traducido por dentro a `:pN` para pg8000 y `%s` para
  PyMySQL). Verificado en vivo contra un PostgreSQL y un MariaDB
  reales instalados en esta sesion, incluyendo un intento de
  inyeccion como valor de parametro (confirmado: 0 resultados, no
  altera la consulta). Ver `docs/CHANGELOG.md` v41.38.

- **`pagina_web` interpolaba `{variable}` directo en el HTML sin
  escapar — XSS real** si ese valor venia de afuera (un formulario
  recibido con `escuchar_ruta`, una fila de base de datos, un
  archivo). Se agrego `interpolar_html` (escapa `&`, `<`, `>`, `"`,
  `'` de cada variable interpolada, sin tocar el texto literal que
  escribe el programador) y se aplico en los ocho puntos de
  `pagina_web` que generan HTML/atributos: `titulo`, `subtitulo`,
  `texto`, `boton`, `imagen`, `enlace`, `lista_web`, `tarjeta` y
  `formulario`/`campo`. Verificado generando una pagina real con
  `<script>`, comillas y un intento de fuga de atributo
  (`" onmouseover="..."`): el HTML resultante los muestra como texto
  plano, nunca se ejecutan. Ver `docs/CHANGELOG.md` v41.39 y
  `docs/KNOWN_ISSUES.md`.

**Alcance de lo que queda pendiente en esta misma familia de
problemas:** el editor visual (`editor_visual`, que sirve HTML en
`127.0.0.1` para editar `ventana`/`pagina_web`/`crear_juego` con el
mouse) no pasa por `interpolar_html` — se decidio no tocarlo en esta
pasada porque refleja el propio codigo fuente del programador de
vuelta a si mismo en una herramienta local de desarrollo, no datos de
terceros; queda anotado por si en algun momento ese editor se expone
mas alla de `127.0.0.1`.
