🛡️ sandbox-labs GitHub ↗

15 · Instalación de cadena de suministro#

En una frase, para cualquiera: instalar una biblioteca descarga y ejecuta código de cientos de personas que nunca verás, antes de que hayas escrito una sola línea. Este caso enseña qué ocurre exactamente en ese momento.

Estado real: 🟡 building — hay código y 8 comprobaciones automáticas, sin levantarse bajo bwrap en CI · Carpeta: cases/15-supply-chain/ · Puerto: 8815


Por qué se realiza este caso#

Instalar una dependencia no es copiar ficheros. En la mayoría de ecosistemas ejecuta scripts: postinstall, preinstall, build.rs. Con tus permisos. Antes de que nadie mire el código.

Y no instalas una biblioteca: instalas su árbol entero. Un paquete con cinco dependencias directas puede arrastrar cuatrocientas transitivas, mantenidas por gente que no se conoce entre sí.

VectorCómo funciona
TyposquattingUn paquete llamado casi igual que el que querías
postinstall maliciosoSe ejecuta al instalar, lee variables de entorno y las envía
Toma de una cuenta de mantenedorEl paquete de siempre, versión nueva, dueño distinto
Dependencia transitivaEl paquete honesto arrastra uno que no lo es
Confusión de dependenciasUn paquete público con el nombre de uno interno
Versión inmutable que cambiaLo que descargas hoy no es lo que descargaste ayer

La idea que enseña, y que ningún otro caso enseña#

El momento de la instalación es una ejecución de código no confiable, y casi nadie lo trata como tal. Este caso lo hace visible: instala en una jaula, con la red registrada, y entrega un informe de qué se ejecutó, qué leyó y a dónde intentó conectarse durante la instalación.

Casos de uso reales#

Cómo funcionará#

flowchart LR
  M["📜 Manifiesto"] --> J
  subgraph J["🔒 Jaula · red registrada · entorno VACÍO"]
    R["📥 Resolver"]
    I["⚙️ Ejecutar scripts de instalación"]
    R --> I
  end
  J --> T["🌳 Árbol de dependencias<br/>con transitivas"]
  J --> S["📋 Qué scripts se ejecutaron"]
  J --> N["🌐 A dónde intentó conectarse"]
  J --> E["🚫 Qué intentó leer del entorno"]
flowchart TB
  A["Paquete a instalar"] --> B{"¿El nombre se parece<br/>a uno popular?"}
  B -- sí --> B1["📣 Posible typosquatting"]
  B -- no --> C{"¿Trae scripts<br/>de instalación?"}
  C -- sí --> C1["📣 Se ejecutan EN JAULA<br/>y se registra todo"]
  C -- no --> D{"¿El checksum coincide<br/>con el fichero de bloqueo?"}
  D -- no --> D1["🚫 No se instala"]
  D -- sí --> E["✅ Instalado y anotado"]

Esquemas#

Salida — el informe de instalación#

{
  "direct": 5,
  "transitive": 412,
  "packagesWithInstallScripts": [
    { "name": "paquete-x", "version": "2.1.0", "script": "postinstall", "command": "node setup.js" }
  ],
  "environmentReads": [
    { "package": "paquete-x", "variable": "NPM_TOKEN", "outcome": "entorno vacío: no había nada que leer" }
  ],
  "networkAttempts": [
    { "package": "paquete-x", "host": "203.0.113.5:443", "outcome": "bloqueado por lista de permitidos" }
  ],
  "typosquattingSuspects": [{ "name": "reqeusts", "similarTo": "requests", "distance": 2 }],
  "checksumMismatches": []
}

caso: el postinstall hizo el intento, y no encontró nada porque el entorno se había vaciado entero.

Software necesario#

ComponentePara qué¿Obligatorio?
Rust 1.75+El supervisor y el informe
bubblewrap 0.6+La jaula de instalación
El proxy de salida con lista de permitidosYa construido: registra cada conexión
pnpm 9+ / cargo / pipEl gestor real que se está observando
Linux o WSL2Namespaces sin privilegios
Los paquetes comprometidos de este caso son simulados, publicados en un registro local del propio repositorio. No se usan paquetes maliciosos reales.

Instalación#

sudo apt install bubblewrap
corepack enable pnpm
cargo build --release

Procesos que se crearán#

sandboxctl supply-chain install <manifiesto>
  │
  ├─ registro local simulado    ← los paquetes de ejemplo viven aquí
  ├─ proxy de salida            ← lista de permitidos + registro de intentos
  │
  └─ systemd --user scope
      └─ bwrap                  ← entorno VACÍO, disco temporal
          └─ el gestor de paquetes
              └─ los scripts postinstall de cada dependencia

El entorno vacío es el control central: es lo que convierte «el postinstall se llevó tu token» en «el postinstall buscó un token y no había ninguno».

Tiempo de carga estimado#

OperaciónCoste esperado
Arranque de la jaula5–15 ms
Instalación de un árbol pequeño desde registro local1–5 s
Instalación de un árbol grandedecenas de segundos
Generación del informe< 1 s

Qué hace falta para construirlo#

  1. Registro local con paquetes de ejemplo: honesto, typosquatting, postinstall

curioso, dependencia transitiva comprometida.

  1. Entorno vaciado y verificado antes de instalar.
  2. Registro de cada intento de red durante la instalación.
  3. Detección de nombres sospechosamente parecidos.
  4. Verificación de checksums contra el fichero de bloqueo.

Si algo falla#

El caso ya tiene código: el núcleo en core.py y el servicio en app.py. Lo que sigue son sus fallos, la causa y la salida:

SituaciónCausaCómo se resuelve
Un postinstall falla porque no encuentra una variableEl entorno está vacíoEse es el hallazgo: aparece en environmentReads con outcome: entorno vacío. Si la variable es legítima, se declara una a una en la política, nunca heredando el entorno entero
La instalación falla por falta de redLa lista de permitidos no incluye el registroAñadir el registro concreto. Todo lo demás queda registrado como intento bloqueado, que es el dato del caso
checksumMismatches no está vacíoLo descargado no coincide con el fichero de bloqueoNo se instala. Puede ser una caché sucia o un paquete alterado bajo la misma versión. Investigar antes de regenerar el bloqueo
Un paquete legítimo sale como typosquattingLa detección por parecido de nombres tiene falsos positivosEs una señal, no un veredicto: se revisa a mano. La lista de sospechosos incluye la distancia y el nombre parecido para poder juzgar
El árbol de dependencias sale más pequeño de lo esperadoLa resolución usó una cachéInstalar en limpio para ver el árbol completo, incluidas las transitivas

Los fallos que afectan a cualquier caso —no se puede crear el sandbox, no hay cgroups, un puerto ocupado, procesos huérfanos, la compilación en Windows— están resueltos uno a uno en Cuando algo falla.

Cómo se comprueba#

node scripts/verify-cases.mjs

Llama al núcleo del caso con situaciones concretas y comprueba qué hizo con ellas, no cómo está escrito. Son 8 comprobaciones, y corren en cada commit.

cargo run -p sandboxctl -- service up supply-chain

Levanta el caso como producto en 127.0.0.1:8815. POST /api/run acepta el cuerpo que describen los esquemas de arriba.

Sigue en building, no en functional. El núcleo se comprueba, pero el servicio no se levanta bajo bwrap dentro de CI y el caso no emite evidencia firmada. La regla completa está en el ROADMAP.

Ver también: Catálogo completo · Caso 10 · construcción de paquetes · Caso 09 · CI