diff --git a/internal/controller/controller.go b/internal/controller/controller.go index c7b149da..87141251 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -14,6 +14,7 @@ import ( "soarca/internal/capability/ssh" "soarca/internal/decomposer" "soarca/internal/executors/action" + "soarca/internal/executors/playbook_action" "soarca/internal/fin/protocol" "soarca/internal/guid" "soarca/logger" @@ -67,8 +68,9 @@ func (controller *Controller) NewDecomposer() decomposer.IDecomposer { } actionExecutor := action.New(capabilities) + playbookActionExecutor := playbook_action.New(controller, controller) guid := new(guid.Guid) - decompose := decomposer.New(actionExecutor, guid) + decompose := decomposer.New(actionExecutor, playbookActionExecutor, guid) return decompose } diff --git a/internal/decomposer/decomposer.go b/internal/decomposer/decomposer.go index 23663616..c4f9160c 100644 --- a/internal/decomposer/decomposer.go +++ b/internal/decomposer/decomposer.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "soarca/internal/executors" "soarca/internal/executors/action" "soarca/internal/guid" "soarca/logger" @@ -35,22 +36,21 @@ func init() { log = logger.Logger(component, logger.Info, "", logger.Json) } -func New(actionExecutor action.IExecuter, guid guid.IGuid) *Decomposer { - instance := Decomposer{} - if instance.actionExecutor == nil { - instance.actionExecutor = actionExecutor - } - if instance.guid == nil { - instance.guid = guid - } - return &instance +func New(actionExecutor action.IExecuter, + playbookActionExecutor executors.IPlaybookExecuter, + guid guid.IGuid) *Decomposer { + + return &Decomposer{actionExecutor: actionExecutor, + playbookActionExecutor: playbookActionExecutor, + guid: guid} } type Decomposer struct { - playbook cacao.Playbook - details ExecutionDetails - actionExecutor action.IExecuter - guid guid.IGuid + playbook cacao.Playbook + details ExecutionDetails + actionExecutor action.IExecuter + playbookActionExecutor executors.IPlaybookExecuter + guid guid.IGuid } // Execute a Playbook @@ -131,6 +131,12 @@ func (decomposer *Decomposer) ExecuteStep(step cacao.Step, scopeVariables cacao. variables.Merge(scopeVariables) variables.Merge(step.StepVariables) + metadata := execution.Metadata{ + ExecutionId: decomposer.details.ExecutionId, + PlaybookId: decomposer.details.PlaybookId, + StepId: step.ID, + } + switch step.Type { case cacao.StepTypeAction: actionMetadata := action.PlaybookStepMetadata{ @@ -140,12 +146,9 @@ func (decomposer *Decomposer) ExecuteStep(step cacao.Step, scopeVariables cacao. Agent: decomposer.playbook.AgentDefinitions[step.Agent], Variables: variables, } - metadata := execution.Metadata{ - ExecutionId: decomposer.details.ExecutionId, - PlaybookId: decomposer.details.PlaybookId, - StepId: step.ID, - } return decomposer.actionExecutor.Execute(metadata, actionMetadata) + case cacao.StepTypePlaybookAction: + return decomposer.playbookActionExecutor.Execute(metadata, step, variables) default: // NOTE: This currently silently handles unknown step types. Should we return an error instead? return cacao.NewVariables(), nil diff --git a/internal/executors/executors.go b/internal/executors/executors.go new file mode 100644 index 00000000..eb978c04 --- /dev/null +++ b/internal/executors/executors.go @@ -0,0 +1,12 @@ +package executors + +import ( + "soarca/models/cacao" + "soarca/models/execution" +) + +type IPlaybookExecuter interface { + Execute(execution.Metadata, + cacao.Step, + cacao.Variables) (cacao.Variables, error) +} diff --git a/internal/executors/playbook_action/playbook_action.go b/internal/executors/playbook_action/playbook_action.go new file mode 100644 index 00000000..ba19f320 --- /dev/null +++ b/internal/executors/playbook_action/playbook_action.go @@ -0,0 +1,61 @@ +package playbook_action + +import ( + "errors" + "fmt" + "reflect" + "soarca/internal/controller/database" + "soarca/internal/controller/decomposer_controller" + "soarca/logger" + "soarca/models/cacao" + "soarca/models/execution" +) + +type PlaybookAction struct { + decomposerController decomposer_controller.IController + databaseController database.IController +} + +var component = reflect.TypeOf(PlaybookAction{}).PkgPath() +var log *logger.Log + +func init() { + log = logger.Logger(component, logger.Info, "", logger.Json) +} + +func New(controller decomposer_controller.IController, + database database.IController) *PlaybookAction { + return &PlaybookAction{decomposerController: controller, databaseController: database} +} + +func (playbookAction *PlaybookAction) Execute(metadata execution.Metadata, + step cacao.Step, + variables cacao.Variables) (cacao.Variables, error) { + log.Trace(metadata.ExecutionId) + + if step.Type != cacao.StepTypePlaybookAction { + err := errors.New(fmt.Sprint("step type is not of type ", cacao.StepTypePlaybookAction)) + log.Error(err) + return cacao.NewVariables(), err + } + + playbookRepo := playbookAction.databaseController.GetDatabaseInstance() + decomposer := playbookAction.decomposerController.NewDecomposer() + + playbook, err := playbookRepo.Read(step.PlaybookID) + if err != nil { + log.Error("failed loading the playbook from the repository in playbook action") + return cacao.NewVariables(), err + } + + playbook.PlaybookVariables.Merge(variables) + + details, err := decomposer.Execute(playbook) + if err != nil { + err = errors.New(fmt.Sprint("execution of playbook failed with error: ", err)) + log.Error(err) + return cacao.NewVariables(), err + } + return details.Variables, nil + +} diff --git a/models/cacao/cacao.go b/models/cacao/cacao.go index a66de9b9..5e4a43c1 100644 --- a/models/cacao/cacao.go +++ b/models/cacao/cacao.go @@ -11,7 +11,7 @@ const ( StepTypeEnd = "end" StepTypeStart = "start" StepTypeAction = "action" - StepTypePlaybook = "playbook-action" + StepTypePlaybookAction = "playbook-action" StepTypeParallel = "parallel" StepTypeIfCondition = "if-condition" StepTypeWhileCondition = "while-condition" diff --git a/test/unittest/decomposer/decomposer_test.go b/test/unittest/decomposer/decomposer_test.go index 405f0ea8..91c9b0a6 100644 --- a/test/unittest/decomposer/decomposer_test.go +++ b/test/unittest/decomposer/decomposer_test.go @@ -10,6 +10,7 @@ import ( "soarca/models/cacao" "soarca/models/execution" "soarca/test/unittest/mocks/mock_executor" + mock_playbook_action_executor "soarca/test/unittest/mocks/mock_executor/playbook_action" "soarca/test/unittest/mocks/mock_guid" "github.com/go-playground/assert/v2" @@ -18,6 +19,7 @@ import ( func TestExecutePlaybook(t *testing.T) { mock_action_executor := new(mock_executor.Mock_Action_Executor) + mock_playbook_action_executor := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) uuid_mock := new(mock_guid.Mock_Guid) expectedCommand := cacao.Command{ @@ -31,7 +33,9 @@ func TestExecutePlaybook(t *testing.T) { Value: "testing", } - decomposer := decomposer.New(mock_action_executor, uuid_mock) + decomposer := decomposer.New(mock_action_executor, + mock_playbook_action_executor, + uuid_mock) step1 := cacao.Step{ Type: "action", @@ -107,6 +111,7 @@ func TestExecutePlaybook(t *testing.T) { func TestExecutePlaybookMultiStep(t *testing.T) { mock_action_executor := new(mock_executor.Mock_Action_Executor) + mock_playbook_action_executor := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) uuid_mock := new(mock_guid.Mock_Guid) expectedCommand := cacao.Command{ @@ -131,7 +136,9 @@ func TestExecutePlaybookMultiStep(t *testing.T) { Value: "testing2", } - decomposer := decomposer.New(mock_action_executor, uuid_mock) + decomposer := decomposer.New(mock_action_executor, + mock_playbook_action_executor, + uuid_mock) step1 := cacao.Step{ Type: "action", @@ -245,6 +252,7 @@ Test with an Empty OnCompletion will result in not executing the step. */ func TestExecuteEmptyMultiStep(t *testing.T) { mock_action_executor2 := new(mock_executor.Mock_Action_Executor) + mock_playbook_action_executor2 := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) uuid_mock2 := new(mock_guid.Mock_Guid) expectedCommand := cacao.Command{ @@ -268,7 +276,9 @@ func TestExecuteEmptyMultiStep(t *testing.T) { Name: "soarca-ssh", } - decomposer2 := decomposer.New(mock_action_executor2, uuid_mock2) + decomposer2 := decomposer.New(mock_action_executor2, + mock_playbook_action_executor2, + uuid_mock2) step1 := cacao.Step{ Type: "ssh", @@ -307,6 +317,7 @@ Test with an not occuring on completion id will result in not executing the step */ func TestExecuteIllegalMultiStep(t *testing.T) { mock_action_executor2 := new(mock_executor.Mock_Action_Executor) + mock_playbook_action_executor2 := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) uuid_mock2 := new(mock_guid.Mock_Guid) expectedCommand := cacao.Command{ @@ -320,7 +331,9 @@ func TestExecuteIllegalMultiStep(t *testing.T) { Value: "testing", } - decomposer2 := decomposer.New(mock_action_executor2, uuid_mock2) + decomposer2 := decomposer.New(mock_action_executor2, + mock_playbook_action_executor2, + uuid_mock2) step1 := cacao.Step{ Type: "action", @@ -351,3 +364,62 @@ func TestExecuteIllegalMultiStep(t *testing.T) { assert.Equal(t, returnedId.ExecutionId, id) mock_action_executor2.AssertExpectations(t) } + +func TestExecutePlaybookAction(t *testing.T) { + mock_action_executor := new(mock_executor.Mock_Action_Executor) + mock_playbook_action_executor := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) + uuid_mock := new(mock_guid.Mock_Guid) + + expectedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing", + } + + decomposer := decomposer.New(mock_action_executor, + mock_playbook_action_executor, + uuid_mock) + + step1 := cacao.Step{ + Type: "playbook-action", + ID: "playbook-action--test", + Name: "ssh-tests", + StepVariables: cacao.NewVariables(expectedVariables), + PlaybookID: "playbook--1", + OnCompletion: "end--test", + } + + end := cacao.Step{ + Type: "end", + ID: "end--test", + Name: "end step", + } + + playbook := cacao.Playbook{ + ID: "test", + Type: "test", + Name: "playbook-test", + WorkflowStart: step1.ID, + Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, + } + + executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + metaStep1 := execution.Metadata{ExecutionId: executionId, PlaybookId: "test", StepId: step1.ID} + + uuid_mock.On("New").Return(executionId) + + mock_playbook_action_executor.On("Execute", + metaStep1, + step1, + cacao.NewVariables(expectedVariables)).Return(cacao.NewVariables(cacao.Variable{Name: "return", Value: "value"}), nil) + + details, err := decomposer.Execute(playbook) + uuid_mock.AssertExpectations(t) + fmt.Println(err) + assert.Equal(t, err, nil) + assert.Equal(t, details.ExecutionId, executionId) + mock_action_executor.AssertExpectations(t) + value, found := details.Variables.Find("return") + assert.Equal(t, found, true) + assert.Equal(t, value.Value, "value") +} diff --git a/test/unittest/executor/playbook_action/playbook_action_executor_test.go b/test/unittest/executor/playbook_action/playbook_action_executor_test.go new file mode 100644 index 00000000..d17082cb --- /dev/null +++ b/test/unittest/executor/playbook_action/playbook_action_executor_test.go @@ -0,0 +1,84 @@ +package playbook_action_executor_test + +import ( + "testing" + + "soarca/internal/decomposer" + "soarca/internal/executors/playbook_action" + mock_database_controller "soarca/test/unittest/mocks/mock_controller/database" + mock_decomposer_controller "soarca/test/unittest/mocks/mock_controller/decomposer" + "soarca/test/unittest/mocks/mock_decomposer" + mocks_playbook_test "soarca/test/unittest/mocks/playbook" + + "soarca/models/cacao" + "soarca/models/execution" + + "github.com/go-playground/assert/v2" + "github.com/google/uuid" +) + +func TestExecutePlaybook(t *testing.T) { + + playbookRepoMock := new(mocks_playbook_test.MockPlaybook) + mockDecomposer := new(mock_decomposer.Mock_Decomposer) + + controller := new(mock_decomposer_controller.Mock_Controller) + database := new(mock_database_controller.Mock_Controller) + + executerObject := playbook_action.New(controller, database) + executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + playbookId := "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" + stepId := "step--81eff59f-d084-4324-9e0a-59e353dbd28f" + + metadata := execution.Metadata{ExecutionId: executionId, PlaybookId: playbookId, StepId: stepId} + + initialVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing", + } + + addedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing2", + } + + expectedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing2", + } + + returnedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing2", + } + + step := cacao.Step{ + Type: cacao.StepTypePlaybookAction, + Name: "Playbook action test", + ID: stepId, + Description: "", + PlaybookID: playbookId, + } + + database.On("GetDatabaseInstance").Return(playbookRepoMock) + controller.On("NewDecomposer").Return(mockDecomposer) + + playbook := cacao.Playbook{ID: playbookId, PlaybookVariables: cacao.NewVariables(initialVariables)} + playbookRepoMock.On("Read", playbookId).Return(playbook, nil) + details := decomposer.ExecutionDetails{ExecutionId: executionId, + PlaybookId: playbookId, + Variables: cacao.NewVariables(returnedVariables)} + + playbook2 := cacao.Playbook{ID: playbookId, PlaybookVariables: cacao.NewVariables(expectedVariables)} + mockDecomposer.On("Execute", playbook2).Return(&details, nil) + + results, err := executerObject.Execute(metadata, step, cacao.NewVariables(addedVariables)) + + assert.Equal(t, err, nil) + assert.Equal(t, results, cacao.NewVariables(returnedVariables)) + +} diff --git a/test/unittest/mocks/mock_executor/playbook_action/mock_playbook_action_executor.go b/test/unittest/mocks/mock_executor/playbook_action/mock_playbook_action_executor.go new file mode 100644 index 00000000..b5643a76 --- /dev/null +++ b/test/unittest/mocks/mock_executor/playbook_action/mock_playbook_action_executor.go @@ -0,0 +1,19 @@ +package mock_playbook_action_executor + +import ( + "soarca/models/cacao" + "soarca/models/execution" + + "github.com/stretchr/testify/mock" +) + +type Mock_PlaybookActionExecutor struct { + mock.Mock +} + +func (executer *Mock_PlaybookActionExecutor) Execute(metadata execution.Metadata, + step cacao.Step, + variables cacao.Variables) (cacao.Variables, error) { + args := executer.Called(metadata, step, variables) + return args.Get(0).(cacao.Variables), args.Error(1) +}