Skip to content

Commit

Permalink
add e2e tests for spoc
Browse files Browse the repository at this point in the history
  • Loading branch information
mhils committed Mar 1, 2024
1 parent 1c1e047 commit 82a24da
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,21 @@ jobs:
$RUN "echo \"export E2E_TEST_FLAKY_TESTS_ONLY=true\" >> /vagrant/hack/ci/env-flatcar.sh"
$RUN hack/ci/e2e-flatcar-dev-container.sh
e2e-spoc:
runs-on: ubuntu-22.04
timeout-minutes: 90
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3.5.2
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491
with:
go-version: ${{ env.GO_VERSION }}
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y libseccomp-dev libelf-dev libapparmor-dev
sudo hack/install-libbpf.sh
- run: make test-spoc-e2e

baseprofiles:
needs: image
runs-on: ubuntu-22.04
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,10 @@ test-flaky-e2e: ## Only run the flaky end-to-end tests
E2E_SKIP_FLAKY_TESTS=false \
$(GO) test -parallel 1 -timeout 20m -count=1 ./test/... -v -testify.m '^(TestSecurityProfilesOperator_Flaky)$$'

.PHONY: test-spoc-e2e
test-spoc-e2e: build/spoc
$(GO) test -v ./test/spoc/...

# Generate CRD manifests
manifests: $(BUILD_DIR)/kubernetes-split-yaml $(BUILD_DIR)/kustomize
./hack/sort-crds.sh "$(CONTROLLER_GEN_CMD) $(CRD_OPTIONS) paths='./api/spod/...' output:crd:stdout" "deploy/base-crds/crds/securityprofilesoperatordaemon.yaml"
Expand Down
2 changes: 2 additions & 0 deletions test/spoc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
demobinary
demobinary-child
142 changes: 142 additions & 0 deletions test/spoc/demobinary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

// #cgo LDFLAGS: -ldl
// #include <dlfcn.h>
import "C"

import (
"flag"
"fmt"
"log"
"net"
"os"
"os/exec"
"strings"
"syscall"
"time"
)

const LogPrefixEnvVar = "LOGPREFIX"

// Demo binary to exercise various capabilities that may be restricted by seccomp/apparmor.
func main() {
log.SetPrefix(fmt.Sprintf("%s[pid:%d] ", os.Getenv(LogPrefixEnvVar), os.Getpid()))
log.SetFlags(log.Lshortfile)
log.Println("⏩", os.Args)

fileWrite := flag.String("file-write", "", "write file (e.g. /dev/null)")
fileRead := flag.String("file-read", "", "read file (e.g. /dev/null)")
fileSymlink := flag.String("file-symlink", "", "Create symlink using the following syntax: OLD:NEW")
netTCP := flag.Bool("net-tcp", false, "spawn a tcp server")
netUDP := flag.Bool("net-udp", false, "spawn a udp server")
netIcmp := flag.Bool("net-icmp", false, "open an icmp socket, exercise NET_RAW capability.")
library := flag.String("load-library", "", "load a shared library")
sleep := flag.Int("sleep", 0, "sleep N seconds before exiting.")
crash := flag.Bool("crash", false, "crash instead of exiting.")

flag.Parse()

subprocess := flag.Args()

if *fileWrite != "" {
const fileMode = 0o666
err := os.WriteFile(*fileWrite, []byte{}, fileMode)
if err != nil {
log.Fatal("❌ Error creating file:", err)
}
log.Println("✅ File write successful:", *fileWrite)
// make file writable for other users so that sudo/non-sudo testing works.
err = os.Chmod(*fileWrite, fileMode)
if err != nil {
log.Fatal("❌ Error setting file permissions:", err)
}
}
if *fileSymlink != "" {
oldname, newname, found := strings.Cut(*fileSymlink, ":")
if !found {
log.Fatal("❌ Symlink syntax: OLD:NEW")
}
err := os.Symlink(oldname, newname)
if err != nil {
log.Fatal("❌ Error creating symlink:", err)
}
log.Println("✅ Symlink created:", newname, "->", oldname)
}
if *fileRead != "" {
_, err := os.ReadFile(*fileRead)
if err != nil {
log.Fatal("❌ Error reading file:", err)
}
log.Println("✅ File read successful:", *fileRead)
}
if *netTCP {
listener, err := net.Listen("tcp", ":0")
if err != nil {
log.Fatal("❌ Error starting TCP server:", err)
}
log.Println("✅ TCP server spawned:", listener.Addr())
defer listener.Close()
}
if *netUDP {
server, err := net.ListenPacket("udp", ":0")
if err != nil {
//nolint:gocritic // gocritic is terminally confused here.
log.Fatal("❌ Error starting UDP server:", err)
}
log.Println("✅ UDP server spawned:", server.LocalAddr())
defer server.Close()
}
if *netIcmp {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_ICMP)
if err != nil {
log.Fatal("❌ Error opening ICMP socket:", err)
}
log.Println("✅ ICMP socket opened: fd", fd)
defer syscall.Close(fd)
}
if len(subprocess) > 0 {
cmd := exec.Command(subprocess[0], subprocess[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "LOGPREFIX=\t"+os.Getenv(LogPrefixEnvVar))
err := cmd.Run()
if err != nil {
log.Fatal("❌ Error running subprocess:", err)
}
log.Println("✅ Subprocess ran successfully:", subprocess)
}
if *library != "" {
if handle := C.dlopen(C.CString(*library), C.RTLD_NOW); handle == nil {
log.Fatal("❌ Error loading library: ", C.GoString(C.dlerror()))
}
log.Println("✅ Library loaded successfully:", *library)
}
if *sleep > 0 {
log.Println("⏳ Sleeping for", *sleep, "seconds...")
time.Sleep(time.Duration(*sleep) * time.Second)
}
if *crash {
log.Println("🫡 Terminating with SIGKILL...")
err := syscall.Kill(syscall.Getpid(), syscall.SIGKILL)
if err != nil {
log.Fatal("❌ Error sending SIGKILL:", err)
}
}
log.Println("⭐️ Success.")
}
120 changes: 120 additions & 0 deletions test/spoc/e2e_spoc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main_test

