diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 744e098..535da00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,9 +3,9 @@ name: Build on: push: - branches: [ "main" ] + branches: [ "main", "dev" ] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] jobs: build: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 7655bbf..553e170 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -BINARY_NAME = experiments-runtime-tool +BINARY_NAME = secops-chaos REPO_NAME = github.com/operantai/$(BINARY_NAME) GIT_COMMIT = $(shell git rev-list -1 HEAD) BUILD_DATE = $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") @@ -8,7 +8,7 @@ LD_FLAGS = "-X $(REPO_NAME)/cmd.GitCommit=$(GIT_COMMIT) -X $(REPO_NAME)/cmd.Vers all: fmt vet test build build: ## Build binary - @go build -o "bin/$(BINARY_NAME)" -ldflags $(LD_FLAGS) main.go + @go build -o "bin/$(BINARY_NAME)" -ldflags $(LD_FLAGS) cmd/cli/main.go fmt: ## Run go fmt @go fmt ./... diff --git a/README.md b/README.md index a12a62a..0653290 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Experiments Runtime Tool +# secops-chaos ### Usage ``` sh Usage: - experiments-runtime-tool [command] + secops-chaos [command] Available Commands: clean Clean up after an experiment run @@ -15,7 +15,7 @@ Available Commands: version Output CLI version information Flags: - -h, --help help for experiments-runtime-tool + -h, --help help for secops-chaos -Use "experiments-runtime-tool [command] --help" for more information about a command. +Use "secops-chaos [command] --help" for more information about a command. ``` diff --git a/cmd/clean.go b/cmd/clean.go deleted file mode 100644 index 098b288..0000000 --- a/cmd/clean.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2023 Operant AI -*/ -package cmd - -import ( - "github.com/operantai/experiments-runtime-tool/internal/experiments" - "github.com/spf13/cobra" -) - -// cleanCmd represents the clean command -var cleanCmd = &cobra.Command{ - Use: "clean", - Short: "Clean up after an experiment run", - Long: "Clean up after an experiment run", - Run: func(cmd *cobra.Command, args []string) { - ctx := cmd.Context() - er := experiments.NewRunner(ctx, []string{""}) - er.Cleanup() - }, -} - -func init() { - rootCmd.AddCommand(cleanCmd) - - // Define the path of the experiment file to run - cleanCmd.Flags().StringP("file", "f", "", "Experiment file to run") - cleanCmd.MarkFlagRequired("file") - - // Define the namespace(s) to run the experiment in - cleanCmd.Flags().StringP("namespace", "n", "", "Namespace to run experiment in") - cleanCmd.Flags().StringP("all", "a", "", "Run experiment in all namespaces") - cleanCmd.MarkFlagsMutuallyExclusive("namespace", "all") -} diff --git a/cmd/cli/cmd/clean.go b/cmd/cli/cmd/clean.go new file mode 100644 index 0000000..1b3a882 --- /dev/null +++ b/cmd/cli/cmd/clean.go @@ -0,0 +1,50 @@ +/* +Copyright 2023 Operant AI +*/ +package cmd + +import ( + "github.com/operantai/secops-chaos/internal/experiments" + "github.com/operantai/secops-chaos/internal/output" + "github.com/spf13/cobra" +) + +// cleanCmd represents the clean command +var cleanCmd = &cobra.Command{ + Use: "clean", + Short: "Clean up after an experiment run", + Long: "Clean up after an experiment run", + Run: func(cmd *cobra.Command, args []string) { + // Read the flags + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + output.WriteError("Error reading namespace flag: %v", err) + } + allNamespaces, err := cmd.Flags().GetBool("all") + if err != nil { + output.WriteError("Error reading all flag: %v", err) + } + files, err := cmd.Flags().GetStringSlice("file") + if err != nil { + output.WriteError("Error reading file flag: %v", err) + } + + // Create a new experiment runner and clean up + ctx := cmd.Context() + er := experiments.NewRunner(ctx, namespace, allNamespaces, files) + er.Cleanup() + }, +} + +func init() { + rootCmd.AddCommand(cleanCmd) + + // Define the path of the experiment file to run + cleanCmd.Flags().StringSliceP("file", "f", []string{}, "Experiment file(s) to run") + cleanCmd.MarkFlagRequired("file") + + // Define the namespace(s) to run the experiment in + cleanCmd.Flags().StringP("namespace", "n", "", "Namespace to run experiment in") + cleanCmd.Flags().BoolP("all", "a", false, "Run experiment in all namespaces") + cleanCmd.MarkFlagsMutuallyExclusive("namespace", "all") +} diff --git a/cmd/root.go b/cmd/cli/cmd/root.go similarity index 80% rename from cmd/root.go rename to cmd/cli/cmd/root.go index f3da69b..300cdb9 100644 --- a/cmd/root.go +++ b/cmd/cli/cmd/root.go @@ -4,14 +4,13 @@ Copyright 2023 Operant AI package cmd import ( - "os" - + "github.com/operantai/secops-chaos/internal/output" "github.com/spf13/cobra" ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "experiments-runtime-tool", + Use: "secops-chaos", Short: "tbd", Long: "tbd", } @@ -21,6 +20,6 @@ var rootCmd = &cobra.Command{ func Execute() { err := rootCmd.Execute() if err != nil { - os.Exit(1) + output.WriteError(err.Error()) } } diff --git a/cmd/cli/cmd/run.go b/cmd/cli/cmd/run.go new file mode 100644 index 0000000..b70c164 --- /dev/null +++ b/cmd/cli/cmd/run.go @@ -0,0 +1,50 @@ +/* +Copyright 2023 Operant AI +*/ +package cmd + +import ( + "github.com/operantai/secops-chaos/internal/experiments" + "github.com/operantai/secops-chaos/internal/output" + "github.com/spf13/cobra" +) + +// runCmd represents the run command +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run an experiment", + Long: "Run an experiment", + Run: func(cmd *cobra.Command, args []string) { + // Read the flags + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + output.WriteError("Error reading namespace flag: %v", err) + } + allNamespaces, err := cmd.Flags().GetBool("all") + if err != nil { + output.WriteError("Error reading all flag: %v", err) + } + files, err := cmd.Flags().GetStringSlice("file") + if err != nil { + output.WriteError("Error reading file flag: %v", err) + } + + // Run the experiment + ctx := cmd.Context() + er := experiments.NewRunner(ctx, namespace, allNamespaces, files) + er.Run() + }, +} + +func init() { + rootCmd.AddCommand(runCmd) + + // Define the path of the experiment file to run + runCmd.Flags().StringSliceP("file", "f", []string{}, "Experiment file(s) to run") + runCmd.MarkFlagRequired("file") + + // Define the namespace(s) to run the experiment in + runCmd.Flags().StringP("namespace", "n", "", "Namespace to run experiment in") + runCmd.Flags().BoolP("all", "a", false, "Run experiment in all namespaces") + runCmd.MarkFlagsMutuallyExclusive("namespace", "all") +} diff --git a/cmd/cli/cmd/verify.go b/cmd/cli/cmd/verify.go new file mode 100644 index 0000000..2b3587a --- /dev/null +++ b/cmd/cli/cmd/verify.go @@ -0,0 +1,57 @@ +/* +Copyright 2023 Operant AI +*/ +package cmd + +import ( + "github.com/operantai/secops-chaos/internal/experiments" + "github.com/operantai/secops-chaos/internal/output" + "github.com/spf13/cobra" +) + +// verifyCmd represents the verify command +var verifyCmd = &cobra.Command{ + Use: "verify", + Short: "Verify the outcome of an experiment", + Long: "Verify the outcome of an experiment", + Run: func(cmd *cobra.Command, args []string) { + // Read the flags + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + output.WriteError("Error reading namespace flag: %v", err) + } + allNamespaces, err := cmd.Flags().GetBool("all") + if err != nil { + output.WriteError("Error reading all flag: %v", err) + } + files, err := cmd.Flags().GetStringSlice("file") + if err != nil { + output.WriteError("Error reading file flag: %v", err) + } + outputJSON, err := cmd.Flags().GetBool("json") + if err != nil { + output.WriteError("Error reading json output flag: %v", err) + } + + // Run the verifiers + ctx := cmd.Context() + er := experiments.NewRunner(ctx, namespace, allNamespaces, files) + er.RunVerifiers(outputJSON) + }, +} + +func init() { + rootCmd.AddCommand(verifyCmd) + + // Define the path of the experiment file to run + verifyCmd.Flags().StringSliceP("file", "f", []string{}, "Experiment file(s) to run") + verifyCmd.MarkFlagRequired("file") + + // Output the results in JSON format + verifyCmd.Flags().BoolP("json", "j", false, "Output results in JSON format") + + // Define the namespace(s) to run the experiment in + verifyCmd.Flags().StringP("namespace", "n", "", "Namespace to run experiment in") + verifyCmd.Flags().BoolP("all", "a", false, "Run experiment in all namespaces") + verifyCmd.MarkFlagsMutuallyExclusive("namespace", "all") +} diff --git a/cmd/version.go b/cmd/cli/cmd/version.go similarity index 100% rename from cmd/version.go rename to cmd/cli/cmd/version.go diff --git a/main.go b/cmd/cli/main.go similarity index 56% rename from main.go rename to cmd/cli/main.go index 436e825..33b4402 100644 --- a/main.go +++ b/cmd/cli/main.go @@ -3,7 +3,7 @@ Copyright 2023 Operant AI */ package main -import "github.com/operantai/experiments-runtime-tool/cmd" +import "github.com/operantai/secops-chaos/cmd/cli/cmd" func main() { cmd.Execute() diff --git a/cmd/run.go b/cmd/run.go deleted file mode 100644 index 9322dc6..0000000 --- a/cmd/run.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2023 Operant AI -*/ -package cmd - -import ( - "github.com/operantai/experiments-runtime-tool/internal/experiments" - "github.com/spf13/cobra" -) - -// runCmd represents the run command -var runCmd = &cobra.Command{ - Use: "run", - Short: "Run an experiment", - Long: "Run an experiment", - Run: func(cmd *cobra.Command, args []string) { - ctx := cmd.Context() - er := experiments.NewRunner(ctx, []string{""}) - er.Run() - }, -} - -func init() { - rootCmd.AddCommand(runCmd) - - // Define the path of the experiment file to run - runCmd.Flags().StringP("file", "f", "", "Experiment file to run") - runCmd.MarkFlagRequired("file") - - // Define the namespace(s) to run the experiment in - runCmd.Flags().StringP("namespace", "n", "", "Namespace to run experiment in") - runCmd.Flags().StringP("all", "a", "", "Run experiment in all namespaces") - runCmd.MarkFlagsMutuallyExclusive("namespace", "all") -} diff --git a/cmd/secops-chaos/main.go b/cmd/secops-chaos/main.go new file mode 100644 index 0000000..33b4402 --- /dev/null +++ b/cmd/secops-chaos/main.go @@ -0,0 +1,10 @@ +/* +Copyright 2023 Operant AI +*/ +package main + +import "github.com/operantai/secops-chaos/cmd/cli/cmd" + +func main() { + cmd.Execute() +} diff --git a/cmd/verify.go b/cmd/verify.go deleted file mode 100644 index ae94916..0000000 --- a/cmd/verify.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2023 Operant AI -*/ -package cmd - -import ( - "github.com/operantai/experiments-runtime-tool/internal/verifiers" - "github.com/spf13/cobra" -) - -// verifyCmd represents the verify command -var verifyCmd = &cobra.Command{ - Use: "verify", - Short: "Verify the outcome of an experiment", - Long: "Verify the outcome of an experiment", - Run: func(cmd *cobra.Command, args []string) { - ctx := cmd.Context() - vr := verifiers.NewRunner(ctx, []string{""}) - vr.Run() - }, -} - -func init() { - rootCmd.AddCommand(verifyCmd) - - // Define the path of the experiment file to run - verifyCmd.Flags().StringP("file", "f", "", "Experiment file to run") - verifyCmd.MarkFlagRequired("file") - - // Define the namespace(s) to run the experiment in - verifyCmd.Flags().StringP("namespace", "n", "", "Namespace to run experiment in") - verifyCmd.Flags().StringP("all", "a", "", "Run experiment in all namespaces") - verifyCmd.MarkFlagsMutuallyExclusive("namespace", "all") -} diff --git a/experiments/run_privileged_container.yaml b/experiments/run_privileged_container.yaml new file mode 100644 index 0000000..8597a67 --- /dev/null +++ b/experiments/run_privileged_container.yaml @@ -0,0 +1,9 @@ +experiments: + - name: run-privileged-container + type: privileged_container + namespace: default + parameters: + privileged: true + host_pid: true + host_network: true + run_as_root: true diff --git a/go.mod b/go.mod index dea101c..622d35c 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,22 @@ -module github.com/operantai/experiments-runtime-tool +module github.com/operantai/secops-chaos go 1.21.3 require ( + github.com/charmbracelet/lipgloss v0.9.1 + github.com/mitchellh/mapstructure v1.5.0 github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.8.4 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.28.3 + k8s.io/apimachinery v0.28.3 k8s.io/client-go v0.28.3 + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect @@ -24,27 +32,31 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.28.3 // indirect - k8s.io/apimachinery v0.28.3 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index b07d52a..128da90 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,13 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -51,21 +56,38 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -80,8 +102,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -96,14 +118,15 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= @@ -119,8 +142,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -129,8 +152,8 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/categories/categories.go b/internal/categories/categories.go new file mode 100644 index 0000000..fff63ad --- /dev/null +++ b/internal/categories/categories.go @@ -0,0 +1,153 @@ +package categories + +// mitreCategories struct to hold MITRE categories +type mitreCategories struct { + InitialAccess InitialAccess + Execution Execution + Persistence Persistence + PrivilegeEscalation PrivilegeEscalation + DefenseEvasion DefenseEvasion + Credentials Credentials + Discovery Discovery + LateralMovement LateralMovement +} + +type mitreEntry struct { + CategoryID string + Category string + Name string +} + +type InitialAccess struct { + UsingCloudCredentials mitreEntry + CompromisedImagesInRegistry mitreEntry + KubeConfigFile mitreEntry + ApplicationVulnerability mitreEntry + ExposedSensitiveInterfaces mitreEntry +} + +type Execution struct { + ExecIntoContainer mitreEntry + BashCmdInsideContainer mitreEntry + NewContainer mitreEntry + ApplicationExploit mitreEntry + RCE mitreEntry + SSHServerRunningInContainer mitreEntry + SidecarInjection mitreEntry +} + +type Persistence struct { + BackdoorContainer mitreEntry + WriteableHostPathMount mitreEntry + KubernetesCronJob mitreEntry + MaliciousAdmissionController mitreEntry +} + +type PrivilegeEscalation struct { + PrivilegedContainer mitreEntry + ClusterAdminBinding mitreEntry + HostPathMount mitreEntry + AccessCloudResources mitreEntry +} + +type DefenseEvasion struct { + ClearContainerLogs mitreEntry + DeleteK8sEvents mitreEntry + PodContainerNameSimilarity mitreEntry + ConnectFromProxyServer mitreEntry +} + +type Credentials struct { + ListK8sSecrets mitreEntry + MountServicePrincipal mitreEntry + AccessContainerServiceAccount mitreEntry + ApplicationCredentialsInConfigurationFiles mitreEntry + AccessManagedIdentityCredentials mitreEntry + MaliciousAdmissionController mitreEntry +} + +type Discovery struct { + AccessTheK8sApiServer mitreEntry + AccessKubeletAPI mitreEntry + NetworkMapping mitreEntry + AccessKubernetesDashboard mitreEntry + InstanceMetadataAPI mitreEntry +} + +type LateralMovement struct { + AccessCloudResources mitreEntry + ContainerServiceAccount mitreEntry + ClusterInternalNetworking mitreEntry + ApplicationsCredentialsInConfigurationFiles mitreEntry + WritableVolumeMountsOnTheHost mitreEntry + CoreDNSPoisoning mitreEntry + ARPPoisoningOrIPSpoofing mitreEntry +} + +// Exported instances of the categories +var ( + MITRE mitreCategories +) + +func init() { + MITRE = mitreCategories{ + InitialAccess{ + UsingCloudCredentials: mitreEntry{"TA0001", "Intial Access", "Using Cloud Credentials"}, + CompromisedImagesInRegistry: mitreEntry{"TA0001", "Intial Access", "Compromised Images in Registry"}, + KubeConfigFile: mitreEntry{"TA0001", "Intial Access", "Kube Config File"}, + ApplicationVulnerability: mitreEntry{"TA0001", "Intial Access", "Application Vulnerability"}, + ExposedSensitiveInterfaces: mitreEntry{"TA0001", "Intial Access", "Exposed Sensitive Interfaces"}, + }, + Execution{ + ExecIntoContainer: mitreEntry{"TA0002", "Execution", "Exec Into Container"}, + BashCmdInsideContainer: mitreEntry{"TA0002", "Execution", "Bash Cmd Inside Container"}, + NewContainer: mitreEntry{"TA0002", "Execution", "New Container"}, + ApplicationExploit: mitreEntry{"TA0002", "Execution", "Application Exploit"}, + RCE: mitreEntry{"TA0002", "Execution", "Application Exploit"}, + SSHServerRunningInContainer: mitreEntry{"TA0002", "Execution", "SSH Server Running In Container"}, + SidecarInjection: mitreEntry{"TA0002", "Execution", "Sidecar Injection"}, + }, + Persistence{ + BackdoorContainer: mitreEntry{"TA0003", "Persistence", "Backdoor Container"}, + WriteableHostPathMount: mitreEntry{"TA0003", "Persistence", "Writeable Host Path Mount"}, + KubernetesCronJob: mitreEntry{"TA0003", "Persistence", "Kubernetes Cron Job"}, + MaliciousAdmissionController: mitreEntry{"TA0003", "Persistence", "Malicious Admission Controller"}, + }, + PrivilegeEscalation{ + PrivilegedContainer: mitreEntry{"TA0004", "Privilege Escalation", "Privileged Container"}, + ClusterAdminBinding: mitreEntry{"TA0004", "Privilege Escalation", "Cluster Admin Binding"}, + HostPathMount: mitreEntry{"TA0004", "Privilege Escalation", "Host Path Mount"}, + AccessCloudResources: mitreEntry{"TA0004", "Privilege Escalation", "Access Cloud Resources"}, + }, + DefenseEvasion{ + ClearContainerLogs: mitreEntry{"TA0005", "Defense Evasion", "Clear Container Logs"}, + DeleteK8sEvents: mitreEntry{"TA0005", "Defense Evasion", "Delete K8s Events"}, + PodContainerNameSimilarity: mitreEntry{"TA0005", "Defense Evasion", "Pod Container Name Similarity"}, + ConnectFromProxyServer: mitreEntry{"TA0005", "Defense Evasion", "Connect From Proxy Server"}, + }, + Credentials{ + ListK8sSecrets: mitreEntry{"TA0006", "Credentials", "List K8s Secrets"}, + MountServicePrincipal: mitreEntry{"TA0006", "Credentials", "Mount Service Principal"}, + AccessContainerServiceAccount: mitreEntry{"TA0006", "Credentials", "Access Container Service Account"}, + ApplicationCredentialsInConfigurationFiles: mitreEntry{"TA0006", "Credentials", "Application Credentials In Configuration Files"}, + AccessManagedIdentityCredentials: mitreEntry{"TA0006", "Credentials", "Access Managed Identity Credentials"}, + MaliciousAdmissionController: mitreEntry{"TA0006", "Credentials", "Malicious Admission Controller"}, + }, + Discovery{ + AccessTheK8sApiServer: mitreEntry{"TA0007", "Discovery", "Access The K8s Api Server"}, + AccessKubeletAPI: mitreEntry{"TA0007", "Discovery", "Access Kubelet API"}, + NetworkMapping: mitreEntry{"TA0007", "Discovery", "Network Mapping"}, + AccessKubernetesDashboard: mitreEntry{"TA0007", "Discovery", "Access Kubernetes Dashboard"}, + InstanceMetadataAPI: mitreEntry{"TA0007", "Discovery", "Instance Metadata API"}, + }, + LateralMovement{ + AccessCloudResources: mitreEntry{"TA0008", "Lateral Movement", "Access Cloud Resources"}, + ContainerServiceAccount: mitreEntry{"TA0008", "Lateral Movement", "Container Service Account"}, + ClusterInternalNetworking: mitreEntry{"TA0008", "Lateral Movement", "Cluster Internal Networking"}, + ApplicationsCredentialsInConfigurationFiles: mitreEntry{"TA0008", "Lateral Movement", "Applications Credentials In Configuration Files"}, + WritableVolumeMountsOnTheHost: mitreEntry{"TA0008", "Lateral Movement", "Writable Volume Mounts On The Host"}, + CoreDNSPoisoning: mitreEntry{"TA0008", "Lateral Movement", "CoreDNS Poisoning"}, + ARPPoisoningOrIPSpoofing: mitreEntry{"TA0008", "Lateral Movement", "ARP Poisoning Or IP Spoofing"}, + }, + } +} diff --git a/internal/experiments/experiments.go b/internal/experiments/experiments.go index 38e63b3..209a460 100644 --- a/internal/experiments/experiments.go +++ b/internal/experiments/experiments.go @@ -5,9 +5,11 @@ package experiments import ( "context" + "encoding/json" "fmt" - "github.com/operantai/experiments-runtime-tool/internal/k8s" + "github.com/operantai/secops-chaos/internal/k8s" + "github.com/operantai/secops-chaos/internal/output" "k8s.io/client-go/kubernetes" ) @@ -18,65 +20,124 @@ var Experiments = []Experiment{ type Experiment interface { // Name returns the name of the experiment Name() string + // Category returns the MITRE/OWASP category of the experiment + Category() string // Run runs the experiment, returning an error if it fails - Run(ctx context.Context, client *kubernetes.Clientset) error + Run(ctx context.Context, client *kubernetes.Clientset, config *ExperimentConfig) error + // Verify verifies the experiment, returning an error if it fails + Verify(ctx context.Context, client *kubernetes.Clientset, config *ExperimentConfig) (*Outcome, error) // Cleanup cleans up the experiment, returning an error if it fails - Cleanup(ctx context.Context, client *kubernetes.Clientset) error + Cleanup(ctx context.Context, client *kubernetes.Clientset, config *ExperimentConfig) error } // Runner runs a set of experiments type Runner struct { - ctx context.Context - client *kubernetes.Clientset - experiments map[string]Experiment + ctx context.Context + client *kubernetes.Clientset + experiments map[string]Experiment + experimentsConfig map[string]*ExperimentConfig +} + +type JSONOutput struct { + K8sVersion string `json:"k8s_version"` + Results []*Outcome `json:"results"` +} + +type Outcome struct { + Experiment string `json:"experiment"` + Category string `json:"category"` + Success bool `json:"success"` } // NewRunner returns a new Runner -func NewRunner(ctx context.Context, experiments []string) *Runner { +func NewRunner(ctx context.Context, namespace string, allNamespaces bool, experiments []string) *Runner { // Create a new Kubernetes client client, err := k8s.NewClient() if err != nil { - panic(err) + output.WriteFatal("Failed to create Kubernetes client: %s", err) } - // Check if experiment exists in Experiments slice - experimentsToRun := make(map[string]Experiment) + experimentMap := make(map[string]Experiment) + experimentConfigMap := make(map[string]*ExperimentConfig) + for _, e := range Experiments { - for _, providedExperiment := range experiments { - if e.Name() == providedExperiment { - experimentsToRun[e.Name()] = e - } - } + experimentMap[e.Name()] = e } - // Check if all experiments provided are valid - if len(experimentsToRun) != len(experiments) { - panic("One or more experiments provided are not valid") + for _, e := range experiments { + experimentsConfig, err := parseExperimentConfig(e) + if err != nil { + output.WriteFatal("Failed to parse experiment config: %s", err) + } + + for _, eConf := range experimentsConfig.Experiments { + if _, exists := experimentMap[eConf.Type]; exists { + experimentConfigMap[eConf.Type] = &eConf + } else { + output.WriteError("Experiment %s does not exist", eConf.Type) + } + } } return &Runner{ - ctx: ctx, - client: client, - experiments: experimentsToRun, + ctx: ctx, + client: client, + experiments: experimentMap, + experimentsConfig: experimentConfigMap, } } // Run runs all experiments in the Runner func (r *Runner) Run() { - for _, e := range r.experiments { - fmt.Printf("Running experiment %s\n", e.Name()) - if err := e.Run(r.ctx, r.client); err != nil { - fmt.Printf("Experiment %s failed: %s\n", e.Name(), err) + for _, e := range Experiments { + output.WriteInfo("Running experiment %s\n", e.Name()) + if err := e.Run(r.ctx, r.client, r.experimentsConfig[e.Name()]); err != nil { + output.WriteError("Experiment %s failed: %s", e.Name(), err) + } + } +} + +// RunVerifiers runs all verifiers in the Runner for the provided experiments +func (r *Runner) RunVerifiers(outputJSON bool) { + headers := []string{"Experiment", "Category", "Result"} + rows := [][]string{} + outcomes := []*Outcome{} + for _, e := range Experiments { + outcome, err := e.Verify(r.ctx, r.client, r.experimentsConfig[e.Name()]) + if err != nil { + output.WriteError("Verifier %s failed: %s", e.Name(), err) + } + if outputJSON { + outcomes = append(outcomes, outcome) + } else { + rows = append(rows, []string{outcome.Experiment, outcome.Category, fmt.Sprintf("%t", outcome.Success)}) + } + } + if outputJSON { + k8sVersion, err := k8s.GetK8sVersion(r.client) + if err != nil { + output.WriteError("Failed to get Kubernetes version: %s", err) + } + out := JSONOutput{ + K8sVersion: k8sVersion.String(), + Results: outcomes, + } + jsonOutput, err := json.MarshalIndent(out, "", " ") + if err != nil { + output.WriteError("Failed to marshal JSON: %s", err) } + fmt.Println(string(jsonOutput)) + return } + output.WriteTable(headers, rows) } // Cleanup cleans up all experiments in the Runner func (r *Runner) Cleanup() { for _, e := range r.experiments { - fmt.Printf("Cleaning up experiment %s\n", e.Name()) - if err := e.Cleanup(r.ctx, r.client); err != nil { - fmt.Printf("Experiment %s cleanup failed: %s\n", e.Name(), err) + output.WriteInfo("Cleaning up experiment %s\n", e.Name()) + if err := e.Cleanup(r.ctx, r.client, r.experimentsConfig[e.Name()]); err != nil { + output.WriteError("Experiment %s cleanup failed: %s", e.Name(), err) } } diff --git a/internal/experiments/parser.go b/internal/experiments/parser.go new file mode 100644 index 0000000..e1fb0cf --- /dev/null +++ b/internal/experiments/parser.go @@ -0,0 +1,73 @@ +package experiments + +import ( + "fmt" + "os" + + "github.com/mitchellh/mapstructure" + "gopkg.in/yaml.v3" +) + +type ExperimentsConfig struct { + Experiments []ExperimentConfig `yaml:"experiments"` +} + +type ExperimentConfig struct { + // Name of the experiment + Name string `yaml:"name"` + // Namespace to apply the experiment to + Namespace string `yaml:"namespace"` + // Type of the experiment + Type string `yaml:"type"` + // Parameters for the experiment + Parameters interface{} `yaml:"parameters"` +} + +// parseExperimentConfig parses a YAML file and returns a slice of ExperimentConfig +func parseExperimentConfig(file string) (*ExperimentsConfig, error) { + // Read the file and then unmarshal it into a slice of ExperimentConfig + contents, err := os.ReadFile(file) + if err != nil { + return nil, err + } + config, err := unmarshalYAML(contents) + if err != nil { + return nil, err + } + return config, nil +} + +func unmarshalYAML(contents []byte) (*ExperimentsConfig, error) { + var config ExperimentsConfig + err := yaml.Unmarshal(contents, &config) + if err != nil { + return nil, err + } + + for _, experiment := range config.Experiments { + if experiment.Parameters == nil { + return nil, fmt.Errorf("Experiment %s is missing parameters", experiment.Name) + } + } + + var specificExperiments []ExperimentConfig + + for _, experiment := range config.Experiments { + switch experiment.Type { + case "privileged_container": + var privilegedContainer PrivilegedContainer + err := mapstructure.Decode(experiment.Parameters, &privilegedContainer) + if err != nil { + return nil, err + } + experiment.Parameters = privilegedContainer + specificExperiments = append(specificExperiments, experiment) + default: + return nil, fmt.Errorf("Unsupported experiment type: %s", experiment.Type) + } + } + + config.Experiments = specificExperiments + + return &config, nil +} diff --git a/internal/experiments/parser_test.go b/internal/experiments/parser_test.go new file mode 100644 index 0000000..95cc3f3 --- /dev/null +++ b/internal/experiments/parser_test.go @@ -0,0 +1,67 @@ +package experiments + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalYAML(t *testing.T) { + tests := []struct { + name string + contents []byte + expectError bool + }{ + { + name: "Valid Experiment", + contents: []byte(` +experiments: +- name: "Experiment 1" + namespace: "my-namespace" + type: "privileged_container" + labels: + key1: "value1" + parameters: + hostPid: true +`), + expectError: false, + }, + { + name: "Invalid Experiment (missing Parameters)", + contents: []byte(` +experiments: +- name: "Experiment 2" + namespace: "my-namespace" + type: "privileged_container" + labels: + key1: "value1" +`), + expectError: true, + }, + { + name: "Unsupported Experiment Type", + contents: []byte(` +experiments: +- name: "Experiment 3" + namespace: "my-namespace" + type: "unsupported_type" + labels: + key1: "value1" + parameters: + paramA: "ValueA" +`), + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := unmarshalYAML(test.contents) + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/experiments/privileged_container.go b/internal/experiments/privileged_container.go index 543030b..787fd70 100644 --- a/internal/experiments/privileged_container.go +++ b/internal/experiments/privileged_container.go @@ -6,21 +6,145 @@ package experiments import ( "context" + "github.com/operantai/secops-chaos/internal/categories" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/utils/pointer" ) -type PrivilegedContainer struct{} +type PrivilegedContainer struct { + Privileged bool `yaml:"privileged"` + HostPid bool `yaml:"host_pid"` + HostNetwork bool `yaml:"host_network"` + RunAsRoot bool `yaml:"run_as_root"` +} func (p *PrivilegedContainer) Name() string { - return "PrivilegedContainer" + return "privileged_container" } -func (p *PrivilegedContainer) Run(ctx context.Context, client *kubernetes.Clientset) error { - return nil +func (p *PrivilegedContainer) Category() string { + return categories.MITRE.PrivilegeEscalation.PrivilegedContainer.Name +} + +func (p *PrivilegedContainer) Run(ctx context.Context, client *kubernetes.Clientset, config *ExperimentConfig) error { + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.Name, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: pointer.Int32(1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": config.Name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "experiment": config.Name, + "app": config.Name, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: config.Name, + Image: "alpine:latest", + ImagePullPolicy: corev1.PullAlways, + Command: []string{ + "sh", + "-c", + "while true; do :; done", + }, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 4000, + }, + }, + }, + }, + }, + }, + }, + } + params := config.Parameters.(PrivilegedContainer) + if params.HostPid { + deployment.Spec.Template.Spec.HostPID = true + } + if params.HostNetwork { + deployment.Spec.Template.Spec.HostNetwork = true + } + if params.RunAsRoot { + if deployment.Spec.Template.Spec.Containers[0].SecurityContext == nil { + deployment.Spec.Template.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{ + RunAsUser: pointer.Int64(0), + } + } else { + deployment.Spec.Template.Spec.Containers[0].SecurityContext.RunAsUser = pointer.Int64(0) + } + } + + if params.Privileged { + if deployment.Spec.Template.Spec.Containers[0].SecurityContext == nil { + deployment.Spec.Template.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + } + } else { + deployment.Spec.Template.Spec.Containers[0].SecurityContext.Privileged = pointer.Bool(true) + } + } + + _, err := client.AppsV1().Deployments(config.Namespace).Create(ctx, deployment, metav1.CreateOptions{}) + return err +} + +func (p *PrivilegedContainer) Verify(ctx context.Context, client *kubernetes.Clientset, config *ExperimentConfig) (*Outcome, error) { + deployment, err := client.AppsV1().Deployments(config.Namespace).Get(ctx, config.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + params := config.Parameters.(PrivilegedContainer) + outcome := &Outcome{ + Experiment: config.Name, + Category: p.Category(), + Success: false, + } + + if params.HostPid { + if deployment.Spec.Template.Spec.HostPID { + outcome.Success = true + return outcome, nil + } + } + + if params.HostNetwork { + if deployment.Spec.Template.Spec.HostNetwork { + outcome.Success = true + return outcome, nil + } + } + + if params.Privileged { + if deployment.Spec.Template.Spec.Containers[0].SecurityContext != nil { + if deployment.Spec.Template.Spec.Containers[0].SecurityContext.Privileged != nil { + if *deployment.Spec.Template.Spec.Containers[0].SecurityContext.Privileged { + outcome.Success = true + return outcome, nil + } + } + } + } + + return outcome, nil } -func (p *PrivilegedContainer) Cleanup(ctx context.Context, client *kubernetes.Clientset) error { - return nil +func (p *PrivilegedContainer) Cleanup(ctx context.Context, client *kubernetes.Clientset, config *ExperimentConfig) error { + return client.AppsV1().Deployments(config.Namespace).Delete(ctx, config.Name, metav1.DeleteOptions{}) } var _ Experiment = (*PrivilegedContainer)(nil) diff --git a/internal/k8s/version.go b/internal/k8s/version.go new file mode 100644 index 0000000..5da8e6d --- /dev/null +++ b/internal/k8s/version.go @@ -0,0 +1,15 @@ +package k8s + +import ( + k8sVersion "k8s.io/apimachinery/pkg/version" + + "k8s.io/client-go/kubernetes" +) + +func GetK8sVersion(client *kubernetes.Clientset) (*k8sVersion.Info, error) { + version, err := client.Discovery().ServerVersion() + if err != nil { + return nil, err + } + return version, nil +} diff --git a/internal/output/output.go b/internal/output/output.go new file mode 100644 index 0000000..a9fa779 --- /dev/null +++ b/internal/output/output.go @@ -0,0 +1,54 @@ +package output + +import ( + "fmt" + "os" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" +) + +const ( + InfoColor = lipgloss.Color("86") + SuccessColor = lipgloss.Color("78") + WarningColor = lipgloss.Color("208") + ErrorColor = lipgloss.Color("196") + FatalColor = lipgloss.Color("196") + TableBorderColor = lipgloss.Color("205") +) + +func WriteInfo(msg string, args ...interface{}) { + style := lipgloss.NewStyle().Foreground(InfoColor) + fmt.Printf("%s %s", style.Render("INFO"), fmt.Sprintf(msg, args...)) +} + +func WriteSuccess(msg string, args ...interface{}) { + style := lipgloss.NewStyle().Foreground(SuccessColor) + fmt.Printf("%s %s", style.Render("SUCCESS"), fmt.Sprintf(msg, args...)) +} + +func WriteWarning(msg string, args ...interface{}) { + style := lipgloss.NewStyle().Foreground(WarningColor) + fmt.Printf("%s %s", style.Render("WARN"), fmt.Sprintf(msg, args...)) +} + +func WriteError(msg string, args ...interface{}) { + style := lipgloss.NewStyle().Foreground(ErrorColor) + fmt.Printf("%s %s", style.Render("ERROR"), fmt.Sprintf(msg, args...)) +} + +func WriteFatal(msg string, args ...interface{}) { + style := lipgloss.NewStyle().Foreground(ErrorColor) + fmt.Printf("%s %s", style.Render("FATAL"), fmt.Sprintf(msg, args...)) + os.Exit(1) +} + +func WriteTable(headers []string, rows [][]string) { + t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(TableBorderColor)). + Headers(headers...). + Rows(rows...) + + fmt.Println(t) +} diff --git a/internal/verifiers/privileged_container.go b/internal/verifiers/privileged_container.go deleted file mode 100644 index fe5cd1f..0000000 --- a/internal/verifiers/privileged_container.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright 2023 Operant AI -*/ -package verifiers - -import ( - "context" - - "k8s.io/client-go/kubernetes" -) - -type PrivilegedContainer struct{} - -func (p *PrivilegedContainer) Name() string { - return "PrivilegedContainer" -} - -func (p *PrivilegedContainer) Verify(ctx context.Context, client *kubernetes.Clientset) error { - return nil -} - -var _ Verifier = (*PrivilegedContainer)(nil) diff --git a/internal/verifiers/verifiers.go b/internal/verifiers/verifiers.go deleted file mode 100644 index 2cfcade..0000000 --- a/internal/verifiers/verifiers.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -Copyright 2023 Operant AI -*/ -package verifiers - -import ( - "context" - "fmt" - - "github.com/operantai/experiments-runtime-tool/internal/k8s" - "k8s.io/client-go/kubernetes" -) - -var Verifiers = []Verifier{ - &PrivilegedContainer{}, -} - -type Verifier interface { - // Name returns the name of the verifier - Name() string - // Verify verifies the experiment - Verify(ctx context.Context, client *kubernetes.Clientset) error -} - -type Runner struct { - ctx context.Context - client *kubernetes.Clientset - verifiers []Verifier -} - -func NewRunner(ctx context.Context, verifiers []string) *Runner { - client, err := k8s.NewClient() - if err != nil { - panic(err) - } - - // Check if verifiers exists in Verifier slice - verifiersToRun := make(map[string]Verifier) - for _, v := range Verifiers { - for _, providedVerifier := range verifiers { - if v.Name() == providedVerifier { - verifiersToRun[v.Name()] = v - } - } - } - - // Check if all verifiers provided exist - if len(verifiersToRun) != len(verifiers) { - panic("One or more verifiers provided do not exist") - } - - return &Runner{ - ctx: ctx, - client: client, - verifiers: Verifiers, - } -} - -func (r *Runner) Run() { - for _, v := range r.verifiers { - fmt.Printf("Running verifier: %s\n", v.Name()) - if err := v.Verify(r.ctx, r.client); err != nil { - fmt.Printf("Failed to verify experiment %s: %s\n", v.Name(), err) - } - } -}