From ddc95bf6962c045b3bd6bd6fc5feeae59fad2211 Mon Sep 17 00:00:00 2001 From: lucamrgs <39555424+lucamrgs@users.noreply.github.com> Date: Wed, 24 Apr 2024 20:07:04 +0200 Subject: [PATCH] Feature/101 base reporter (#117) --- .env.example | 1 + .../en/docs/core-components/reporting.md | 49 ++++--- go.mod | 2 +- internal/controller/controller.go | 11 +- internal/decomposer/decomposer.go | 10 +- internal/executors/action/action.go | 7 +- .../playbook_action/playbook_action.go | 8 +- .../downstream_reporter.go | 12 ++ internal/reporter/reporter.go | 79 ++++++++++ test/unittest/decomposer/decomposer_test.go | 28 +++- .../executor/action/action_executor_test.go | 14 +- .../playbook_action_executor_test.go | 6 +- .../mock_reporter/mock_downstream_reporter.go | 22 +++ .../mocks/mock_reporter/mock_reporter.go | 20 +++ .../reporters/reporter/reporter_test.go | 136 ++++++++++++++++++ 15 files changed, 364 insertions(+), 41 deletions(-) create mode 100644 internal/reporter/downstream_reporter/downstream_reporter.go create mode 100644 internal/reporter/reporter.go create mode 100644 test/unittest/mocks/mock_reporter/mock_downstream_reporter.go create mode 100644 test/unittest/mocks/mock_reporter/mock_reporter.go create mode 100644 test/unittest/reporters/reporter/reporter_test.go diff --git a/.env.example b/.env.example index 0f6be478..94ba4b9b 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ DB_USERNAME: "root" DB_PASSWORD: "rootpassword" PLAYBOOK_API_LOG_LEVEL: trace DATABASE: "false" +MAX_REPORTERS: "5" LOG_GLOBAL_LEVEL: "info" LOG_MODE: "development" diff --git a/docs/content/en/docs/core-components/reporting.md b/docs/content/en/docs/core-components/reporting.md index baf7b2f2..f4c0409a 100644 --- a/docs/content/en/docs/core-components/reporting.md +++ b/docs/content/en/docs/core-components/reporting.md @@ -30,49 +30,56 @@ The schema below represents the architecture concept. @startuml set separator :: +interface IStepReporter{ + ReportStep() error +} + interface IWorkflowReporter{ - ReportWorkflow(cacao.workflow) + ReportWorkflow() error } -interface IStepReporter{ - ReportStep(cacao.workflow.Step, cacao.Variables, error) + +interface IDownStreamReporter { + ReportWorkflow() error + ReportStep() error } class Reporter { - stepReporters []IStepReporter - workflowReporters []IWorkflowReporter + reporters []IDownStreamReporter - RegisterStepReporter() - RegisterWorkflowReporter() + RegisterReporters() error + ReportWorkflow() + ReportStep() } -class Database as DB + +class Database +class Cache class 3PTool + class Decomposer class Executor -Decomposer -up-> Reporter -Executor -up-> Reporter +Decomposer -right-> IWorkflowReporter +Executor -left-> IStepReporter -Reporter -up-> IWorkflowReporter -Reporter .up.|> IWorkflowReporter Reporter .up.|> IStepReporter -Reporter -up-> IStepReporter +Reporter .up.|> IWorkflowReporter +Reporter -right-> IDownStreamReporter -DB .up.|> IWorkflowReporter -DB .up.|> IStepReporter -3PTool .up.|> IWorkflowReporter -3PTool .up.|> IStepReporter -Reporter --left--> DB -Reporter --right--> 3PTool +Database .up.|> IDownStreamReporter +Cache .up.|> IDownStreamReporter +3PTool .up.|> IDownStreamReporter ``` ### Interfaces -The logic and extensibility is implemented in the SOARCA architecture by means of reporting interfaces. At this stage, we implement an *IWorkflowReporter* to push information about the entire workflow to be executed, and an *IStepReporter* to push step-specific information as the steps of the workflow are executed. +The reporting logic and extensibility is implemented in the SOARCA architecture by means of reporting interfaces. At this stage, we implement an *IWorkflowReporter* to push information about the entire workflow to be executed, and an *IStepReporter* to push step-specific information as the steps of the workflow are executed. + +A high level *Reporter* component will implement both interfaces, and maintain the list of *DownStreamRepporter*s activated for the SOARCA instance. The *Reporter* class will invoke all reporting functions for each active reporter. The *Executer* and *Decomposer* components will be injected each with the Reporter though, as interface of respectively workflow reporter, and step reporter, to keep the reporting scope separated. -A high level *Reporter* component will implement both interfaces, and maintain the list of decomposer and executor reporters activated for the SOARCA instance. The *Reporter* class will invoke all reporting functions for each active reporter. +The *DownStream* reporters will implement push-based reporting functions specific for the reporting target, as shown in the *IDownStreamReporter* interface. Internal components to SOARCA, and third-party tool reporters, will thus implement the *IDownStreamReporter* interface. ## Future plans diff --git a/go.mod b/go.mod index be95ff74..f2408413 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module soarca -go 1.22 +go 1.22.0 require ( github.com/eclipse/paho.mqtt.golang v1.4.3 diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 87141251..de16749a 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -17,10 +17,13 @@ import ( "soarca/internal/executors/playbook_action" "soarca/internal/fin/protocol" "soarca/internal/guid" + "soarca/internal/reporter" "soarca/logger" "soarca/utils" httpUtil "soarca/utils/http" + downstreamReporter "soarca/internal/reporter/downstream_reporter" + "github.com/gin-gonic/gin" mongo "soarca/database/mongodb" @@ -67,10 +70,12 @@ func (controller *Controller) NewDecomposer() decomposer.IDecomposer { } } - actionExecutor := action.New(capabilities) - playbookActionExecutor := playbook_action.New(controller, controller) + reporter := reporter.New([]downstreamReporter.IDownStreamReporter{}) + + actionExecutor := action.New(capabilities, reporter) + playbookActionExecutor := playbook_action.New(controller, controller, reporter) guid := new(guid.Guid) - decompose := decomposer.New(actionExecutor, playbookActionExecutor, guid) + decompose := decomposer.New(actionExecutor, playbookActionExecutor, guid, reporter) return decompose } diff --git a/internal/decomposer/decomposer.go b/internal/decomposer/decomposer.go index c4f9160c..a570dd4e 100644 --- a/internal/decomposer/decomposer.go +++ b/internal/decomposer/decomposer.go @@ -8,6 +8,7 @@ import ( "soarca/internal/executors" "soarca/internal/executors/action" "soarca/internal/guid" + "soarca/internal/reporter" "soarca/logger" "soarca/models/cacao" "soarca/models/execution" @@ -38,11 +39,12 @@ func init() { func New(actionExecutor action.IExecuter, playbookActionExecutor executors.IPlaybookExecuter, - guid guid.IGuid) *Decomposer { + guid guid.IGuid, reporter reporter.IWorkflowReporter) *Decomposer { return &Decomposer{actionExecutor: actionExecutor, playbookActionExecutor: playbookActionExecutor, - guid: guid} + guid: guid, + reporter: reporter} } type Decomposer struct { @@ -51,6 +53,7 @@ type Decomposer struct { actionExecutor action.IExecuter playbookActionExecutor executors.IPlaybookExecuter guid guid.IGuid + reporter reporter.IWorkflowReporter } // Execute a Playbook @@ -65,6 +68,9 @@ func (decomposer *Decomposer) Execute(playbook cacao.Playbook) (*ExecutionDetail variables := cacao.NewVariables() variables.Merge(playbook.PlaybookVariables) + // Reporting workflow instantiation + decomposer.reporter.ReportWorkflow(decomposer.details.ExecutionId, playbook) + outputVariables, err := decomposer.ExecuteBranch(stepId, variables) decomposer.details.Variables = outputVariables diff --git a/internal/executors/action/action.go b/internal/executors/action/action.go index 9df92146..03e1d926 100644 --- a/internal/executors/action/action.go +++ b/internal/executors/action/action.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "soarca/internal/capability" + "soarca/internal/reporter" "soarca/logger" "soarca/models/cacao" "soarca/models/execution" @@ -17,9 +18,10 @@ func init() { log = logger.Logger(component, logger.Info, "", logger.Json) } -func New(capabilities map[string]capability.ICapability) *Executor { +func New(capabilities map[string]capability.ICapability, reporter reporter.IStepReporter) *Executor { var instance = Executor{} instance.capabilities = capabilities + instance.reporter = reporter return &instance } @@ -38,6 +40,7 @@ type IExecuter interface { type Executor struct { capabilities map[string]capability.ICapability + reporter reporter.IStepReporter } func (executor *Executor) Execute(meta execution.Metadata, metadata PlaybookStepMetadata) (cacao.Variables, error) { @@ -73,12 +76,14 @@ func (executor *Executor) Execute(meta execution.Metadata, metadata PlaybookStep if err != nil { log.Error("Error executing Command ", err) + executor.reporter.ReportStep(meta.ExecutionId, metadata.Step, returnVariables, err) return cacao.NewVariables(), err } else { log.Debug("Command executed") } } } + executor.reporter.ReportStep(meta.ExecutionId, metadata.Step, returnVariables, nil) return returnVariables, nil } diff --git a/internal/executors/playbook_action/playbook_action.go b/internal/executors/playbook_action/playbook_action.go index ba19f320..5b687adf 100644 --- a/internal/executors/playbook_action/playbook_action.go +++ b/internal/executors/playbook_action/playbook_action.go @@ -6,6 +6,7 @@ import ( "reflect" "soarca/internal/controller/database" "soarca/internal/controller/decomposer_controller" + "soarca/internal/reporter" "soarca/logger" "soarca/models/cacao" "soarca/models/execution" @@ -14,6 +15,7 @@ import ( type PlaybookAction struct { decomposerController decomposer_controller.IController databaseController database.IController + reporter reporter.IStepReporter } var component = reflect.TypeOf(PlaybookAction{}).PkgPath() @@ -24,8 +26,8 @@ func init() { } func New(controller decomposer_controller.IController, - database database.IController) *PlaybookAction { - return &PlaybookAction{decomposerController: controller, databaseController: database} + database database.IController, reporter reporter.IStepReporter) *PlaybookAction { + return &PlaybookAction{decomposerController: controller, databaseController: database, reporter: reporter} } func (playbookAction *PlaybookAction) Execute(metadata execution.Metadata, @@ -54,8 +56,10 @@ func (playbookAction *PlaybookAction) Execute(metadata execution.Metadata, if err != nil { err = errors.New(fmt.Sprint("execution of playbook failed with error: ", err)) log.Error(err) + playbookAction.reporter.ReportStep(metadata.ExecutionId, step, playbook.PlaybookVariables, err) return cacao.NewVariables(), err } + playbookAction.reporter.ReportStep(metadata.ExecutionId, step, playbook.PlaybookVariables, nil) return details.Variables, nil } diff --git a/internal/reporter/downstream_reporter/downstream_reporter.go b/internal/reporter/downstream_reporter/downstream_reporter.go new file mode 100644 index 00000000..73e55f09 --- /dev/null +++ b/internal/reporter/downstream_reporter/downstream_reporter.go @@ -0,0 +1,12 @@ +package downstream_reporter + +import ( + "soarca/models/cacao" + + "github.com/google/uuid" +) + +type IDownStreamReporter interface { + ReportWorkflow(executionId uuid.UUID, playbook cacao.Playbook) error + ReportStep(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables, err error) error +} diff --git a/internal/reporter/reporter.go b/internal/reporter/reporter.go new file mode 100644 index 00000000..7625e74b --- /dev/null +++ b/internal/reporter/reporter.go @@ -0,0 +1,79 @@ +package reporter + +import ( + "errors" + "reflect" + "strconv" + + downstreamReporter "soarca/internal/reporter/downstream_reporter" + "soarca/logger" + "soarca/models/cacao" + "soarca/utils" + + "github.com/google/uuid" +) + +type Empty struct{} + +var component = reflect.TypeOf(Empty{}).PkgPath() +var log *logger.Log + +func init() { + log = logger.Logger(component, logger.Info, "", logger.Json) +} + +// Reporter interfaces +type IWorkflowReporter interface { + // -> Give info to downstream reporters + ReportWorkflow(executionId uuid.UUID, playbook cacao.Playbook) +} +type IStepReporter interface { + // -> Give info to downstream reporters + ReportStep(executionId uuid.UUID, step cacao.Step, returnVars cacao.Variables, err error) +} + +const MaxReporters int = 10 + +// High-level reporter class with injection of specific reporters +type Reporter struct { + reporters []downstreamReporter.IDownStreamReporter + maxReporters int +} + +func New(reporters []downstreamReporter.IDownStreamReporter) *Reporter { + maxReporters, _ := strconv.Atoi(utils.GetEnv("MAX_REPORTERS", strconv.Itoa(MaxReporters))) + instance := Reporter{ + reporters: reporters, + maxReporters: maxReporters, + } + return &instance +} + +func (reporter *Reporter) RegisterReporters(reporters []downstreamReporter.IDownStreamReporter) error { + if (len(reporter.reporters) + len(reporters)) > reporter.maxReporters { + log.Warning("reporter not registered, too many reporters") + return errors.New("attempting to register too many reporters") + } + reporter.reporters = append(reporter.reporters, reporters...) + return nil +} + +func (reporter *Reporter) ReportWorkflow(executionId uuid.UUID, playbook cacao.Playbook) { + log.Trace("reporting workflow") + for _, rep := range reporter.reporters { + err := rep.ReportWorkflow(executionId, playbook) + if err != nil { + log.Warning(err) + } + } +} + +func (reporter *Reporter) ReportStep(executionId uuid.UUID, step cacao.Step, returnVars cacao.Variables, err error) { + log.Trace("reporting step data") + for _, rep := range reporter.reporters { + err := rep.ReportStep(executionId, step, returnVars, err) + if err != nil { + log.Warning(err) + } + } +} diff --git a/test/unittest/decomposer/decomposer_test.go b/test/unittest/decomposer/decomposer_test.go index 91c9b0a6..2136cbab 100644 --- a/test/unittest/decomposer/decomposer_test.go +++ b/test/unittest/decomposer/decomposer_test.go @@ -12,6 +12,7 @@ import ( "soarca/test/unittest/mocks/mock_executor" mock_playbook_action_executor "soarca/test/unittest/mocks/mock_executor/playbook_action" "soarca/test/unittest/mocks/mock_guid" + "soarca/test/unittest/mocks/mock_reporter" "github.com/go-playground/assert/v2" "github.com/google/uuid" @@ -21,6 +22,7 @@ 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) + mock_reporter := new(mock_reporter.Mock_Reporter) expectedCommand := cacao.Command{ Type: "ssh", @@ -35,7 +37,7 @@ func TestExecutePlaybook(t *testing.T) { decomposer := decomposer.New(mock_action_executor, mock_playbook_action_executor, - uuid_mock) + uuid_mock, mock_reporter) step1 := cacao.Step{ Type: "action", @@ -96,6 +98,7 @@ func TestExecutePlaybook(t *testing.T) { Variables: cacao.NewVariables(expectedVariables), } + mock_reporter.On("ReportWorkflow", executionId, playbook).Return() mock_action_executor.On("Execute", metaStep1, playbookStepMetadata).Return(cacao.NewVariables(cacao.Variable{Name: "return", Value: "value"}), nil) details, err := decomposer.Execute(playbook) @@ -104,6 +107,7 @@ func TestExecutePlaybook(t *testing.T) { assert.Equal(t, err, nil) assert.Equal(t, details.ExecutionId, executionId) mock_action_executor.AssertExpectations(t) + mock_reporter.AssertExpectations(t) value, found := details.Variables.Find("return") assert.Equal(t, found, true) assert.Equal(t, value.Value, "value") @@ -113,6 +117,7 @@ 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) + mock_reporter := new(mock_reporter.Mock_Reporter) expectedCommand := cacao.Command{ Type: "ssh", @@ -138,7 +143,7 @@ func TestExecutePlaybookMultiStep(t *testing.T) { decomposer := decomposer.New(mock_action_executor, mock_playbook_action_executor, - uuid_mock) + uuid_mock, mock_reporter) step1 := cacao.Step{ Type: "action", @@ -223,6 +228,7 @@ func TestExecutePlaybookMultiStep(t *testing.T) { Variables: cacao.NewVariables(expectedVariables), } + mock_reporter.On("ReportWorkflow", executionId, playbook).Return() mock_action_executor.On("Execute", metaStep1, playbookStepMetadata1).Return(cacao.NewVariables(firstResult), nil) playbookStepMetadata2 := action.PlaybookStepMetadata{ @@ -241,6 +247,7 @@ func TestExecutePlaybookMultiStep(t *testing.T) { assert.Equal(t, err, nil) assert.Equal(t, details.ExecutionId, executionId) mock_action_executor.AssertExpectations(t) + mock_reporter.AssertExpectations(t) value, found := details.Variables.Find("result") assert.Equal(t, found, true) @@ -254,6 +261,7 @@ 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) + mock_reporter := new(mock_reporter.Mock_Reporter) expectedCommand := cacao.Command{ Type: "ssh", @@ -278,7 +286,7 @@ func TestExecuteEmptyMultiStep(t *testing.T) { decomposer2 := decomposer.New(mock_action_executor2, mock_playbook_action_executor2, - uuid_mock2) + uuid_mock2, mock_reporter) step1 := cacao.Step{ Type: "ssh", @@ -304,12 +312,15 @@ func TestExecuteEmptyMultiStep(t *testing.T) { id, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") uuid_mock2.On("New").Return(id) + mock_reporter.On("ReportWorkflow", id, playbook).Return() + returnedId, err := decomposer2.Execute(playbook) uuid_mock2.AssertExpectations(t) fmt.Println(err) assert.Equal(t, err, errors.New("empty success step")) assert.Equal(t, returnedId.ExecutionId, id) mock_action_executor2.AssertExpectations(t) + mock_reporter.AssertExpectations(t) } /* @@ -319,6 +330,7 @@ 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) + mock_reporter := new(mock_reporter.Mock_Reporter) expectedCommand := cacao.Command{ Type: "ssh", @@ -333,7 +345,7 @@ func TestExecuteIllegalMultiStep(t *testing.T) { decomposer2 := decomposer.New(mock_action_executor2, mock_playbook_action_executor2, - uuid_mock2) + uuid_mock2, mock_reporter) step1 := cacao.Step{ Type: "action", @@ -356,9 +368,11 @@ func TestExecuteIllegalMultiStep(t *testing.T) { id, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") uuid_mock2.On("New").Return(id) + mock_reporter.On("ReportWorkflow", id, playbook).Return() returnedId, err := decomposer2.Execute(playbook) uuid_mock2.AssertExpectations(t) + mock_reporter.AssertExpectations(t) fmt.Println(err) assert.Equal(t, err, errors.New("empty success step")) assert.Equal(t, returnedId.ExecutionId, id) @@ -369,7 +383,7 @@ 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) - + mock_reporter := new(mock_reporter.Mock_Reporter) expectedVariables := cacao.Variable{ Type: "string", Name: "var1", @@ -378,7 +392,7 @@ func TestExecutePlaybookAction(t *testing.T) { decomposer := decomposer.New(mock_action_executor, mock_playbook_action_executor, - uuid_mock) + uuid_mock, mock_reporter) step1 := cacao.Step{ Type: "playbook-action", @@ -407,6 +421,7 @@ func TestExecutePlaybookAction(t *testing.T) { metaStep1 := execution.Metadata{ExecutionId: executionId, PlaybookId: "test", StepId: step1.ID} uuid_mock.On("New").Return(executionId) + mock_reporter.On("ReportWorkflow", executionId, playbook).Return() mock_playbook_action_executor.On("Execute", metaStep1, @@ -418,6 +433,7 @@ func TestExecutePlaybookAction(t *testing.T) { fmt.Println(err) assert.Equal(t, err, nil) assert.Equal(t, details.ExecutionId, executionId) + mock_reporter.AssertExpectations(t) mock_action_executor.AssertExpectations(t) value, found := details.Variables.Find("return") assert.Equal(t, found, true) diff --git a/test/unittest/executor/action/action_executor_test.go b/test/unittest/executor/action/action_executor_test.go index fd54b74b..31fce4d4 100644 --- a/test/unittest/executor/action/action_executor_test.go +++ b/test/unittest/executor/action/action_executor_test.go @@ -9,6 +9,7 @@ import ( "soarca/models/cacao" "soarca/models/execution" "soarca/test/unittest/mocks/mock_capability" + "soarca/test/unittest/mocks/mock_reporter" "github.com/go-playground/assert/v2" "github.com/google/uuid" @@ -17,10 +18,11 @@ import ( func TestExecuteStep(t *testing.T) { mock_ssh := new(mock_capability.Mock_Capability) mock_http := new(mock_capability.Mock_Capability) + mock_reporter := new(mock_reporter.Mock_Reporter) capabilities := map[string]capability.ICapability{"mock-ssh": mock_ssh, "http-api": mock_http} - executerObject := action.New(capabilities) + executerObject := action.New(capabilities, mock_reporter) executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") playbookId := "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" stepId := "step--81eff59f-d084-4324-9e0a-59e353dbd28f" @@ -73,6 +75,7 @@ func TestExecuteStep(t *testing.T) { Variables: cacao.NewVariables(expectedVariables), } + mock_reporter.On("ReportStep", executionId, step, cacao.NewVariables(expectedVariables), nil).Return() mock_ssh.On("Execute", metadata, expectedCommand, @@ -86,16 +89,18 @@ func TestExecuteStep(t *testing.T) { actionMetadata) assert.Equal(t, err, nil) + mock_reporter.AssertExpectations(t) mock_ssh.AssertExpectations(t) } func TestExecuteActionStep(t *testing.T) { mock_ssh := new(mock_capability.Mock_Capability) mock_http := new(mock_capability.Mock_Capability) + mock_reporter := new(mock_reporter.Mock_Reporter) capabilities := map[string]capability.ICapability{"ssh": mock_ssh, "http-api": mock_http} - executerObject := action.New(capabilities) + executerObject := action.New(capabilities, mock_reporter) executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") playbookId := "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" stepId := "step--81eff59f-d084-4324-9e0a-59e353dbd28f" @@ -143,6 +148,7 @@ func TestExecuteActionStep(t *testing.T) { agent) assert.Equal(t, err, nil) + mock_reporter.AssertExpectations(t) mock_ssh.AssertExpectations(t) } @@ -152,7 +158,7 @@ func TestNonExistingCapabilityStep(t *testing.T) { capabilities := map[string]capability.ICapability{"ssh": mock_ssh, "http-api": mock_http} - executerObject := action.New(capabilities) + executerObject := action.New(capabilities, new(mock_reporter.Mock_Reporter)) executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") playbookId := "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" stepId := "step--81eff59f-d084-4324-9e0a-59e353dbd28f" @@ -199,7 +205,7 @@ func TestVariableInterpolation(t *testing.T) { capabilities := map[string]capability.ICapability{"cap1": mock_capability1} - executerObject := action.New(capabilities) + executerObject := action.New(capabilities, new(mock_reporter.Mock_Reporter)) executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") playbookId := "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" stepId := "step--81eff59f-d084-4324-9e0a-59e353dbd28f" diff --git a/test/unittest/executor/playbook_action/playbook_action_executor_test.go b/test/unittest/executor/playbook_action/playbook_action_executor_test.go index d17082cb..fc52714e 100644 --- a/test/unittest/executor/playbook_action/playbook_action_executor_test.go +++ b/test/unittest/executor/playbook_action/playbook_action_executor_test.go @@ -8,6 +8,7 @@ import ( 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" + "soarca/test/unittest/mocks/mock_reporter" mocks_playbook_test "soarca/test/unittest/mocks/playbook" "soarca/models/cacao" @@ -21,11 +22,12 @@ func TestExecutePlaybook(t *testing.T) { playbookRepoMock := new(mocks_playbook_test.MockPlaybook) mockDecomposer := new(mock_decomposer.Mock_Decomposer) + mock_reporter := new(mock_reporter.Mock_Reporter) controller := new(mock_decomposer_controller.Mock_Controller) database := new(mock_database_controller.Mock_Controller) - executerObject := playbook_action.New(controller, database) + executerObject := playbook_action.New(controller, database, mock_reporter) executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") playbookId := "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" stepId := "step--81eff59f-d084-4324-9e0a-59e353dbd28f" @@ -66,6 +68,7 @@ func TestExecutePlaybook(t *testing.T) { database.On("GetDatabaseInstance").Return(playbookRepoMock) controller.On("NewDecomposer").Return(mockDecomposer) + mock_reporter.On("ReportStep", executionId, step, cacao.NewVariables(returnedVariables), nil).Return() playbook := cacao.Playbook{ID: playbookId, PlaybookVariables: cacao.NewVariables(initialVariables)} playbookRepoMock.On("Read", playbookId).Return(playbook, nil) @@ -78,6 +81,7 @@ func TestExecutePlaybook(t *testing.T) { results, err := executerObject.Execute(metadata, step, cacao.NewVariables(addedVariables)) + mock_reporter.AssertExpectations(t) assert.Equal(t, err, nil) assert.Equal(t, results, cacao.NewVariables(returnedVariables)) diff --git a/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go new file mode 100644 index 00000000..168edfa5 --- /dev/null +++ b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go @@ -0,0 +1,22 @@ +package mock_reporter + +import ( + "soarca/models/cacao" + + "github.com/google/uuid" + "github.com/stretchr/testify/mock" +) + +type Mock_Downstream_Reporter struct { + mock.Mock +} + +func (reporter *Mock_Downstream_Reporter) ReportWorkflow(executionId uuid.UUID, playbook cacao.Playbook) error { + args := reporter.Called(executionId, playbook) + return args.Error(0) +} + +func (reporter *Mock_Downstream_Reporter) ReportStep(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables, err error) error { + args := reporter.Called(executionId, step, stepResults, err) + return args.Error(0) +} diff --git a/test/unittest/mocks/mock_reporter/mock_reporter.go b/test/unittest/mocks/mock_reporter/mock_reporter.go new file mode 100644 index 00000000..14b567c9 --- /dev/null +++ b/test/unittest/mocks/mock_reporter/mock_reporter.go @@ -0,0 +1,20 @@ +package mock_reporter + +import ( + "soarca/models/cacao" + + "github.com/google/uuid" + "github.com/stretchr/testify/mock" +) + +type Mock_Reporter struct { + mock.Mock +} + +func (reporter *Mock_Reporter) ReportWorkflow(executionId uuid.UUID, playbook cacao.Playbook) { + _ = reporter.Called(executionId, playbook) +} + +func (reporter *Mock_Reporter) ReportStep(executionId uuid.UUID, step cacao.Step, returnVars cacao.Variables, err error) { + _ = reporter.Called(executionId, step, returnVars, err) +} diff --git a/test/unittest/reporters/reporter/reporter_test.go b/test/unittest/reporters/reporter/reporter_test.go new file mode 100644 index 00000000..ba4b6f32 --- /dev/null +++ b/test/unittest/reporters/reporter/reporter_test.go @@ -0,0 +1,136 @@ +package reporter_test + +import ( + "errors" + "soarca/internal/reporter" + ds_reporter "soarca/internal/reporter/downstream_reporter" + "soarca/models/cacao" + "soarca/test/unittest/mocks/mock_reporter" + "testing" + + "github.com/go-playground/assert/v2" + "github.com/google/uuid" +) + +func TestRegisterReporter(t *testing.T) { + mock_ds_reporter := mock_reporter.Mock_Downstream_Reporter{} + reporter := reporter.New([]ds_reporter.IDownStreamReporter{}) + err := reporter.RegisterReporters([]ds_reporter.IDownStreamReporter{&mock_ds_reporter}) + if err != nil { + t.Fail() + } +} + +func TestRegisterTooManyReporters(t *testing.T) { + too_many_reporters := make([]ds_reporter.IDownStreamReporter, reporter.MaxReporters+1) + mock_ds_reporter := mock_reporter.Mock_Downstream_Reporter{} + for i := range too_many_reporters { + too_many_reporters[i] = &mock_ds_reporter + } + + reporter := reporter.New([]ds_reporter.IDownStreamReporter{}) + err := reporter.RegisterReporters(too_many_reporters) + + expected_err := errors.New("attempting to register too many reporters") + assert.Equal(t, expected_err, err) +} + +func TestReportWorkflow(t *testing.T) { + mock_ds_reporter := mock_reporter.Mock_Downstream_Reporter{} + reporter := reporter.New([]ds_reporter.IDownStreamReporter{&mock_ds_reporter}) + + expectedCommand := cacao.Command{ + Type: "ssh", + Command: "ssh ls -la", + } + + expectedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing", + } + + step1 := cacao.Step{ + Type: "action", + ID: "action--test", + Name: "ssh-tests", + StepVariables: cacao.NewVariables(expectedVariables), + Commands: []cacao.Command{expectedCommand}, + Cases: map[string]string{}, + OnCompletion: "end--test", + Agent: "agent1", + Targets: []string{"target1"}, + } + + end := cacao.Step{ + Type: "end", + ID: "end--test", + Name: "end step", + } + + expectedAuth := cacao.AuthenticationInformation{ + Name: "user", + ID: "auth1", + } + + expectedTarget := cacao.AgentTarget{ + Name: "sometarget", + AuthInfoIdentifier: "auth1", + ID: "target1", + } + + expectedAgent := cacao.AgentTarget{ + Type: "soarca", + Name: "soarca-ssh", + } + + playbook := cacao.Playbook{ + ID: "test", + Type: "test", + Name: "ssh-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}, + + Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, + } + + executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + + mock_ds_reporter.On("ReportWorkflow", executionId, playbook).Return(nil) + reporter.ReportWorkflow(executionId, playbook) +} + +func TestReportStep(t *testing.T) { + mock_ds_reporter := mock_reporter.Mock_Downstream_Reporter{} + reporter := reporter.New([]ds_reporter.IDownStreamReporter{&mock_ds_reporter}) + + expectedCommand := cacao.Command{ + Type: "ssh", + Command: "ssh ls -la", + } + + expectedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing", + } + + step1 := cacao.Step{ + Type: "action", + ID: "action--test", + Name: "ssh-tests", + StepVariables: cacao.NewVariables(expectedVariables), + Commands: []cacao.Command{expectedCommand}, + Cases: map[string]string{}, + OnCompletion: "end--test", + Agent: "agent1", + Targets: []string{"target1"}, + } + + executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + + mock_ds_reporter.On("ReportStep", executionId, step1, cacao.NewVariables(expectedVariables), nil).Return(nil) + reporter.ReportStep(executionId, step1, cacao.NewVariables(expectedVariables), nil) +}