post inicial
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAskSecretAndPlaceholders(t *testing.T) {
|
||||
r, env, dir := newEnv(t, "EXECUTION_MODE=privileged")
|
||||
env.Secrets = NewSecretStore()
|
||||
|
||||
// Modo tareas: sin operador no se puede pedir.
|
||||
if res := call(t, r, "ask_secret", map[string]any{"name": "db", "prompt": "pw"}); res.OK || !strings.Contains(res.Output, "no interactive operator") {
|
||||
t.Fatalf("task mode: %+v", res)
|
||||
}
|
||||
|
||||
var asked string
|
||||
env.Prompt = func(_ context.Context, prompt string) (string, error) {
|
||||
asked = prompt
|
||||
return `s3cr3t'; rm -rf / #`, nil
|
||||
}
|
||||
res := call(t, r, "ask_secret", map[string]any{"name": "db", "prompt": "Password of admin on db01"})
|
||||
if !res.OK || !strings.Contains(res.Output, "{{secret:db}}") || strings.Contains(res.Output, "s3cr3t") {
|
||||
t.Fatalf("%+v", res)
|
||||
}
|
||||
if !strings.Contains(asked, "Password of admin on db01") {
|
||||
t.Errorf("prompt = %q", asked)
|
||||
}
|
||||
// Segunda petición: no se vuelve a preguntar.
|
||||
asked = ""
|
||||
call(t, r, "ask_secret", map[string]any{"name": "db", "prompt": "x"})
|
||||
if asked != "" {
|
||||
t.Error("known secret must not be asked again")
|
||||
}
|
||||
|
||||
// exec: el valor llega al proceso, pero la salida muestra el marcador.
|
||||
res = call(t, r, "exec", map[string]any{"command": "printf", "args": []string{"%s", "{{secret:db}}"}})
|
||||
if !res.OK || strings.Contains(res.Output, "s3cr3t") || !strings.Contains(res.Output, "{{secret:db}}") {
|
||||
t.Errorf("exec output must mask the secret: %s", res.Output)
|
||||
}
|
||||
|
||||
// shell: se pasa por variable de entorno; las comillas del valor no rompen
|
||||
// ni inyectan nada en el script.
|
||||
res = call(t, r, "shell", map[string]any{"script": `printf '%s' "{{secret:db}}" > out.txt; echo written`})
|
||||
if !res.OK {
|
||||
t.Fatalf("shell: %+v", res)
|
||||
}
|
||||
if data, _ := os.ReadFile(filepath.Join(dir, "out.txt")); string(data) != `s3cr3t'; rm -rf / #` {
|
||||
t.Errorf("shell received %q", data)
|
||||
}
|
||||
if strings.Contains(res.Output, "s3cr3t") {
|
||||
t.Error("shell output leaked the secret")
|
||||
}
|
||||
|
||||
// Archivos: se escribe el valor real; el diff mostrado lo enmascara.
|
||||
os.WriteFile(filepath.Join(dir, "app.conf"), []byte("password=old\n"), 0o600)
|
||||
res = call(t, r, "edit_file", map[string]any{"path": "app.conf", "old_string": "password=old", "new_string": "password={{secret:db}}"})
|
||||
if !res.OK {
|
||||
t.Fatalf("%+v", res)
|
||||
}
|
||||
if data, _ := os.ReadFile(filepath.Join(dir, "app.conf")); !strings.Contains(string(data), "s3cr3t'") {
|
||||
t.Errorf("file content = %q", data)
|
||||
}
|
||||
if strings.Contains(res.Output, "s3cr3t") || !strings.Contains(res.Output, "+password={{secret:db}}") {
|
||||
t.Errorf("diff must mask the secret: %s", res.Output)
|
||||
}
|
||||
|
||||
// El marcador funciona en cualquier contexto de comillas.
|
||||
for _, script := range []string{
|
||||
`printf %s {{secret:db}} > q.txt`,
|
||||
`printf %s "{{secret:db}}" > q.txt`,
|
||||
`printf %s '{{secret:db}}' > q.txt`,
|
||||
`printf %s 'pre-{{secret:db}}-post' | sed 's/^pre-//; s/-post$//' > q.txt`,
|
||||
`printf %s "$(printf %s '{{secret:db}}')" > q.txt`,
|
||||
"printf %s \"`printf %s '{{secret:db}}'`\" > q.txt",
|
||||
`x="$(echo "$(printf %s {{secret:db}})")"; printf %s "$x" > q.txt`,
|
||||
} {
|
||||
res = call(t, r, "shell", map[string]any{"script": script})
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "q.txt"))
|
||||
if !res.OK || string(data) != `s3cr3t'; rm -rf / #` {
|
||||
t.Errorf("%s: got %q (%s)", script, data, res.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
// Marcador desconocido: error, nunca se usa el texto literal.
|
||||
if res := call(t, r, "exec", map[string]any{"command": "echo", "args": []string{"{{secret:nope}}"}}); res.OK {
|
||||
t.Error("unknown placeholder must fail")
|
||||
}
|
||||
logData, _ := os.ReadFile(filepath.Join(dir, "agent.log"))
|
||||
if strings.Contains(string(logData), "s3cr3t") {
|
||||
t.Error("secret leaked into agent.log")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAskSecretErrors(t *testing.T) {
|
||||
r, env, _ := newEnv(t, "")
|
||||
env.Secrets = NewSecretStore()
|
||||
env.Prompt = func(context.Context, string) (string, error) { return "", errors.New("secret input cancelled") }
|
||||
if res := call(t, r, "ask_secret", map[string]any{"name": "bad name!", "prompt": "x"}); res.OK {
|
||||
t.Error("invalid name must fail")
|
||||
}
|
||||
if res := call(t, r, "ask_secret", map[string]any{"name": "x", "prompt": "x"}); res.OK {
|
||||
t.Error("cancelled input must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransientRetry(t *testing.T) {
|
||||
old := retryScale
|
||||
retryScale = 0
|
||||
defer func() { retryScale = old }()
|
||||
r, _, dir := newEnv(t, "EXECUTION_MODE=privileged")
|
||||
script := `n=$(cat count 2>/dev/null || echo 0); n=$((n+1)); echo $n > count
|
||||
if [ "$n" -lt 3 ]; then echo "E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 123" >&2; exit 100; fi
|
||||
echo installed`
|
||||
res := call(t, r, "shell", map[string]any{"script": script})
|
||||
if !res.OK || !strings.Contains(res.Output, "installed") || !strings.Contains(res.Output, "retry 2/3") {
|
||||
t.Fatalf("%+v", res.Output)
|
||||
}
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "count"))
|
||||
if strings.TrimSpace(string(data)) != "3" {
|
||||
t.Errorf("attempts = %s", data)
|
||||
}
|
||||
|
||||
// Un error permanente no se reintenta.
|
||||
os.Remove(filepath.Join(dir, "count"))
|
||||
res = call(t, r, "shell", map[string]any{"script": `n=$(cat count 2>/dev/null || echo 0); echo $((n+1)) > count; echo "E: Unable to locate package foo" >&2; exit 100`})
|
||||
data, _ = os.ReadFile(filepath.Join(dir, "count"))
|
||||
if res.OK || strings.TrimSpace(string(data)) != "1" {
|
||||
t.Errorf("permanent error retried: attempts=%s", data)
|
||||
}
|
||||
|
||||
// Límite de reintentos.
|
||||
res = call(t, r, "shell", map[string]any{"script": `echo "Temporary failure in name resolution" >&2; exit 2`})
|
||||
if res.OK || !strings.Contains(res.Output, "retry 2/2") || strings.Contains(res.Output, "retry 3/") {
|
||||
t.Errorf("retry limit: %s", res.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionDeniedHint(t *testing.T) {
|
||||
r, _, _ := newEnv(t, "EXECUTION_MODE=privileged")
|
||||
res := call(t, r, "shell", map[string]any{"script": `echo "open /etc/shadow: Permission denied" >&2; exit 1`})
|
||||
if !strings.Contains(res.Output, "hint:") {
|
||||
t.Errorf("missing escalation hint: %s", res.Output)
|
||||
}
|
||||
res = call(t, r, "exec", map[string]any{"command": "false"})
|
||||
if strings.Contains(res.Output, "hint:") {
|
||||
t.Error("hint must appear only for permission errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRecordsDiff(t *testing.T) {
|
||||
r, _, dir := newEnv(t, "")
|
||||
os.WriteFile(filepath.Join(dir, "sshd_config"), []byte("Port 22\nPermitRootLogin yes\n"), 0o600)
|
||||
res := call(t, r, "edit_file", map[string]any{"path": "sshd_config", "old_string": "PermitRootLogin yes", "new_string": "PermitRootLogin no", "reason": "hardening"})
|
||||
if !res.OK || !strings.Contains(res.Diff, "-PermitRootLogin yes\n+PermitRootLogin no") || !strings.Contains(res.Output, "change:") {
|
||||
t.Fatalf("%+v", res)
|
||||
}
|
||||
logData, _ := os.ReadFile(filepath.Join(dir, "agent.log"))
|
||||
if !strings.Contains(string(logData), `diff=`) || !strings.Contains(string(logData), `reason="hardening"`) {
|
||||
t.Errorf("agent.log must record diff and reason:\n%s", logData)
|
||||
}
|
||||
// Archivo nuevo: sin diff.
|
||||
res = call(t, r, "write_file", map[string]any{"path": "new.txt", "content": "x\n"})
|
||||
if res.Diff != "" {
|
||||
t.Error("new files have no previous value")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user