From 94493f2071c3ff9d925bd3d047f04ee0379b42a5 Mon Sep 17 00:00:00 2001 From: GLVS Kiriti Date: Thu, 25 Jul 2024 02:53:11 +0530 Subject: [PATCH] Implement ExecuteStep method for the container runner Updated the file structure in declarative package Added a helper function createTarReader which helps in copying files into the container Copied event-generator executable into the container Refactored setup and cleanup methods of the container runner Signed-off-by: GLVS Kiriti --- cmd/declarative.go | 18 ++-- events/exampleyamlfile.yml | 3 +- pkg/declarative/container.go | 191 +++++++++++++++++++++++++++++++++++ pkg/declarative/helpers.go | 39 +++++++ pkg/declarative/host.go | 56 ++++++++++ pkg/declarative/interface.go | 24 +++++ pkg/declarative/runner.go | 152 ---------------------------- 7 files changed, 321 insertions(+), 162 deletions(-) create mode 100644 pkg/declarative/container.go create mode 100644 pkg/declarative/host.go create mode 100644 pkg/declarative/interface.go delete mode 100644 pkg/declarative/runner.go diff --git a/cmd/declarative.go b/cmd/declarative.go index ca946b64..16adfb8c 100644 --- a/cmd/declarative.go +++ b/cmd/declarative.go @@ -15,6 +15,7 @@ limitations under the License. package cmd import ( + "context" "fmt" "github.com/falcosecurity/event-generator/pkg/declarative" @@ -67,33 +68,34 @@ func NewDeclarative() *cobra.Command { // runTestSteps executes the steps, before and after scripts defined in the test. func runTestSteps(test declarative.Test) error { var runner declarative.Runner + var ctx context.Context // Assign a runner based on test.Runner value switch test.Runner { case "HostRunner": runner = &declarative.Hostrunner{} + ctx = context.TODO() case "ContainerRunner": // spawn an alpine container - runner = &declarative.Containerrunner{Image: "alpine"} + runner = &declarative.Containerrunner{Image: "golang"} + ctx = context.Background() default: return fmt.Errorf("unsupported runner: %v", test.Runner) } // Execute the "Before" script. - if err := runner.Setup(test.Before); err != nil { + if err := runner.Setup(ctx, test.Before); err != nil { return err } // Execute each step in the test. - for _, step := range test.Steps { - err := runner.ExecuteStep(step) - if err != nil { - return fmt.Errorf("error executing steps for the rule %v : %v", test.Rule, err) - } + err := runner.ExecuteStep(ctx, test) + if err != nil { + return fmt.Errorf("error executing steps for the rule %v : %v", test.Rule, err) } // Execute the "After" script. - if err := runner.Cleanup(test.After); err != nil { + if err := runner.Cleanup(ctx, test.After); err != nil { return err } return nil diff --git a/events/exampleyamlfile.yml b/events/exampleyamlfile.yml index 4d60a21f..c483006d 100644 --- a/events/exampleyamlfile.yml +++ b/events/exampleyamlfile.yml @@ -21,7 +21,6 @@ tests: - rule: LaunchIngressRemoteFileCopyToolsInsideContainer runner: ContainerRunner - before: "wget" + before: "wget example.com" steps: after: "" - diff --git a/pkg/declarative/container.go b/pkg/declarative/container.go new file mode 100644 index 00000000..f75d8897 --- /dev/null +++ b/pkg/declarative/container.go @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2024 The Falco 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 declarative + +import ( + "context" + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "gopkg.in/yaml.v2" +) + +type Containerrunner struct { + ContainerId string + Image string +} + +func (r *Containerrunner) Setup(ctx context.Context, beforeScript string) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("error creating Docker Client: %v", err) + } + + // Pull the image + pullRes, err := cli.ImagePull(ctx, r.Image, image.PullOptions{}) + if err != nil { + return fmt.Errorf("error pulling Docker Image: %v", err) + } + + // Read the response of body inorder to download the image + defer pullRes.Close() + _, err = io.Copy(io.Discard, pullRes) + if err != nil { + log.Fatal(err) + } + + // Create the container with name "alpine-container-by-event-generator" + resp, err := cli.ContainerCreate(ctx, + &container.Config{ + Image: r.Image, + Cmd: []string{"sleep", "1h"}, + }, nil, nil, nil, "alpine-container-by-event-generator") + + if err != nil { + return fmt.Errorf("error creating Docker Container: %v", err) + } + + // Store created container id + r.ContainerId = resp.ID + + // Path of the event-generator executable + path_eg, err := os.Executable() + if err != nil { + return fmt.Errorf("error extracting path of event-generator executable") + } + + tarReader, err := CreateTarReader(path_eg) + if err != nil { + return fmt.Errorf("error creating tar reader: %v", err) + } + + err = cli.CopyToContainer(ctx, r.ContainerId, "/", tarReader, container.CopyToContainerOptions{}) + if err != nil { + return fmt.Errorf("error copying file to container: %v", err) + } + + // Start the container + err = cli.ContainerStart(ctx, r.ContainerId, container.StartOptions{}) + if err != nil { + return fmt.Errorf("error starting Docker container: %v", err) + } + return nil +} + +func (r *Containerrunner) ExecuteStep(ctx context.Context, test Test) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("error creating Docker Client: %v", err) + } + + // Create a yaml structure for given step + test.Runner = "HostRunner" // Change container runner to host runner + testFile := Tests{ + Tests: []Test{test}, + } + + // Marshall struct to yaml data + yamldata, err := yaml.Marshal(testFile) + if err != nil { + return fmt.Errorf("error marshalling syscallstep input to yaml: %v", err) + } + + // Write the yaml data to temporary file + tempFile, err := os.CreateTemp("", "syscall-*.yaml") + if err != nil { + return fmt.Errorf("error creating temporary file: %v", err) + } + defer os.Remove(tempFile.Name()) + + if _, err := tempFile.Write(yamldata); err != nil { + return fmt.Errorf("error writing to temporary file: %v", err) + } + if err := tempFile.Close(); err != nil { + return fmt.Errorf("error closing temporary file: %w", err) + } + + tarReader, err := CreateTarReader(tempFile.Name()) + if err != nil { + return fmt.Errorf("error creating tar reader: %v", err) + } + + err = cli.CopyToContainer(ctx, r.ContainerId, "/", tarReader, container.CopyToContainerOptions{}) + if err != nil { + return fmt.Errorf("error copying yaml file to container: %v", err) + } + + // Prepare the command to run the event-generator inside container + yamlFileName := filepath.Base(tempFile.Name()) + cmd := []string{"/event-generator", "run", "declarative", "/" + yamlFileName} + + execConfig := container.ExecOptions{ + Cmd: cmd, + AttachStdout: true, + AttachStderr: true, + } + + execID, err := cli.ContainerExecCreate(ctx, r.ContainerId, execConfig) + if err != nil { + return fmt.Errorf("error creating Docker exec: %v", err) + } + + err = cli.ContainerExecStart(ctx, execID.ID, container.ExecStartOptions{}) + if err != nil { + return fmt.Errorf("error starting Docker exec: %v", err) + } + + // Use this channel to wait for the exec instance to complete + done := make(chan error, 1) + go func() { + for { + inspectResp, err := cli.ContainerExecInspect(ctx, execID.ID) + if err != nil { + done <- fmt.Errorf("error inspecting Docker exec: %v", err) + return + } + + if !inspectResp.Running { + if inspectResp.ExitCode != 0 { + done <- fmt.Errorf("ExecuteStep failed with exit code %v", inspectResp.ExitCode) + } else { + done <- nil + } + return + } + } + }() + + err = <-done + return err +} + +func (r *Containerrunner) Cleanup(ctx context.Context, afterScript string) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("error creating Docker Client: %v", err) + } + + err = cli.ContainerRemove(ctx, r.ContainerId, container.RemoveOptions{Force: true}) + if err != nil { + return fmt.Errorf("error removing Docker container: %v", err) + } + return nil +} diff --git a/pkg/declarative/helpers.go b/pkg/declarative/helpers.go index 1c824be7..50ceb4e7 100644 --- a/pkg/declarative/helpers.go +++ b/pkg/declarative/helpers.go @@ -15,11 +15,50 @@ limitations under the License. package declarative import ( + "archive/tar" + "bytes" "fmt" + "io" + "os" "golang.org/x/sys/unix" ) +// It creates a tar reader for the file in given path. +func CreateTarReader(filePath string) (io.Reader, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("error opening file: %v", err) + } + + fileInfo, err := file.Stat() + if err != nil { + file.Close() + return nil, fmt.Errorf("error getting file info: %v", err) + } + + // Create a tar archive in memory + tarBuffer := new(bytes.Buffer) + tw := tar.NewWriter(tarBuffer) + defer tw.Close() + + header := &tar.Header{ + Name: fileInfo.Name(), + Mode: 0755, + Size: fileInfo.Size(), + } + + if err := tw.WriteHeader(header); err != nil { + return nil, fmt.Errorf("error writing tar header: %v", err) + } + + if _, err := io.Copy(tw, file); err != nil { + return nil, fmt.Errorf("error copying file to tar writer: %v", err) + } + + return tarBuffer, nil +} + func WriteSyscall(filepath string, content string) error { // Open the file using unix.Open syscall fd, err := unix.Open(filepath, unix.O_WRONLY|unix.O_CREAT, 0644) diff --git a/pkg/declarative/host.go b/pkg/declarative/host.go new file mode 100644 index 00000000..32147212 --- /dev/null +++ b/pkg/declarative/host.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2024 The Falco 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 declarative + +import ( + "context" + "fmt" + "os/exec" +) + +type Hostrunner struct{} + +func (r *Hostrunner) Setup(ctx context.Context, beforeScript string) error { + if beforeScript != "" { + if err := exec.Command("sh", "-c", beforeScript).Run(); err != nil { + return fmt.Errorf("error executing before script: %v", err) + } + } + return nil +} + +func (r *Hostrunner) ExecuteStep(ctx context.Context, test Test) error { + steps := test.Steps + for _, step := range steps { + switch step.Syscall { + case "write": + if err := WriteSyscall(step.Args["filepath"], step.Args["content"]); err != nil { + return fmt.Errorf("write syscall failed with error: %v", err) + } + default: + return fmt.Errorf("unsupported syscall: %s", step.Syscall) + } + } + return nil +} + +func (r *Hostrunner) Cleanup(ctx context.Context, afterScript string) error { + if afterScript != "" { + if err := exec.Command("sh", "-c", afterScript).Run(); err != nil { + return fmt.Errorf("error executing after script: %v", err) + } + } + return nil +} diff --git a/pkg/declarative/interface.go b/pkg/declarative/interface.go new file mode 100644 index 00000000..4f588599 --- /dev/null +++ b/pkg/declarative/interface.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2024 The Falco 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 declarative + +import "context" + +// Common runner interface for runners like hostrunner, container-runner etc.. +type Runner interface { + Setup(ctx context.Context, beforeScript string) error + ExecuteStep(ctx context.Context, test Test) error + Cleanup(ctx context.Context, afterScript string) error +} diff --git a/pkg/declarative/runner.go b/pkg/declarative/runner.go deleted file mode 100644 index 56f430c2..00000000 --- a/pkg/declarative/runner.go +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* -Copyright (C) 2024 The Falco 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 declarative - -import ( - "context" - "fmt" - "os/exec" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" -) - -// Common runner interface for runners like hostrunner, container-runner etc.. -type Runner interface { - Setup(beforeScript string) error - ExecuteStep(step SyscallStep) error - Cleanup(afterScript string) error -} - -type Hostrunner struct{} - -func (r *Hostrunner) Setup(beforeScript string) error { - if beforeScript != "" { - if err := exec.Command("sh", "-c", beforeScript).Run(); err != nil { - return fmt.Errorf("error executing before script: %v", err) - } - } - return nil -} - -func (r *Hostrunner) ExecuteStep(step SyscallStep) error { - switch step.Syscall { - case "write": - if err := WriteSyscall(step.Args["filepath"], step.Args["content"]); err != nil { - return fmt.Errorf("write syscall failed with error: %v", err) - } - default: - return fmt.Errorf("unsupported syscall: %s", step.Syscall) - } - return nil -} - -func (r *Hostrunner) Cleanup(afterScript string) error { - if afterScript != "" { - if err := exec.Command("sh", "-c", afterScript).Run(); err != nil { - return fmt.Errorf("error executing after script: %v", err) - } - } - return nil -} - -type Containerrunner struct { - ContainerId string - Image string -} - -func (r *Containerrunner) Setup(beforeScript string) error { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return fmt.Errorf("error creating Docker Client: %v", err) - } - - // Pull the image - _, err = cli.ImagePull(context.Background(), r.Image, image.PullOptions{}) - if err != nil { - return fmt.Errorf("error pulling Docker Image: %v", err) - } - - // Create the container with name "alpine-container-by-event-generator" - resp, err := cli.ContainerCreate(context.Background(), - &container.Config{ - Image: r.Image, - Cmd: []string{"sh", "-c", beforeScript}, - }, nil, nil, nil, "alpine-container-by-event-generator") - - if err != nil { - return fmt.Errorf("error creating Docker Container: %v", err) - } - - // Store created container id - r.ContainerId = resp.ID - - // Start the container - err = cli.ContainerStart(context.Background(), r.ContainerId, container.StartOptions{}) - if err != nil { - return fmt.Errorf("error starting Docker container: %v", err) - } - - // Wait for the container to finish executing the beforescript - // ContainerWait returns 2 channels - // statusCh - receives the container exit status once it stops running - // errCh - receives any error occurs while waiting for the container to stop - statusCh, errCh := cli.ContainerWait(context.Background(), r.ContainerId, container.WaitConditionNotRunning) - select { - case err := <-errCh: - if err != nil { - return fmt.Errorf("error waiting for the docker container: %v", err) - } - case <-statusCh: - } - - return nil -} - -func (r *Containerrunner) ExecuteStep(step SyscallStep) error { - return nil -} - -func (r *Containerrunner) Cleanup(afterScript string) error { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return fmt.Errorf("error creating Docker Client: %v", err) - } - - // If there is any afterScript execute it before removing the container - if afterScript != "" { - cmd := []string{"sh", "-c", afterScript} - execConfig := container.ExecOptions{ - Cmd: cmd, - } - - execID, err := cli.ContainerExecCreate(context.Background(), r.ContainerId, execConfig) - if err != nil { - return fmt.Errorf("error creating Docker exec: %v", err) - } - - err = cli.ContainerExecStart(context.Background(), execID.ID, container.ExecStartOptions{}) - if err != nil { - return fmt.Errorf("error starting Docker exec: %v", err) - } - } - - err = cli.ContainerRemove(context.Background(), r.ContainerId, container.RemoveOptions{Force: true}) - if err != nil { - return fmt.Errorf("error removing Docker container: %v", err) - } - return nil -}