From b30a2c3e0c2e16e41c0bca3235365243938e4367 Mon Sep 17 00:00:00 2001 From: Shahriyar Jalayeri Date: Fri, 3 Nov 2023 12:31:21 +0100 Subject: [PATCH] add security test suite to eden Signed-off-by: Shahriyar Jalayeri --- tests/sec/Makefile | 75 ++++++++++++++++++ tests/sec/eden-config.yml | 7 ++ tests/sec/eden.sec.tests.txt | 1 + tests/sec/remote.go | 88 +++++++++++++++++++++ tests/sec/sec_test.go | 110 +++++++++++++++++++++++++++ tests/workflow/smoke.tests.txt | 4 +- tests/workflow/testdata/sec_eden.txt | 1 + 7 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 tests/sec/Makefile create mode 100644 tests/sec/eden-config.yml create mode 100644 tests/sec/eden.sec.tests.txt create mode 100644 tests/sec/remote.go create mode 100644 tests/sec/sec_test.go create mode 100644 tests/workflow/testdata/sec_eden.txt diff --git a/tests/sec/Makefile b/tests/sec/Makefile new file mode 100644 index 000000000..b37fb25fe --- /dev/null +++ b/tests/sec/Makefile @@ -0,0 +1,75 @@ +DEBUG ?= "debug" + +# HOSTARCH is the host architecture +# ARCH is the target architecture +# we need to keep track of them separately +HOSTARCH ?= $(shell uname -m) +HOSTOS ?= $(shell uname -s | tr A-Z a-z) + +# canonicalized names for host architecture +override HOSTARCH := $(subst aarch64,arm64,$(subst x86_64,amd64,$(HOSTARCH))) + +# unless otherwise set, I am building for my own architecture, i.e. not cross-compiling +# and for my OS +ARCH ?= $(HOSTARCH) +OS ?= $(HOSTOS) + +# canonicalized names for target architecture +override ARCH := $(subst aarch64,arm64,$(subst x86_64,amd64,$(ARCH))) + +WORKDIR ?= $(CURDIR)/../../dist +TESTDIR := tests/$(shell basename $(CURDIR)) +BINDIR := $(WORKDIR)/bin +DATADIR := $(WORKDIR)/$(TESTDIR)/ +BIN := eden +LOCALBIN := $(BINDIR)/$(BIN)-$(OS)-$(ARCH) +TESTNAME := eden.sec +TESTBIN := $(TESTNAME).test +TESTSCN := $(TESTNAME).tests.txt +LOCALTESTBIN := $(TESTBIN)-$(OS)-$(ARCH) +LINKDIR := ../../tests/sec + +.DEFAULT_GOAL := help + +clean: + rm -rf $(LOCALTESTBIN) $(BINDIR)/$(TESTBIN) $(WORKDIR)/$(TESTSCN) $(CURDIR)/$(TESTBIN) $(BINDIR)/$(TESTBIN) + +$(BINDIR): + mkdir -p $@ +$(DATADIR): + mkdir -p $@ + +test_sec: + go test sec_test.go common.go -v -count=1 -timeout 3000s + +test: + $(LOCALBIN) test $(CURDIR) -v $(DEBUG) + +build: setup + +testbin: $(TESTBIN) +$(LOCALTESTBIN): $(BINDIR) *.go + CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go test -c -ldflags "-s -w" -o $@ *.go + +$(TESTBIN): $(LOCALTESTBIN) + ln -sf $(LOCALTESTBIN) $(CURDIR)/$(TESTBIN) + +setup: testbin $(BINDIR) $(DATADIR) + cp -a $(LOCALTESTBIN) $(CURDIR)/$(TESTBIN) $(BINDIR) + cp -a *.yml $(TESTSCN) $(DATADIR) + +.PHONY: test build setup clean all testbin + +help: + @echo "EDEN is the harness for testing EVE and ADAM" + @echo + @echo "This Makefile automates commons tasks of EDEN testing" + @echo + @echo "Commonly used maintenance and development targets:" + @echo " build build test-binary (OS and ARCH options supported, for ex. OS=linux ARCH=arm64)" + @echo " setup setup of test environment" + @echo " test run tests" + @echo " clean cleanup of test harness" + @echo + @echo "You need install requirements for EVE (look at https://github.com/lf-edge/eve#install-dependencies)." + @echo "You need access to docker socket and installed qemu packages." diff --git a/tests/sec/eden-config.yml b/tests/sec/eden-config.yml new file mode 100644 index 000000000..276736307 --- /dev/null +++ b/tests/sec/eden-config.yml @@ -0,0 +1,7 @@ +--- +eden: + # test binary + test-bin: "eden.sec.test" + + # test scenario + test-scenario: "eden.sec.tests.txt" diff --git a/tests/sec/eden.sec.tests.txt b/tests/sec/eden.sec.tests.txt new file mode 100644 index 000000000..1a62e0793 --- /dev/null +++ b/tests/sec/eden.sec.tests.txt @@ -0,0 +1 @@ +eden.sec.test diff --git a/tests/sec/remote.go b/tests/sec/remote.go new file mode 100644 index 000000000..c259a3af9 --- /dev/null +++ b/tests/sec/remote.go @@ -0,0 +1,88 @@ +package sec_test + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/lf-edge/eden/pkg/defaults" + "github.com/lf-edge/eden/pkg/openevec" + "github.com/lf-edge/eden/pkg/utils" +) + +type remoteNode struct { + openEVEC *openevec.OpenEVEC +} + +func getOpenEVEC() *openevec.OpenEVEC { + edenConfigEnv := os.Getenv(defaults.DefaultConfigEnv) + configName := utils.GetConfig(edenConfigEnv) + + viperCfg, err := openevec.FromViper(configName, "debug") + if err != nil { + return nil + } + + return openevec.CreateOpenEVEC(viperCfg) +} + +func createRemoteNode() *remoteNode { + evec := getOpenEVEC() + if evec == nil { + return nil + } + + return &remoteNode{openEVEC: evec} +} + +func (node *remoteNode) runCommand(command string) ([]byte, error) { + realStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + return nil, err + } + + os.Stdout = w + + // unfortunately, we can't capture command return value from SSHEve + err = node.openEVEC.SSHEve(command) + + os.Stdout = realStdout + w.Close() + + if err != nil { + return nil, err + } + + out, _ := io.ReadAll(r) + return out, nil +} + +func (node *remoteNode) fileExists(fileName string) (bool, error) { + command := fmt.Sprintf("if stat \"%s\"; then echo \"1\"; else echo \"0\"; fi", fileName) + out, err := node.runCommand(command) + if err != nil { + return false, err + } + + if strings.TrimSpace(string(out)) == "0" { + return false, nil + } + + return true, nil +} + +func (node *remoteNode) readFile(fileName string) ([]byte, error) { + exist, err := node.fileExists(fileName) + if err != nil { + return nil, err + } + + if !exist { + return nil, fmt.Errorf("file %s does not exist", fileName) + } + + command := fmt.Sprintf("cat %s", fileName) + return node.runCommand(command) +} diff --git a/tests/sec/sec_test.go b/tests/sec/sec_test.go new file mode 100644 index 000000000..537606930 --- /dev/null +++ b/tests/sec/sec_test.go @@ -0,0 +1,110 @@ +package sec_test + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/lf-edge/eden/pkg/device" + "github.com/lf-edge/eden/pkg/projects" + "github.com/lf-edge/eden/pkg/tests" +) + +var ( + tc *projects.TestContext + rnode *remoteNode +) + +// TestMain is used to provide setup and teardown for the rest of the +// tests. As part of setup we make sure that context has a slice of +// EVE instances that we can operate on. For any action, if the instance +// is not specified explicitly it is assumed to be the first one in the slice +func TestMain(m *testing.M) { + log.Println("Security Test Suite started") + + tests.TestArgsParse() + + tc = projects.NewTestContext() + + projectName := fmt.Sprintf("%s_%s", "TestSecurity", time.Now()) + + // Registering our own project namespace with controller for easy cleanup + tc.InitProject(projectName) + + // Create representation of EVE instances (based on the names + // or UUIDs that were passed in) in the context. This is the first place + // where we're using zcli-like API: + for _, node := range tc.GetNodeDescriptions() { + edgeNode := node.GetEdgeNode(tc) + if edgeNode == nil { + // Couldn't find existing edgeNode record in the controller. + // Need to create it from scratch now: + // this is modeled after: zcli edge-node create + // --project= --model= [--title=] + // ([--edge-node-certificate=<certificate>] | + // [--onboarding-certificate=<certificate>] | + // [(--onboarding-key=<key> --serial=<serial-number>)]) + // [--network=<network>...] + // + // XXX: not sure if struct (giving us optional fields) would be better + edgeNode = tc.NewEdgeNode(tc.WithNodeDescription(node), tc.WithCurrentProject()) + } else { + // make sure to move EdgeNode to the project we created, again + // this is modeled after zcli edge-node update <name> [--title=<title>] + // [--lisp-mode=experimental|default] [--project=<project>] + // [--clear-onboarding-certs] [--config=<key:value>...] [--network=<network>...] + edgeNode.SetProject(projectName) + } + + tc.ConfigSync(edgeNode) + + // finally we need to make sure that the edgeNode is in a state that we need + // it to be, before the test can run -- this could be multiple checks on its + // status, but for example: + if edgeNode.GetState() == device.NotOnboarded { + log.Fatal("Node is not onboarded now") + } + + // this is a good node -- lets add it to the test context + tc.AddNode(edgeNode) + } + + tc.StartTrackingState(false) + + // create a remote node + rnode = createRemoteNode() + if rnode == nil { + log.Fatal("Can't initlize the remote node") + } + + // we now have a situation where TestContext has enough EVE nodes known + // for the rest of the tests to run. So run them: + res := m.Run() + + // Finally, we need to cleanup whatever objects may be in in the + // project we created and then we can exit + os.Exit(res) +} + +func TestAppArmorEnabled(t *testing.T) { + log.Println("TestAppArmorEnabled started") + defer log.Println("TestAppArmorEnabled finished") + t.Parallel() + + edgeNode := tc.GetEdgeNode(tc.WithTest(t)) + tc.WaitForState(edgeNode, 60) + + out, err := rnode.readFile("/sys/module/apparmor/parameters/enabled") + if err != nil { + t.Fatal(err) + } + + exits := strings.TrimSpace(string(out)) + if exits != "Y" { + t.Fatal("AppArmor is not enabled") + } +} diff --git a/tests/workflow/smoke.tests.txt b/tests/workflow/smoke.tests.txt index 1e4a15a7e..717433a4e 100644 --- a/tests/workflow/smoke.tests.txt +++ b/tests/workflow/smoke.tests.txt @@ -1,5 +1,5 @@ # Number of tests -{{$tests := 21}} +{{$tests := 22}} # EDEN_TEST_SETUP env. var. -- "y"(default) performs the EDEN setup steps {{$setup := "y"}} {{$setup_env := EdenGetEnv "EDEN_TEST_SETUP"}} @@ -77,3 +77,5 @@ eden.escript.test -testdata ../eclient/testdata/ -test.run TestEdenScripts/shutd /bin/echo EVE reset (21/{{$tests}}) eden.escript.test -test.run TestEdenScripts/eden_reset +/bin/echo EVE security tests (22/{{$tests}}) +eden.escript.test -test.run TestEdenScripts/sec_eden diff --git a/tests/workflow/testdata/sec_eden.txt b/tests/workflow/testdata/sec_eden.txt new file mode 100644 index 000000000..4eebdaf71 --- /dev/null +++ b/tests/workflow/testdata/sec_eden.txt @@ -0,0 +1 @@ +test eden.sec.test