155 lines
4.7 KiB
Go
155 lines
4.7 KiB
Go
//go:build linux
|
|
|
|
package deployment
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestRecoveryInspectionOverPasswordSFTP(t *testing.T) {
|
|
signer := newTestECDSAHostSigner(t)
|
|
endpoint := startTestPasswordSSHServerWithHandler(t, func(password string) error {
|
|
if password != "test-password" {
|
|
return errors.New("invalid test password")
|
|
}
|
|
return nil
|
|
}, serveTestSFTP, signer)
|
|
for _, scenario := range []string{"trusted", "legacy", "target-symlink", "target-directory", "target-unreadable", "target-oversized", "owner-oversized", "owner-symlink", "lock-symlink"} {
|
|
t.Run(scenario, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
server, memory, record := recoveryPreflightFixture(root)
|
|
model := testSSHServerModel(endpoint, "secret://ssh/inspection", ssh.FingerprintSHA256(signer.PublicKey()))
|
|
server.Address, server.Port, server.Username = model.Address, model.Port, model.Username
|
|
server.AuthMethod, server.PasswordRef, server.HostKeyFingerprint = model.AuthMethod, model.PasswordRef, model.HostKeyFingerprint
|
|
record.Evidence.Address, record.Evidence.Port, record.Evidence.HostKeyFingerprint = server.Address, server.Port, server.HostKeyFingerprint
|
|
for path, file := range memory.files {
|
|
if strings.HasSuffix(path, ".scert-lock") {
|
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, file.data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
target := record.Paths[0]
|
|
lock := record.LockPaths[0]
|
|
owner := lock + "/owner"
|
|
switch scenario {
|
|
case "legacy":
|
|
record.Evidence = nil
|
|
case "target-symlink", "owner-symlink", "lock-symlink":
|
|
name := target
|
|
if scenario == "owner-symlink" {
|
|
name = owner
|
|
}
|
|
if scenario == "lock-symlink" {
|
|
name = lock
|
|
}
|
|
if err := os.Rename(name, name+".original"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(name+".original", name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "target-directory":
|
|
if err := os.Rename(target, target+".original"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "target-unreadable":
|
|
if os.Geteuid() == 0 {
|
|
t.Skip("requires unprivileged Linux test user")
|
|
}
|
|
if err := os.Chmod(target, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "target-oversized":
|
|
if err := os.Truncate(target, 16*1024*1024+1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "owner-oversized":
|
|
if err := os.Truncate(owner, 129); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
before := snapshotInspectionFiles(t, root)
|
|
journal := &memoryTransactionJournal{pending: []TransactionRecord{record}}
|
|
service := NewServiceWithJournal(SSHFactory{
|
|
Secrets: staticSecretReader{server.PasswordRef: {"password": "test-password"}},
|
|
Timeout: 5 * time.Second,
|
|
}, journal)
|
|
result, err := service.InspectPending(t.Context(), record.ID, func(context.Context, string) (Server, error) { return server, nil })
|
|
if scenario == "trusted" || scenario == "legacy" {
|
|
if err != nil || len(result.Files) != 9 || len(result.Locks) != 3 || result.HistoricalEvidence != (scenario == "trusted") {
|
|
t.Fatalf("inspection failed: %+v %v", result, err)
|
|
}
|
|
for _, file := range result.Files {
|
|
want := "match"
|
|
if scenario == "legacy" {
|
|
want = "unrecorded"
|
|
}
|
|
if file.Comparison != want {
|
|
t.Fatalf("unexpected observation: %+v", file)
|
|
}
|
|
}
|
|
} else if !errors.Is(err, ErrRecoveryInspectionRead) || !reflect.DeepEqual(result, RecoveryInspection{}) {
|
|
t.Fatalf("unsafe or unreadable file was accepted: %+v %v", result, err)
|
|
}
|
|
if !reflect.DeepEqual(before, snapshotInspectionFiles(t, root)) || len(journal.updated) != 0 || len(journal.finished) != 0 {
|
|
t.Fatal("read-only inspection changed files or journal")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func snapshotInspectionFiles(t *testing.T, root string) map[string]string {
|
|
t.Helper()
|
|
result := make(map[string]string)
|
|
err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value := info.Mode().String()
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
target, err := os.Readlink(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value += target
|
|
} else if info.Mode().IsRegular() && info.Mode().Perm() != 0 {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value += contentDigest(data)
|
|
}
|
|
result[path] = value
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result
|
|
}
|