Skip to content

Commit

Permalink
Refactor decomposer
Browse files Browse the repository at this point in the history
Refactor the decomposer to pave the way for other step types.
  • Loading branch information
hidde-jan committed Mar 15, 2024
1 parent db0463a commit 1dddd7e
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 59 deletions.
169 changes: 128 additions & 41 deletions internal/decomposer/decomposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package decomposer

import (
"errors"
"fmt"
"reflect"
"soarca/internal/executer"
"soarca/internal/guid"
Expand All @@ -20,7 +21,7 @@ var log *logger.Log
type ExecutionDetails struct {
ExecutionId uuid.UUID
PlaybookId string
CurrentStep string
Variables cacao.Variables
}

type IDecomposer interface {
Expand All @@ -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
}
47 changes: 29 additions & 18 deletions test/unittest/decomposer/decomposer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -42,7 +42,6 @@ func TestExecutePlaybook(t *testing.T) {
OnCompletion: "end--test",
Agent: "agent1",
Targets: []string{"target1"},
//OnCompletion: "action--test2",
}

var end = cacao.Step{
Expand All @@ -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},
Expand All @@ -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) {
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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
}

/*
Expand Down Expand Up @@ -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{
Expand All @@ -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)
}
Expand All @@ -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),
Expand All @@ -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)
}

0 comments on commit 1dddd7e

Please sign in to comment.