feat(infra): auto-commit con 86 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 19:38:15 +02:00
parent de9bfec498
commit fe65c5e527
85 changed files with 11840 additions and 92 deletions
+95
View File
@@ -0,0 +1,95 @@
---
name: shell_exec_whitelist
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func ShellExecWhitelist(opts ShellExecOpts) (ShellExecResult, error)"
description: "Ejecuta argv shell con whitelist obligatoria de binarios, SIN shell expansion, timeout context-cancellable con SIGTERM+SIGKILL, stdout/stderr separados con truncate opcional. Para device_agent y otros sandboxes que reciben requests externos."
tags: [shell, exec, security, sandbox, device-agent, infra, agents, docker]
uses_functions: []
uses_types: [shell_exec_result_go_infra, error_go_core]
returns: [shell_exec_result_go_infra]
returns_optional: false
error_type: "error_go_core"
imports: [bytes, context, fmt, os, os/exec, os/user, path/filepath, strconv, strings, syscall, time]
tested: true
tests:
- "echo whitelisted returns stdout"
- "binary not in whitelist rejected without spawn"
- "timeout kills process and sets TimedOut"
- "empty whitelist returns error"
- "stdin payload passes to process"
- "output exceeding MaxOutputBytes is truncated"
- "absolute path in whitelist matches resolved binary"
test_file_path: "functions/infra/shell_exec_whitelist_test.go"
file_path: "functions/infra/shell_exec_whitelist.go"
params:
- name: opts.Cmd
desc: "argv completo. Cmd[0] es el binario (path absoluto o nombre en PATH). Obligatorio, no puede estar vacío."
- name: opts.BinariesAllowed
desc: "Whitelist de binarios permitidos. EMPTY = rechaza todo sin spawn (defense in depth). Entry con / se compara con path resolvido; entry bare name se compara con basename del resolvido."
- name: opts.Env
desc: "Variables de entorno KEY=VAL. Si vacío, se aplica entorno mínimo: PATH=/usr/bin:/bin, HOME, USER, LANG."
- name: opts.WorkingDir
desc: "Directorio de trabajo. Si vacío usa HOME del usuario actual."
- name: opts.TimeoutSeconds
desc: "Timeout máximo en segundos. Default 30. Al expirar: SIGTERM → espera 1s → SIGKILL."
- name: opts.StdinPayload
desc: "Bytes a pasar como stdin al proceso. Nil/vacío = sin stdin."
- name: opts.MaxOutputBytes
desc: "Límite de bytes por stream (stdout y stderr por separado). Default 1 MB. Activa Truncated=true si se supera."
- name: opts.User
desc: "Usuario para ejecutar el proceso (nombre o 'uid:gid'). Requiere uid=0. Vacío = usuario actual."
output: "ShellExecResult con ExitCode, Stdout, Stderr, Duration (ms), Truncated y TimedOut."
---
## Ejemplo
```go
result, err := infra.ShellExecWhitelist(infra.ShellExecOpts{
Cmd: []string{"ls", "-la", "/tmp"},
BinariesAllowed: []string{"ls", "cat", "echo", "id"},
TimeoutSeconds: 10,
MaxOutputBytes: 64 * 1024,
})
if err != nil {
log.Fatalf("exec rejected: %v", err)
}
fmt.Printf("exit=%d duration=%dms truncated=%v timedOut=%v\n",
result.ExitCode, result.Duration, result.Truncated, result.TimedOut)
fmt.Println(result.Stdout)
```
Con stdin:
```go
result, err := infra.ShellExecWhitelist(infra.ShellExecOpts{
Cmd: []string{"cat"},
BinariesAllowed: []string{"cat"},
StdinPayload: []byte("payload from device_agent"),
})
```
Con path absoluto en whitelist:
```go
result, err := infra.ShellExecWhitelist(infra.ShellExecOpts{
Cmd: []string{"/usr/bin/id"},
BinariesAllowed: []string{"/usr/bin/id"},
})
```
## Cuando usarla
Cuando recibes requests externos (Element Matrix, webhook, agente) que especifican un comando a ejecutar en el host, y necesitas garantizar que solo binarios pre-aprobados corren, sin posibilidad de shell injection. Reemplaza `exec.Command` directa en device_agent o cualquier sandbox que acepte comandos de fuentes no confiables.
## Gotchas
- **Empty whitelist rechaza por diseño**: `BinariesAllowed: []string{}` devuelve error inmediato. NUNCA construyas la whitelist dinámicamente desde input externo.
- **PATH default mínimo** (`/usr/bin:/bin`): si tu binario está en `/usr/local/bin` u otro directorio, añádelo explícitamente a `Env` o usa el path absoluto en `Cmd[0]` y en `BinariesAllowed`.
- **SIGTERM+1s+SIGKILL**: algunos procesos pueden ignorar SIGTERM. SIGKILL es forzoso pero puede dejar recursos abiertos (ficheros, sockets). Diseña el proceso objetivo para manejar SIGTERM limpiamente.
- **Truncate aplica POST-exec**: no es streaming. Si el proceso produce 10 GB de output, el buffer crece hasta ese tamaño en RAM antes de truncar. Para procesos con output gigante usa pipes propios o un wrapper de streaming.
- **User switch requiere uid=0**: en un entorno sin root (contenedor sin privilegios, proceso normal), pasar `User != ""` siempre devuelve error. Verificar con `os.Getuid() == 0` antes si el campo es opcional.
- **`Cmd[0]` es el nombre del binario en PATH** pero la whitelist puede tener paths absolutos o bare names. Precedencia: entry con `/` compara contra el path resolvido por `LookPath`; entry sin `/` compara contra `filepath.Base` del path resolvido. Ambas formas son válidas y pueden coexistir en la misma whitelist.