Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor decomposer #43

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}