import (
"os"
"os/exec"
"testing"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"

"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"sigs.k8s.io/release-utils/util"
apparmorprofileapi "sigs.k8s.io/security-profiles-operator/api/apparmorprofile/v1alpha1"
seccompprofileapi "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1beta1"
)

//nolint:paralleltest // should not run in parallel
func TestSpoc(t *testing.T) {
logrus.Info("Building demobinary...")
cmd := exec.Command("go", "build", "demobinary.go")
err := cmd.Run()
require.Nil(t, err, "failed to build demobinary.go")
err = util.CopyFileLocal("demobinary", "demobinary-child", true)
require.Nil(t, err)
err = os.Chmod("demobinary-child", 0o700)
require.Nil(t, err)

t.Run("record", recordTest)
}

func recordTest(t *testing.T) {
t.Run("AppArmor", recordAppArmorTest)
t.Run("Seccomp", recordSeccompTest)
}

func recordAppArmorTest(t *testing.T) {
t.Run("files", func(t *testing.T) {
profile := recordAppArmor(t, "--file-read", "../../README.md", "--file-write", "/dev/null")
// TODO: This is still wrong - it should be an absolute path.
require.Contains(t, *profile.Filesystem.ReadOnlyPaths, "../../README.md")
require.Contains(t, *profile.Filesystem.WriteOnlyPaths, "/dev/null")

profile = recordAppArmor(t, "--file-read", "/dev/null", "--file-write", "/dev/null")
require.Contains(t, *profile.Filesystem.ReadWritePaths, "/dev/null")
})
t.Run("sockets", func(t *testing.T) {
profile := recordAppArmor(t, "--net-tcp")
require.True(t, *profile.Network.Protocols.AllowTCP)
profile = recordAppArmor(t, "--net-udp")
require.True(t, *profile.Network.Protocols.AllowUDP)
profile = recordAppArmor(t, "--net-icmp")
require.True(t, *profile.Network.AllowRaw)
})

t.Run("subprocess", func(t *testing.T) {
profile := recordAppArmor(t, "./demobinary-child", "--file-read", "/dev/null")
require.Contains(t, (*profile.Executable.AllowedExecutables)[0], "/demobinary-child")
// TODO: requires child process tracing
// require.Contains(t, *profile.Filesystem.ReadOnlyPaths, "/dev/null")

profile = recordAppArmor(t, "./demobinary", "--file-read", "/dev/null")
require.Contains(t, (*profile.Executable.AllowedExecutables)[0], "/demobinary")
// TODO: requires child process tracing
// require.Contains(t, *profile.Filesystem.ReadOnlyPaths, "/dev/null")
})
}

func recordSeccompTest(t *testing.T) {
profile := recordSeccomp(t, "--net-tcp")
require.Contains(t, profile.Syscalls[0].Names, "listen")
}

func runSpoc(t *testing.T, args ...string) []byte {
t.Helper()
args = append([]string{"../../build/spoc"}, args...)
cmd := exec.Command("sudo", args...)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
require.Nil(t, err, "failed to run spoc")
return out
}

func record(t *testing.T, typ string, profile client.Object, args ...string) {
t.Helper()
args = append([]string{
"record", "-t", typ, "-o", "/dev/stdout", "--no-base-syscalls", "./demobinary",
}, args...)
content := runSpoc(t, args...)
err := yaml.Unmarshal(content, &profile)
require.Nil(t, err, "failed to parse yaml")
}

func recordAppArmor(t *testing.T, args ...string) apparmorprofileapi.AppArmorAbstract {

Check failure on line 110 in test/spoc/e2e_spoc_test.go

View workflow job for this annotation

GitHub Actions / macos

test helper function should start from t.Helper() (thelper)
profile := apparmorprofileapi.AppArmorProfile{}
record(t, "apparmor", &profile, args...)
return profile.Spec.Abstract
}

func recordSeccomp(t *testing.T, args ...string) seccompprofileapi.SeccompProfileSpec {

Check failure on line 116 in test/spoc/e2e_spoc_test.go

View workflow job for this annotation

GitHub Actions / macos

test helper function should start from t.Helper() (thelper)
profile := seccompprofileapi.SeccompProfile{}
record(t, "seccomp", &profile, args...)
return profile.Spec
}

0 comments on commit 82a24da

Please sign in to comment.