go-scm/agentci/security_test.go
Virgil a54bd834ff fix(agentci): restore security helpers and map attack vectors
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 13:15:55 +00:00

251 lines
5.3 KiB
Go

// SPDX-Licence-Identifier: EUPL-1.2
package agentci
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSanitizePath_Good(t *testing.T) {
tests := []struct {
input string
want string
}{
{"simple", "simple"},
{"with-dash", "with-dash"},
{"with_underscore", "with_underscore"},
{"with.dot", "with.dot"},
{"CamelCase", "CamelCase"},
{"123", "123"},
{"path/to/file.txt", "file.txt"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := SanitizePath(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestSanitizePath_Bad(t *testing.T) {
tests := []struct {
name string
input string
}{
{"spaces", "has space"},
{"special chars", "file@name"},
{"backtick", "file`name"},
{"semicolon", "file;name"},
{"pipe", "file|name"},
{"ampersand", "file&name"},
{"dollar", "file$name"},
{"parent traversal base", ".."},
{"root", "/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := SanitizePath(tt.input)
assert.Error(t, err)
})
}
}
func TestValidatePathElement_Good(t *testing.T) {
tests := []struct {
input string
want string
}{
{"simple", "simple"},
{"with-dash", "with-dash"},
{"with_underscore", "with_underscore"},
{"with.dot", "with.dot"},
{"ticket-host-uk-core-1.json", "ticket-host-uk-core-1.json"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := ValidatePathElement(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestValidatePathElement_Bad(t *testing.T) {
tests := []struct {
name string
input string
}{
{"empty", ""},
{"space", "has space"},
{"leading space", " name"},
{"slash", "org/repo"},
{"backslash", `org\\repo`},
{"traversal", ".."},
{"absolute", "/tmp"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ValidatePathElement(tt.input)
assert.Error(t, err)
})
}
}
func TestResolvePathWithinRoot_Good(t *testing.T) {
root := t.TempDir()
gotName, gotPath, err := ResolvePathWithinRoot(root, "plugin-one")
require.NoError(t, err)
assert.Equal(t, "plugin-one", gotName)
assert.Equal(t, filepath.Join(root, "plugin-one"), gotPath)
}
func TestResolvePathWithinRoot_Bad(t *testing.T) {
root := t.TempDir()
_, _, err := ResolvePathWithinRoot(root, "../escape")
require.Error(t, err)
}
func TestValidateRemoteDir_Good(t *testing.T) {
tests := []struct {
input string
want string
}{
{"/tmp/queue", "/tmp/queue"},
{"/tmp//queue/", "/tmp/queue"},
{"~/ai-work/queue", "~/ai-work/queue"},
{"~/ai-work//queue/", "~/ai-work/queue"},
{"~", "~"},
{"/", "/"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := ValidateRemoteDir(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestValidateRemoteDir_Bad(t *testing.T) {
tests := []struct {
name string
input string
}{
{"empty", ""},
{"relative", "queue"},
{"shell metachar", "/tmp/queue; touch /tmp/pwned"},
{"space", "/tmp/queue name"},
{"traversal", "~/../queue"},
{"newline", "/tmp/queue\nother"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ValidateRemoteDir(tt.input)
assert.Error(t, err)
})
}
}
func TestJoinRemotePath_Good(t *testing.T) {
tests := []struct {
name string
base string
elem string
want string
}{
{"absolute", "/tmp/queue", "ticket.json", "/tmp/queue/ticket.json"},
{"home relative", "~/ai-work/queue", "ticket.json", "~/ai-work/queue/ticket.json"},
{"home root", "~", ".env.ticket", "~/.env.ticket"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := JoinRemotePath(tt.base, tt.elem)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestJoinRemotePath_Bad(t *testing.T) {
_, err := JoinRemotePath("/tmp/queue", "../ticket.json")
assert.Error(t, err)
}
func TestEscapeShellArg_Good(t *testing.T) {
tests := []struct {
input string
want string
}{
{"simple", "'simple'"},
{"with spaces", "'with spaces'"},
{"it's", "'it'\\''s'"},
{"", "''"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, EscapeShellArg(tt.input))
})
}
}
func TestSecureSSHCommand_Good(t *testing.T) {
cmd := SecureSSHCommand("host.example.com", "ls -la")
args := cmd.Args
assert.Equal(t, "ssh", args[0])
assert.Contains(t, args, "-o")
assert.Contains(t, args, "StrictHostKeyChecking=yes")
assert.Contains(t, args, "BatchMode=yes")
assert.Contains(t, args, "ConnectTimeout=10")
assert.Equal(t, "host.example.com", args[len(args)-2])
assert.Equal(t, "ls -la", args[len(args)-1])
}
func TestMaskToken_Good(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"long token", "abcdefghijklmnop", "abcd****mnop"},
{"exactly 8", "12345678", "1234****5678"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, MaskToken(tt.input))
})
}
}
func TestMaskToken_Bad(t *testing.T) {
tests := []struct {
name string
input string
}{
{"short", "abc"},
{"empty", ""},
{"seven chars", "1234567"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, "*****", MaskToken(tt.input))
})
}
}