From e2f5925d383710b21ba197406467613aa0098279 Mon Sep 17 00:00:00 2001 From: Hidde-Jan Jongsma Date: Tue, 6 Feb 2024 10:36:55 +0100 Subject: [PATCH] Refactor decomposer Refactor the decomposer to pave the way for other step types. --- internal/decomposer/decomposer.go | 169 +++++++++++++++----- test/unittest/decomposer/decomposer_test.go | 47 +++--- 2 files changed, 157 insertions(+), 59 deletions(-) diff --git a/internal/decomposer/decomposer.go b/internal/decomposer/decomposer.go index 9db1c3a3..9ae90245 100644 --- a/internal/decomposer/decomposer.go +++ b/internal/decomposer/decomposer.go @@ -2,6 +2,7 @@ package decomposer import ( "errors" + "fmt" "reflect" "soarca/internal/executer" "soarca/internal/guid" @@ -20,7 +21,7 @@ var log *logger.Log type ExecutionDetails struct { ExecutionId uuid.UUID PlaybookId string - CurrentStep string + Variables cacao.Variables } type IDecomposer interface { @@ -43,58 +44,144 @@ func New(executor executer.IExecuter, guid guid.IGuid) *Decomposer { } type Decomposer struct { - playbooks cacao.Playbook - details ExecutionDetails - executor executer.IExecuter - guid guid.IGuid + playbook cacao.Playbook + details ExecutionDetails + executor executer.IExecuter + guid guid.IGuid } -func Callback(executionId uuid.UUID, outputVariables cacao.Variables) { +// Execute a Playbook +func (decomposer *Decomposer) Execute(playbook cacao.Playbook) (*ExecutionDetails, error) { + executionId := decomposer.guid.New() + log.Debugf("Starting execution %s for Playbook %s", executionId, playbook.ID) -} + decomposer.details = ExecutionDetails{executionId, playbook.ID, playbook.PlaybookVariables} + decomposer.playbook = playbook -func (decomposer *Decomposer) Execute(playbook cacao.Playbook) (*ExecutionDetails, error) { - var executionId = decomposer.guid.New() + stepId := playbook.WorkflowStart + variables := cacao.NewVariables() + variables.Merge(playbook.PlaybookVariables) + + outputVariables, err := decomposer.ExecuteBranch(stepId, variables) - decomposer.details = ExecutionDetails{executionId, playbook.ID, ""} - decomposer.playbooks = playbook + decomposer.details.Variables = outputVariables + return &decomposer.details, err +} + +// Execute a Workflow branch of a Playbook +// +// Runs until it find an End step or returns an error in case there are no valid next step. +func (decomposer *Decomposer) ExecuteBranch(stepId string, scopeVariables cacao.Variables) (cacao.Variables, error) { + playbook := decomposer.playbook + log.Debug("Executing branch starting from ", stepId) - var stepId = playbook.WorkflowStart + returnVariables := cacao.NewVariables() for { - if playbook.Workflow[stepId].OnCompletion == "" && - playbook.Workflow[stepId].Type == "end" || - playbook.Workflow[stepId].Type == "end" { + currentStep, ok := playbook.Workflow[stepId] + if !ok { + return cacao.NewVariables(), fmt.Errorf("step with id %s not found", stepId) + } + + log.Debug("Executing step ", stepId) + + if currentStep.Type == "end" { break - } else if playbook.Workflow[stepId].OnCompletion == "" { - err := errors.New("empty on_completion_id") - return &decomposer.details, err - } else if _, ok := playbook.Workflow[playbook.Workflow[stepId].OnCompletion]; !ok { - err := errors.New("on_completion_id key is not in workflows") - return &decomposer.details, err } - if len(playbook.Workflow[stepId].Commands) > 0 { - for _, command := range playbook.Workflow[stepId].Commands { - agent := playbook.AgentDefinitions[playbook.Workflow[stepId].Agent] - - for _, element := range playbook.Workflow[stepId].Targets { - target := playbook.TargetDefinitions[element] - auth := playbook.AuthenticationInfoDefinitions[target.AuthInfoIdentifier] - meta := execution.Metadata{PlaybookId: playbook.ID, ExecutionId: executionId, StepId: stepId} - var id, vars, _ = decomposer.executor.Execute(meta, - command, - auth, - target, - playbook.Workflow[stepId].StepVariables, - agent) - log.Trace(id) - log.Trace(vars) - } - } + + onSuccessStepId := currentStep.OnSuccess + if onSuccessStepId == "" { + onSuccessStepId = currentStep.OnCompletion + } + if _, ok := playbook.Workflow[onSuccessStepId]; !ok { + return cacao.NewVariables(), errors.New("empty success step") } - stepId = playbook.Workflow[stepId].OnCompletion + onFailureStepId := currentStep.OnFailure + if onFailureStepId == "" { + onFailureStepId = currentStep.OnCompletion + } + if _, ok := playbook.Workflow[onFailureStepId]; !ok { + return cacao.NewVariables(), errors.New("empty failure step") + } + + outputVariables, err := decomposer.ExecuteStep(currentStep, scopeVariables) + + if err == nil { + stepId = onSuccessStepId + returnVariables.Merge(outputVariables) + scopeVariables.Merge(outputVariables) + } else { + stepId = onFailureStepId + } + } + + return returnVariables, nil +} + +// Execute a single Step within a Workflow +func (decomposer *Decomposer) ExecuteStep(step cacao.Step, scopeVariables cacao.Variables) (cacao.Variables, error) { + log.Debug("Executing step type ", step.Type) + + // Combine parent scope and Step variables + variables := cacao.NewVariables() + variables.Merge(scopeVariables) + variables.Merge(step.StepVariables) + + switch step.Type { + case "action": + return decomposer.ExecuteActionStep(step, variables) + default: + // NOTE: This currently silently handles unknown step types. Should we return an error instead? + return cacao.NewVariables(), nil + } +} + +// Execute a Step of type Action +func (decomposer *Decomposer) ExecuteActionStep(step cacao.Step, scopeVariables cacao.Variables) (cacao.Variables, error) { + log.Debug("Executing action step") + + agent := decomposer.playbook.AgentDefinitions[step.Agent] + returnVariables := cacao.NewVariables() + + for _, command := range step.Commands { + // NOTE: This assumes we want to run Command for every Target individually. + // Is that something we want to enforce or leave up to the capability? + for _, element := range step.Targets { + target := decomposer.playbook.TargetDefinitions[element] + // NOTE: What about Agent authentication? + auth := decomposer.playbook.AuthenticationInfoDefinitions[target.AuthInfoIdentifier] + + meta := execution.Metadata{ + ExecutionId: decomposer.details.ExecutionId, + PlaybookId: decomposer.playbook.ID, + StepId: step.ID, + } + + _, outputVariables, err := decomposer.executor.Execute( + meta, + command, + auth, + target, + scopeVariables, + agent) + + if len(step.OutArgs) > 0 { + // If OutArgs is set, only update execution args that are explicitly referenced + outputVariables = outputVariables.Select(step.OutArgs) + } + + returnVariables.Merge(outputVariables) + scopeVariables.Merge(outputVariables) + + if err != nil { + log.Error("Error executing Command") + return cacao.NewVariables(), err + } else { + log.Debug("Command executed") + } + } } - return &decomposer.details, nil + return returnVariables, nil } diff --git a/test/unittest/decomposer/decomposer_test.go b/test/unittest/decomposer/decomposer_test.go index e1198d90..2c728be1 100644 --- a/test/unittest/decomposer/decomposer_test.go +++ b/test/unittest/decomposer/decomposer_test.go @@ -33,7 +33,7 @@ func TestExecutePlaybook(t *testing.T) { decomposer := decomposer.New(mock_executer, uuid_mock) var step1 = cacao.Step{ - Type: "ssh", + Type: "action", ID: "action--test", Name: "ssh-tests", StepVariables: cacao.NewVariables(expectedVariables), @@ -42,7 +42,6 @@ func TestExecutePlaybook(t *testing.T) { OnCompletion: "end--test", Agent: "agent1", Targets: []string{"target1"}, - //OnCompletion: "action--test2", } var end = cacao.Step{ @@ -69,7 +68,7 @@ func TestExecutePlaybook(t *testing.T) { ID: "test", Type: "test", Name: "ssh-test", - WorkflowStart: "action--test", + WorkflowStart: step1.ID, AuthenticationInfoDefinitions: map[string]cacao.AuthenticationInformation{"id": expectedAuth}, AgentDefinitions: map[string]cacao.AgentTarget{"agent1": expectedAgent}, TargetDefinitions: map[string]cacao.AgentTarget{"target1": expectedTarget}, @@ -86,14 +85,17 @@ func TestExecutePlaybook(t *testing.T) { expectedAuth, expectedTarget, cacao.NewVariables(expectedVariables), - expectedAgent).Return(executionId, cacao.NewVariables(), nil) + expectedAgent).Return(executionId, cacao.NewVariables(cacao.Variable{Name: "return", Value: "value"}), nil) - returnedId, err := decomposer.Execute(playbook) + details, err := decomposer.Execute(playbook) uuid_mock.AssertExpectations(t) fmt.Println(err) assert.Equal(t, err, nil) - assert.Equal(t, returnedId.ExecutionId, executionId) + assert.Equal(t, details.ExecutionId, executionId) mock_executer.AssertExpectations(t) + value, found := details.Variables.Find("return") + assert.Equal(t, found, true) + assert.Equal(t, value.Value, "value") } func TestExecutePlaybookMultiStep(t *testing.T) { @@ -126,7 +128,7 @@ func TestExecutePlaybookMultiStep(t *testing.T) { decomposer := decomposer.New(mock_executer, uuid_mock) var step1 = cacao.Step{ - Type: "ssh", + Type: "action", ID: "action--test", Name: "ssh-tests", StepVariables: cacao.NewVariables(expectedVariables), @@ -139,7 +141,7 @@ func TestExecutePlaybookMultiStep(t *testing.T) { } var step2 = cacao.Step{ - Type: "ssh", + Type: "action", ID: "action--test2", Name: "ssh-tests", StepVariables: cacao.NewVariables(expectedVariables2), @@ -150,7 +152,7 @@ func TestExecutePlaybookMultiStep(t *testing.T) { OnCompletion: "end--test", } var step3 = cacao.Step{ - Type: "ssh", + Type: "action", ID: "action--test3", Name: "ssh-tests", StepVariables: cacao.NewVariables(expectedVariables2), @@ -201,22 +203,31 @@ func TestExecutePlaybookMultiStep(t *testing.T) { expectedAuth, expectedTarget, cacao.NewVariables(expectedVariables), - expectedAgent).Return(executionId, cacao.NewVariables(), nil) + expectedAgent).Return( + executionId, + cacao.NewVariables(cacao.Variable{Name: "result", Value: "value"}), + nil) mock_executer.On("Execute", metaStep2, expectedCommand2, expectedAuth, expectedTarget, - cacao.NewVariables(expectedVariables2), - expectedAgent).Return(executionId, cacao.NewVariables(), nil) + cacao.NewVariables(expectedVariables2, cacao.Variable{Name: "result", Value: "value"}), + expectedAgent).Return( + executionId, + cacao.NewVariables(cacao.Variable{Name: "result", Value: "updated"}), + nil) - returnedId, err := decomposer.Execute(playbook) + details, err := decomposer.Execute(playbook) uuid_mock.AssertExpectations(t) fmt.Println(err) assert.Equal(t, err, nil) - assert.Equal(t, returnedId.ExecutionId, executionId) + assert.Equal(t, details.ExecutionId, executionId) mock_executer.AssertExpectations(t) + value, found := details.Variables.Find("result") + assert.Equal(t, found, true) + assert.Equal(t, value.Value, "updated") // Value overwritten } /* @@ -258,7 +269,7 @@ func TestExecuteEmptyMultiStep(t *testing.T) { StepVariables: cacao.NewVariables(expectedVariables), Commands: []cacao.Command{expectedCommand}, Cases: map[string]string{}, - OnCompletion: "", + OnCompletion: "", // Empty } playbook := cacao.Playbook{ @@ -277,7 +288,7 @@ func TestExecuteEmptyMultiStep(t *testing.T) { returnedId, err := decomposer2.Execute(playbook) uuid_mock2.AssertExpectations(t) fmt.Println(err) - assert.Equal(t, err, errors.New("empty on_completion_id")) + assert.Equal(t, err, errors.New("empty success step")) assert.Equal(t, returnedId.ExecutionId, id) mock_executer2.AssertExpectations(t) } @@ -304,7 +315,7 @@ func TestExecuteIllegalMultiStep(t *testing.T) { decomposer2 := decomposer.New(mock_executer2, uuid_mock2) var step1 = cacao.Step{ - Type: "ssh", + Type: "action", ID: "action--test", Name: "ssh-tests", StepVariables: cacao.NewVariables(expectedVariables), @@ -328,7 +339,7 @@ func TestExecuteIllegalMultiStep(t *testing.T) { returnedId, err := decomposer2.Execute(playbook) uuid_mock2.AssertExpectations(t) fmt.Println(err) - assert.Equal(t, err, errors.New("on_completion_id key is not in workflows")) + assert.Equal(t, err, errors.New("empty success step")) assert.Equal(t, returnedId.ExecutionId, id) mock_executer2.AssertExpectations(t) }