From 233191eb03a777aae2af6a58511ab78952888e3d Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Wed, 1 May 2024 11:56:39 +0200 Subject: [PATCH 01/40] add first reporter api documentation --- .../en/docs/core-components/api-reporter.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/content/en/docs/core-components/api-reporter.md diff --git a/docs/content/en/docs/core-components/api-reporter.md b/docs/content/en/docs/core-components/api-reporter.md new file mode 100644 index 00000000..ee17ef05 --- /dev/null +++ b/docs/content/en/docs/core-components/api-reporter.md @@ -0,0 +1,168 @@ +--- +title: Reporter API Description +description: > + Descriptions for the Reporter REST API endpoints +categories: [API] +tags: [protocol, http, rest, api] +weight: 2 +date: 2024-05-1 +--- + +# Endpoint description + +We will use HTTP status codes https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + + +```plantuml +@startuml +protocol Reporter { + GET /reporter + GET /reporter/{execution-id} +} +@enduml +``` + + +## /reporter +The reporter endpoint is used to fetch information about ongoing playbook executions in SOARCA + +### GET `/reporter` +Get all execution IDs of currently ongoing executions. + +#### Call payload +None + +#### Response +200/OK with payload: + +```plantuml +@startjson +[ + { + "executions": ["execution-id", "..."] + } +] +@endjson +``` +#### Error +400/BAD REQUEST with payload: +General error + + + +### GET `/reporter/{execution-id}` +Get information about ongoing execution + +#### Call payload +None + +#### Response + +Response data model: + + +|field |content |type | description | +| ----------------- | --------------------- | ----------------- | ----------- | +|type |execution-status |string |The type of this content +|id |UUID |string |The id of the execution +|execution_id |UUID |string |The id of the execution +|playbook_id |UUID |string |The id of the CACAO playbook executed by the execution +|started |timestamp |string |The time at which the execution of the playbook started +|ended |timestamp |string |The time at which the execution of the playbook ended (if so) +|status |execution-status-enum |string |The current [status](#execution-stataus) of the execution +|status_text |explanation |string |A natural language explanation of the current status or related info +|errors |errors |list of string |Errors raised along the execution of the playbook at execution level +|step_results |step_results |dictionary |Map of step-id to related [step execution data](#step-execution-data) +|request_interval |seconds |integer |Suggests the polling interval for the next request (default suggested is 5 seconds). + + +##### Step execution data +|field |content |type | description | +| ----------------- | --------------------- | ----------------- | ----------- | +|execution_id |UUID |string |The id of the execution of the playbook where the step resides +|step_id |UUID |string |The id of the step being executed +|started |timestamp |string |The time at which the execution of the step started +|ended |timestamp |string |The time at which the execution of the step ended (if so) +|status |execution-status-enum |string |The current [status](#execution-stataus) of the execution of this step +|status_text |explanation |string |A natural language explanation of the current status or related info +|errors |errors |list of string |Errors raised along the execution of the step +|variables |cacao variables |dictionary |Map of [cacao variables](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/cs01/security-playbooks-v2.0-cs01.html#_Toc152256555) handled in the step (both in and out) with current values and definitions + +##### Execution stataus +Table from [Cyentific RNI workflow Status](https://github.com/cyentific-rni/workflow-status/blob/main/README.md#21-refined-execution-status-enumeration) +**Vocabulary Name:** `execution-status-enum` +| Property Name | Description| +| :--- | :--- | +| successfully_executed | The workflow step was executed successfully (completed). | +|failed| The workflow step failed. | +|ongoing| The workflow step is in progress. | +|server_side_error| A server-side error occurred. | +|client_side_error| A client-side error occurred.| +|timeout_error| A timeout error occurred. The timeout of a CACAO workflow step is specified in the “timeout” property. | +|exception_condition_error| A exception condition error ocurred. A CACAO playbook can incorporate an exception condition at the playbook level and, in particular, with the "workflow_exception" property. | + + +If the execution has completed and no further steps need to be executed + +200/OK +with payload: + +```plantuml +@startjson +[ + { + "type" : "execution-status", + "id" : "", + "execution_id" : "", + "playbook_id" : "", + "started" : "", + "ended" : "", + "status" : "", + "status_text": "", + "errors" : ["error1", "..."], + "step_results" : { + "" : { + "execution_id": "", + "step_id" : "", + "started" : "", + "ended" : "", + "status" : "", + "status_text": "", + "errors" : ["error1", "..."], + "variables": { + "" : { + "type": "", + "name": "", + "description": "", + "value": "", + "constant": "", + "external": "" + } + } + } + }, + "request_interval" : "" + } +] +@endjson + +The payload will include all information that the finished execution has created. + + +If the execution is still ongoing: + +206/Partial Content +with payload equal to the 200 response, but impliclty not including all information from the execution, since the execution is still ongoing. + +The step results object will list the steps that have been executed until the report request, and those that are being executed at the moment of the report request. + +The "request_interval" suggests the polling interval for the next request (default suggested is 5 seconds). + +#### Error +400/BAD REQUEST with payload: +General error + +404/NOT FOUND +No execution with the specified ID was found. + + From 0baa4882f7763834431ee05eff86fcc33fc71621 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Mon, 6 May 2024 18:53:13 +0200 Subject: [PATCH 02/40] create api reporter instantiation and links. must test --- internal/controller/controller.go | 13 +++- .../controller/informer/execution_informer.go | 12 ++++ .../downstream_reporter/cache/cache.go | 2 +- routes/reporter/init.go | 15 +++++ routes/reporter/reporter_api.go | 60 +++++++++++++++++++ routes/reporter/reporter_endpoints.go | 19 ++++++ routes/router.go | 10 +++- .../downstream_reporter/cache_test.go | 8 +-- 8 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 internal/controller/informer/execution_informer.go create mode 100644 routes/reporter/init.go create mode 100644 routes/reporter/reporter_api.go create mode 100644 routes/reporter/reporter_endpoints.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 48b21264..c33a6fcf 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -20,9 +20,11 @@ import ( "soarca/internal/fin/protocol" "soarca/internal/guid" "soarca/internal/reporter" + "soarca/internal/reporter/downstream_reporter/cache" "soarca/logger" "soarca/utils" httpUtil "soarca/utils/http" + timeUtil "soarca/utils/time" "soarca/utils/stix/expression/comparison" downstreamReporter "soarca/internal/reporter/downstream_reporter" @@ -31,7 +33,7 @@ import ( mongo "soarca/database/mongodb" playbookrepository "soarca/database/playbook" - routes "soarca/routes" + "soarca/routes" ) var log *logger.Log @@ -171,6 +173,15 @@ func initializeCore(app *gin.Engine) error { } } + // NOTE: Assuming that the cache is the main information mediator for + // the reporter API + informer := cache.New(&timeUtil.Time{}) + err = routes.Reporter(app, informer) + if err != nil { + log.Error(err) + return err + } + routes.Logging(app) routes.Swagger(app) diff --git a/internal/controller/informer/execution_informer.go b/internal/controller/informer/execution_informer.go new file mode 100644 index 00000000..967de65c --- /dev/null +++ b/internal/controller/informer/execution_informer.go @@ -0,0 +1,12 @@ +package informer + +import ( + "soarca/models/report" + + "github.com/google/uuid" +) + +type IExecutionInformer interface { + GetExecutionsIds() []string + GetExecutionReport(executionKey uuid.UUID) (report.ExecutionEntry, error) +} diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 727b751e..9c3a8672 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -186,7 +186,7 @@ func (cacheReporter *Cache) ReportStepEnd(executionId uuid.UUID, step cacao.Step return nil } -func (cacheReporter *Cache) GetExecutionsIDs() []string { +func (cacheReporter *Cache) GetExecutionsIds() []string { executions := make([]string, len(cacheReporter.fifoRegister)) _ = copy(executions, cacheReporter.fifoRegister) return executions diff --git a/routes/reporter/init.go b/routes/reporter/init.go new file mode 100644 index 00000000..29b1c513 --- /dev/null +++ b/routes/reporter/init.go @@ -0,0 +1,15 @@ +package reporter + +import ( + "reflect" + + "soarca/logger" +) + +var log *logger.Log + +type Empty struct{} + +func init() { + log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Info, "", logger.Json) +} diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go new file mode 100644 index 00000000..d97cff55 --- /dev/null +++ b/routes/reporter/reporter_api.go @@ -0,0 +1,60 @@ +package reporter + +import ( + "net/http" + "soarca/internal/controller/informer" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// A PlaybookController implements the playbook API endpoints is dependent on a database. +type executionInformer struct { + informer informer.IExecutionInformer +} + +// NewPlaybookController makes a new instance of playbookControler +func NewExecutionInformer(informer informer.IExecutionInformer) *executionInformer { + return &executionInformer{informer: informer} +} + +// getExecutions GET handler for obtaining all the executions that can be retrieved. +// Returns this to the gin context as a list if execution IDs in json format +// +// @Summary gets all the UUIDs for the executions that can be retireved +// @Schemes models.report.ExecutionEntry +// @Description return all stored executions +// @Tags reporter +// @Produce json +// @success 200 {array} string +// @error 400 +// @Router /report/ [GET] +func (executionInformer *executionInformer) getExecutions(g *gin.Context) { + executions := executionInformer.informer.GetExecutionsIds() + g.JSON(http.StatusOK, executions) +} + +func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { + id := g.Param("id") + log.Trace("Trying to obtain execution for id: ", id) + uuid := uuid.MustParse(id) + + executionEntry, err := executionInformer.informer.GetExecutionReport(uuid) + if err != nil { + log.Debug("Could not find execution for given id") + SendErrorResponse(g, http.StatusBadRequest, "Could not find execution for given ID", "GET /report/{id}") + return + } + + g.JSON(http.StatusOK, executionEntry) +} + +func SendErrorResponse(g *gin.Context, status int, message string, orginal_call string) { + msg := gin.H{ + "status": strconv.Itoa(status), + "message": message, + "original-call": orginal_call, + } + g.JSON(status, msg) +} diff --git a/routes/reporter/reporter_endpoints.go b/routes/reporter/reporter_endpoints.go new file mode 100644 index 00000000..8f7e3213 --- /dev/null +++ b/routes/reporter/reporter_endpoints.go @@ -0,0 +1,19 @@ +package reporter + +import ( + "soarca/internal/controller/informer" + + "github.com/gin-gonic/gin" +) + +// Main Router for the following endpoints: +// GET /reporter +// GET /reporter/{execution-id} +func Routes(route *gin.Engine, informer informer.IExecutionInformer) { + executionInformer := NewExecutionInformer(informer) + report := route.Group("/report") + { + report.GET("/", executionInformer.getExecutions) + report.GET("/:id", executionInformer.getExecutionReport) + } +} diff --git a/routes/router.go b/routes/router.go index c8459da2..a2f7652e 100644 --- a/routes/router.go +++ b/routes/router.go @@ -3,9 +3,11 @@ package routes import ( "soarca/internal/controller/database" "soarca/internal/controller/decomposer_controller" + "soarca/internal/controller/informer" coa_routes "soarca/routes/coa" operator "soarca/routes/operator" playbook_routes "soarca/routes/playbook" + reporter "soarca/routes/reporter" status "soarca/routes/status" swagger "soarca/routes/swagger" "soarca/routes/trigger" @@ -30,6 +32,12 @@ func Logging(app *gin.Engine) { // app.Use(middelware.LoggingMiddleware(log.Logger)) } +func Reporter(app *gin.Engine, informer informer.IExecutionInformer) error { + log.Trace("Setting up reporter routes") + reporter.Routes(app, informer) + return nil +} + func Api(app *gin.Engine, controller decomposer_controller.IController, ) error { @@ -37,9 +45,7 @@ func Api(app *gin.Engine, // gin.SetMode(gin.ReleaseMode) trigger_api := trigger.New(controller) - coa_routes.Routes(app) - status.Routes(app) operator.Routes(app) trigger.Routes(app, trigger_api) diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index 107a7b1b..e2c439d1 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -97,7 +97,7 @@ func TestReportWorkflowStartFirst(t *testing.T) { } exec, err := cacheReporter.GetExecutionReport(executionId0) - assert.Equal(t, expectedExecutions, cacheReporter.GetExecutionsIDs()) + assert.Equal(t, expectedExecutions, cacheReporter.GetExecutionsIds()) assert.Equal(t, expectedExecutionEntry.ExecutionId, exec.ExecutionId) assert.Equal(t, expectedExecutionEntry.PlaybookId, exec.PlaybookId) assert.Equal(t, expectedExecutionEntry.StepResults, exec.StepResults) @@ -251,13 +251,13 @@ func TestReportWorkflowStartFifo(t *testing.T) { t.Fail() } - assert.Equal(t, expectedExecutionsFull, cacheReporter.GetExecutionsIDs()) + assert.Equal(t, expectedExecutionsFull, cacheReporter.GetExecutionsIds()) err = cacheReporter.ReportWorkflowStart(executionId10, playbook) if err != nil { t.Fail() } - assert.Equal(t, expectedExecutionsFifo, cacheReporter.GetExecutionsIDs()) + assert.Equal(t, expectedExecutionsFifo, cacheReporter.GetExecutionsIds()) mock_time.AssertExpectations(t) } @@ -350,7 +350,7 @@ func TestReportWorkflowEnd(t *testing.T) { } exec, err := cacheReporter.GetExecutionReport(executionId0) - assert.Equal(t, expectedExecutions, cacheReporter.GetExecutionsIDs()) + assert.Equal(t, expectedExecutions, cacheReporter.GetExecutionsIds()) assert.Equal(t, expectedExecutionEntry.ExecutionId, exec.ExecutionId) assert.Equal(t, expectedExecutionEntry.PlaybookId, exec.PlaybookId) assert.Equal(t, expectedExecutionEntry.StepResults, exec.StepResults) From 0c8f3b3209b89167b108af7c21c20f4888baac23 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 09:46:22 +0200 Subject: [PATCH 03/40] add model for api response --- models/api/reporter.go | 42 +++++++++++++++++++++++++++++++++ routes/reporter/reporter_api.go | 13 +++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 models/api/reporter.go diff --git a/models/api/reporter.go b/models/api/reporter.go new file mode 100644 index 00000000..f10e46bf --- /dev/null +++ b/models/api/reporter.go @@ -0,0 +1,42 @@ +package api + +import ( + "soarca/models/cacao" +) + +type Status uint8 + +const ( + SuccessfullyExecuted = "successfully_executed" + Failed = "failed" + Ongoing = "ongoing" + ServerSideError = "server_side_error" + ClientSideError = "client_side_error" + TimeoutError = "timeout_error" + ExceptionConditionError = "exception_condition_error" +) + +type PlaybookExecutionReport struct { + ExecutionId string + PlaybookId string + Started string + Ended string + Status string + StatusText string + StepResults map[string]StepExecutionReport + Errors []string + requestInterval int +} + +type StepExecutionReport struct { + ExecutionId string + PlaybookId string + Started string + Ended string + Status string + StatusText string + Errors []string + Variables cacao.Variables + // Make sure we can have a playbookID for playbook actions, and also + // the execution ID for the invoked playbook +} diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index d97cff55..ecd7af7d 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -23,7 +23,7 @@ func NewExecutionInformer(informer informer.IExecutionInformer) *executionInform // Returns this to the gin context as a list if execution IDs in json format // // @Summary gets all the UUIDs for the executions that can be retireved -// @Schemes models.report.ExecutionEntry +// @Schemes []list // @Description return all stored executions // @Tags reporter // @Produce json @@ -35,6 +35,17 @@ func (executionInformer *executionInformer) getExecutions(g *gin.Context) { g.JSON(http.StatusOK, executions) } +// getExecutionReport GET handler for obtaining the information about an execution. +// Returns this to the gin context as a PlaybookExecutionReport object at soarca/model/api/reporter +// +// @Summary gets information about an ongoing playbook execution +// @Schemes soarca/models/api/PlaybookExecutionEntry +// @Description return execution information +// @Tags reporter +// @Produce json +// @success 200 PlaybookExecutionEntry +// @error 400 +// @Router /report/:id [GET] func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { id := g.Param("id") log.Trace("Trying to obtain execution for id: ", id) From b2c93736bc437f01ddd2ab5bc4bbed0643cd978d Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 10:35:37 +0200 Subject: [PATCH 04/40] add parsing from cache entry to reporter api results --- .../en/docs/core-components/api-reporter.md | 6 +-- models/api/reporter.go | 32 ++++++++++-- routes/reporter/reporter_api.go | 51 +++++++++++++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/docs/content/en/docs/core-components/api-reporter.md b/docs/content/en/docs/core-components/api-reporter.md index ee17ef05..be085d0d 100644 --- a/docs/content/en/docs/core-components/api-reporter.md +++ b/docs/content/en/docs/core-components/api-reporter.md @@ -63,7 +63,7 @@ Response data model: |field |content |type | description | | ----------------- | --------------------- | ----------------- | ----------- | -|type |execution-status |string |The type of this content +|type |"execution_status" |string |The type of this content |id |UUID |string |The id of the execution |execution_id |UUID |string |The id of the execution |playbook_id |UUID |string |The id of the CACAO playbook executed by the execution @@ -71,7 +71,7 @@ Response data model: |ended |timestamp |string |The time at which the execution of the playbook ended (if so) |status |execution-status-enum |string |The current [status](#execution-stataus) of the execution |status_text |explanation |string |A natural language explanation of the current status or related info -|errors |errors |list of string |Errors raised along the execution of the playbook at execution level +|error |error |string |Error raised along the execution of the playbook at execution level |step_results |step_results |dictionary |Map of step-id to related [step execution data](#step-execution-data) |request_interval |seconds |integer |Suggests the polling interval for the next request (default suggested is 5 seconds). @@ -85,7 +85,7 @@ Response data model: |ended |timestamp |string |The time at which the execution of the step ended (if so) |status |execution-status-enum |string |The current [status](#execution-stataus) of the execution of this step |status_text |explanation |string |A natural language explanation of the current status or related info -|errors |errors |list of string |Errors raised along the execution of the step +|error |error |string |Error raised along the execution of the step |variables |cacao variables |dictionary |Map of [cacao variables](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/cs01/security-playbooks-v2.0-cs01.html#_Toc152256555) handled in the step (both in and out) with current values and definitions ##### Execution stataus diff --git a/models/api/reporter.go b/models/api/reporter.go index f10e46bf..b596f704 100644 --- a/models/api/reporter.go +++ b/models/api/reporter.go @@ -1,7 +1,9 @@ package api import ( + "errors" "soarca/models/cacao" + cache_model "soarca/models/report" ) type Status uint8 @@ -17,6 +19,7 @@ const ( ) type PlaybookExecutionReport struct { + Type string ExecutionId string PlaybookId string Started string @@ -24,19 +27,40 @@ type PlaybookExecutionReport struct { Status string StatusText string StepResults map[string]StepExecutionReport - Errors []string - requestInterval int + Error string + RequestInterval int } type StepExecutionReport struct { ExecutionId string - PlaybookId string + StepId string Started string Ended string Status string StatusText string - Errors []string + Error string Variables cacao.Variables // Make sure we can have a playbookID for playbook actions, and also // the execution ID for the invoked playbook } + +func CacheStatusEnum2String(status cache_model.Status) (string, error) { + switch status { + case cache_model.SuccessfullyExecuted: + return SuccessfullyExecuted, nil + case cache_model.Failed: + return Failed, nil + case cache_model.Ongoing: + return Ongoing, nil + case cache_model.ServerSideError: + return ServerSideError, nil + case cache_model.ClientSideError: + return ClientSideError, nil + case cache_model.TimeoutError: + return TimeoutError, nil + case cache_model.ExceptionConditionError: + return ExceptionConditionError, nil + default: + return "", errors.New("unable to read execution information status") + } +} diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index ecd7af7d..b07cc18d 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -3,6 +3,8 @@ package reporter import ( "net/http" "soarca/internal/controller/informer" + api_model "soarca/models/api" + cache_model "soarca/models/report" "strconv" "github.com/gin-gonic/gin" @@ -69,3 +71,52 @@ func SendErrorResponse(g *gin.Context, status int, message string, orginal_call } g.JSON(status, msg) } + +func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.PlaybookExecutionReport, error) { + playbookStatus, err := api_model.CacheStatusEnum2String(cacheEntry.Status) + if err != nil { + return api_model.PlaybookExecutionReport{}, err + } + + stepResults, err := parseCacheStepEntries(cacheEntry.StepResults) + if err != nil { + return api_model.PlaybookExecutionReport{}, err + } + + executionReport := api_model.PlaybookExecutionReport{ + Type: "execution_status", + ExecutionId: cacheEntry.ExecutionId.String(), + PlaybookId: cacheEntry.PlaybookId, + Started: cacheEntry.Started.String(), + Ended: cacheEntry.Ended.String(), + Status: playbookStatus, + StatusText: cacheEntry.PlaybookResult.Error(), + Error: cacheEntry.PlaybookResult.Error(), + StepResults: stepResults, + RequestInterval: 5, + } + return executionReport, nil +} + +func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) (map[string]api_model.StepExecutionReport, error) { + parsedEntries := map[string]api_model.StepExecutionReport{} + for stepId, stepEntry := range cacheStepEntries { + + stepStatus, err := api_model.CacheStatusEnum2String(stepEntry.Status) + if err != nil { + return map[string]api_model.StepExecutionReport{}, err + } + + parsedEntries[stepId] = api_model.StepExecutionReport{ + ExecutionId: stepEntry.ExecutionId.String(), + StepId: stepEntry.StepId, + Started: stepEntry.Started.String(), + Ended: stepEntry.Ended.String(), + Status: stepStatus, + StatusText: stepEntry.Error.Error(), + Error: stepEntry.Error.Error(), + Variables: stepEntry.Variables, + } + } + return parsedEntries, nil +} From 607dcb82e6c3a72cb6541b8b709a112ed0377b11 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 10:41:01 +0200 Subject: [PATCH 05/40] enroll soarca-level cache object in both api and reporting --- internal/controller/controller.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index c33a6fcf..4c45c18b 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -51,6 +51,8 @@ type Controller struct { var mainController = Controller{} +var mainCache = cache.New(&timeUtil.Time{}) + func (controller *Controller) NewDecomposer() decomposer.IDecomposer { ssh := new(ssh.SshCapability) capabilities := map[string]capability.ICapability{ssh.GetType(): ssh} @@ -78,7 +80,10 @@ func (controller *Controller) NewDecomposer() decomposer.IDecomposer { } } + // NOTE: Enrolling mainCache by default as reporter reporter := reporter.New([]downstreamReporter.IDownStreamReporter{}) + downstreamReporters := []downstreamReporter.IDownStreamReporter{mainCache} + reporter.RegisterReporters(downstreamReporters) actionExecutor := action.New(capabilities, reporter) playbookActionExecutor := playbook_action.New(controller, controller, reporter) @@ -175,8 +180,7 @@ func initializeCore(app *gin.Engine) error { // NOTE: Assuming that the cache is the main information mediator for // the reporter API - informer := cache.New(&timeUtil.Time{}) - err = routes.Reporter(app, informer) + err = routes.Reporter(app, mainCache) if err != nil { log.Error(err) return err From de791724fdecc822017b451427b7b5178c54cf3a Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 10:46:07 +0200 Subject: [PATCH 06/40] add template report api testing --- routes/reporter/reporter_api.go | 51 ----------------- routes/reporter/reporter_parser.go | 55 +++++++++++++++++++ .../routes/reporter_api/reporter_api_test.go | 50 +++++++++++++++++ 3 files changed, 105 insertions(+), 51 deletions(-) create mode 100644 routes/reporter/reporter_parser.go create mode 100644 test/unittest/routes/reporter_api/reporter_api_test.go diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index b07cc18d..ecd7af7d 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -3,8 +3,6 @@ package reporter import ( "net/http" "soarca/internal/controller/informer" - api_model "soarca/models/api" - cache_model "soarca/models/report" "strconv" "github.com/gin-gonic/gin" @@ -71,52 +69,3 @@ func SendErrorResponse(g *gin.Context, status int, message string, orginal_call } g.JSON(status, msg) } - -func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.PlaybookExecutionReport, error) { - playbookStatus, err := api_model.CacheStatusEnum2String(cacheEntry.Status) - if err != nil { - return api_model.PlaybookExecutionReport{}, err - } - - stepResults, err := parseCacheStepEntries(cacheEntry.StepResults) - if err != nil { - return api_model.PlaybookExecutionReport{}, err - } - - executionReport := api_model.PlaybookExecutionReport{ - Type: "execution_status", - ExecutionId: cacheEntry.ExecutionId.String(), - PlaybookId: cacheEntry.PlaybookId, - Started: cacheEntry.Started.String(), - Ended: cacheEntry.Ended.String(), - Status: playbookStatus, - StatusText: cacheEntry.PlaybookResult.Error(), - Error: cacheEntry.PlaybookResult.Error(), - StepResults: stepResults, - RequestInterval: 5, - } - return executionReport, nil -} - -func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) (map[string]api_model.StepExecutionReport, error) { - parsedEntries := map[string]api_model.StepExecutionReport{} - for stepId, stepEntry := range cacheStepEntries { - - stepStatus, err := api_model.CacheStatusEnum2String(stepEntry.Status) - if err != nil { - return map[string]api_model.StepExecutionReport{}, err - } - - parsedEntries[stepId] = api_model.StepExecutionReport{ - ExecutionId: stepEntry.ExecutionId.String(), - StepId: stepEntry.StepId, - Started: stepEntry.Started.String(), - Ended: stepEntry.Ended.String(), - Status: stepStatus, - StatusText: stepEntry.Error.Error(), - Error: stepEntry.Error.Error(), - Variables: stepEntry.Variables, - } - } - return parsedEntries, nil -} diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go new file mode 100644 index 00000000..02506a9e --- /dev/null +++ b/routes/reporter/reporter_parser.go @@ -0,0 +1,55 @@ +package reporter + +import ( + api_model "soarca/models/api" + cache_model "soarca/models/report" +) + +func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.PlaybookExecutionReport, error) { + playbookStatus, err := api_model.CacheStatusEnum2String(cacheEntry.Status) + if err != nil { + return api_model.PlaybookExecutionReport{}, err + } + + stepResults, err := parseCacheStepEntries(cacheEntry.StepResults) + if err != nil { + return api_model.PlaybookExecutionReport{}, err + } + + executionReport := api_model.PlaybookExecutionReport{ + Type: "execution_status", + ExecutionId: cacheEntry.ExecutionId.String(), + PlaybookId: cacheEntry.PlaybookId, + Started: cacheEntry.Started.String(), + Ended: cacheEntry.Ended.String(), + Status: playbookStatus, + StatusText: cacheEntry.PlaybookResult.Error(), + Error: cacheEntry.PlaybookResult.Error(), + StepResults: stepResults, + RequestInterval: 5, + } + return executionReport, nil +} + +func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) (map[string]api_model.StepExecutionReport, error) { + parsedEntries := map[string]api_model.StepExecutionReport{} + for stepId, stepEntry := range cacheStepEntries { + + stepStatus, err := api_model.CacheStatusEnum2String(stepEntry.Status) + if err != nil { + return map[string]api_model.StepExecutionReport{}, err + } + + parsedEntries[stepId] = api_model.StepExecutionReport{ + ExecutionId: stepEntry.ExecutionId.String(), + StepId: stepEntry.StepId, + Started: stepEntry.Started.String(), + Ended: stepEntry.Ended.String(), + Status: stepStatus, + StatusText: stepEntry.Error.Error(), + Error: stepEntry.Error.Error(), + Variables: stepEntry.Variables, + } + } + return parsedEntries, nil +} diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go new file mode 100644 index 00000000..c53a764f --- /dev/null +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -0,0 +1,50 @@ +package reporter_test + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "soarca/internal/decomposer" + "soarca/models/cacao" + "soarca/routes/trigger" + mock_decomposer_controller "soarca/test/unittest/mocks/mock_controller/decomposer" + "soarca/test/unittest/mocks/mock_decomposer" + + "github.com/gin-gonic/gin" + "github.com/go-playground/assert/v2" +) + +func TestGetExecutions(t *testing.T) { + jsonFile, err := os.Open("../playbook.json") + if err != nil { + fmt.Println(err) + t.Fail() + } + defer jsonFile.Close() + byteValue, _ := io.ReadAll(jsonFile) + + app := gin.New() + gin.SetMode(gin.DebugMode) + mock_decomposer := new(mock_decomposer.Mock_Decomposer) + mock_controller := new(mock_decomposer_controller.Mock_Controller) + mock_controller.On("NewDecomposer").Return(mock_decomposer) + playbook := cacao.Decode(byteValue) + mock_decomposer.On("Execute", *playbook).Return(&decomposer.ExecutionDetails{}, nil) + + recorder := httptest.NewRecorder() + trigger_api := trigger.New(mock_controller) + trigger.Routes(app, trigger_api) + + request, err := http.NewRequest("POST", "/trigger/playbook", bytes.NewBuffer(byteValue)) + if err != nil { + t.Fail() + } + app.ServeHTTP(recorder, request) + assert.Equal(t, 200, recorder.Code) + mock_decomposer.AssertExpectations(t) +} From 73467f83a8cb849708270b1a5756da7f0c913681 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 10:50:27 +0200 Subject: [PATCH 07/40] rename report model to cache model --- internal/controller/controller.go | 2 +- .../controller/informer/execution_informer.go | 4 +- .../downstream_reporter/cache/cache.go | 48 +++++++++---------- models/api/reporter.go | 2 +- models/{report/report.go => cache/cache.go} | 2 +- routes/reporter/reporter_parser.go | 2 +- .../downstream_reporter/cache_test.go | 2 +- 7 files changed, 31 insertions(+), 31 deletions(-) rename models/{report/report.go => cache/cache.go} (98%) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 4c45c18b..81406d52 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -20,7 +20,7 @@ import ( "soarca/internal/fin/protocol" "soarca/internal/guid" "soarca/internal/reporter" - "soarca/internal/reporter/downstream_reporter/cache" + cache "soarca/internal/reporter/downstream_reporter/cache" "soarca/logger" "soarca/utils" httpUtil "soarca/utils/http" diff --git a/internal/controller/informer/execution_informer.go b/internal/controller/informer/execution_informer.go index 967de65c..f9851505 100644 --- a/internal/controller/informer/execution_informer.go +++ b/internal/controller/informer/execution_informer.go @@ -1,12 +1,12 @@ package informer import ( - "soarca/models/report" + "soarca/models/cache" "github.com/google/uuid" ) type IExecutionInformer interface { GetExecutionsIds() []string - GetExecutionReport(executionKey uuid.UUID) (report.ExecutionEntry, error) + GetExecutionReport(executionKey uuid.UUID) (cache.ExecutionEntry, error) } diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 9c3a8672..cd98b760 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -6,7 +6,7 @@ import ( "reflect" "soarca/logger" "soarca/models/cacao" - "soarca/models/report" + cache_report "soarca/models/cache" "soarca/utils" itime "soarca/utils/time" "strconv" @@ -28,45 +28,45 @@ const MaxSteps int = 10 type Cache struct { Size int timeUtil itime.ITime - Cache map[string]report.ExecutionEntry // Cached up to max - fifoRegister []string // Used for O(1) FIFO cache management + Cache map[string]cache_report.ExecutionEntry // Cached up to max + fifoRegister []string // Used for O(1) FIFO cache management } func New(timeUtil itime.ITime) *Cache { maxExecutions, _ := strconv.Atoi(utils.GetEnv("MAX_EXECUTIONS", strconv.Itoa(MaxExecutions))) return &Cache{ Size: maxExecutions, - Cache: make(map[string]report.ExecutionEntry), + Cache: make(map[string]cache_report.ExecutionEntry), timeUtil: timeUtil, } } -func (cacheReporter *Cache) getExecution(executionKey uuid.UUID) (report.ExecutionEntry, error) { +func (cacheReporter *Cache) getExecution(executionKey uuid.UUID) (cache_report.ExecutionEntry, error) { executionKeyStr := executionKey.String() executionEntry, ok := cacheReporter.Cache[executionKeyStr] if !ok { err := errors.New("execution is not in cache") log.Warning("execution is not in cache. consider increasing cache size.") - return report.ExecutionEntry{}, err + return cache_report.ExecutionEntry{}, err // TODO Retrieve from database } return executionEntry, nil } -func (cacheReporter *Cache) getExecutionStep(executionKey uuid.UUID, stepKey string) (report.StepResult, error) { +func (cacheReporter *Cache) getExecutionStep(executionKey uuid.UUID, stepKey string) (cache_report.StepResult, error) { executionEntry, err := cacheReporter.getExecution(executionKey) if err != nil { - return report.StepResult{}, err + return cache_report.StepResult{}, err } executionStep, ok := executionEntry.StepResults[stepKey] if !ok { err := errors.New("execution step is not in cache") - return report.StepResult{}, err + return cache_report.StepResult{}, err } return executionStep, nil } // Adding executions in FIFO logic -func (cacheReporter *Cache) addExecution(newExecutionEntry report.ExecutionEntry) error { +func (cacheReporter *Cache) addExecution(newExecutionEntry cache_report.ExecutionEntry) error { if !(len(cacheReporter.fifoRegister) == len(cacheReporter.Cache)) { return errors.New("cache fifo register and content are desynchronized") @@ -90,13 +90,13 @@ func (cacheReporter *Cache) addExecution(newExecutionEntry report.ExecutionEntry } func (cacheReporter *Cache) ReportWorkflowStart(executionId uuid.UUID, playbook cacao.Playbook) error { - newExecutionEntry := report.ExecutionEntry{ + newExecutionEntry := cache_report.ExecutionEntry{ ExecutionId: executionId, PlaybookId: playbook.ID, Started: cacheReporter.timeUtil.Now(), Ended: time.Time{}, - StepResults: map[string]report.StepResult{}, - Status: report.Ongoing, + StepResults: map[string]cache_report.StepResult{}, + Status: cache_report.Ongoing, } err := cacheReporter.addExecution(newExecutionEntry) if err != nil { @@ -114,9 +114,9 @@ func (cacheReporter *Cache) ReportWorkflowEnd(executionId uuid.UUID, playbook ca if workflowError != nil { executionEntry.PlaybookResult = workflowError - executionEntry.Status = report.Failed + executionEntry.Status = cache_report.Failed } else { - executionEntry.Status = report.SuccessfullyExecuted + executionEntry.Status = cache_report.SuccessfullyExecuted } executionEntry.Ended = cacheReporter.timeUtil.Now() cacheReporter.Cache[executionId.String()] = executionEntry @@ -130,7 +130,7 @@ func (cacheReporter *Cache) ReportStepStart(executionId uuid.UUID, step cacao.St return err } - if executionEntry.Status != report.Ongoing { + if executionEntry.Status != cache_report.Ongoing { return errors.New("trying to report on the execution of a step for an already reported completed or failed execution") } @@ -141,13 +141,13 @@ func (cacheReporter *Cache) ReportStepStart(executionId uuid.UUID, step cacao.St log.Warning("a step execution was already reported for this step. overwriting.") } - newStepEntry := report.StepResult{ + newStepEntry := cache_report.StepResult{ ExecutionId: executionId, StepId: step.ID, Started: cacheReporter.timeUtil.Now(), Ended: time.Time{}, Variables: variables, - Status: report.Ongoing, + Status: cache_report.Ongoing, Error: nil, } executionEntry.StepResults[step.ID] = newStepEntry @@ -160,7 +160,7 @@ func (cacheReporter *Cache) ReportStepEnd(executionId uuid.UUID, step cacao.Step return err } - if executionEntry.Status != report.Ongoing { + if executionEntry.Status != cache_report.Ongoing { return errors.New("trying to report on the execution of a step for an already reported completed or failed execution") } @@ -169,15 +169,15 @@ func (cacheReporter *Cache) ReportStepEnd(executionId uuid.UUID, step cacao.Step return err } - if executionStepResult.Status != report.Ongoing { + if executionStepResult.Status != cache_report.Ongoing { return errors.New("trying to report on the execution of a step that was already reported completed or failed") } if stepError != nil { executionStepResult.Error = stepError - executionStepResult.Status = report.ServerSideError + executionStepResult.Status = cache_report.ServerSideError } else { - executionStepResult.Status = report.SuccessfullyExecuted + executionStepResult.Status = cache_report.SuccessfullyExecuted } executionStepResult.Ended = cacheReporter.timeUtil.Now() executionStepResult.Variables = returnVars @@ -192,10 +192,10 @@ func (cacheReporter *Cache) GetExecutionsIds() []string { return executions } -func (cacheReporter *Cache) GetExecutionReport(executionKey uuid.UUID) (report.ExecutionEntry, error) { +func (cacheReporter *Cache) GetExecutionReport(executionKey uuid.UUID) (cache_report.ExecutionEntry, error) { executionEntry, err := cacheReporter.getExecution(executionKey) if err != nil { - return report.ExecutionEntry{}, err + return cache_report.ExecutionEntry{}, err } report := executionEntry diff --git a/models/api/reporter.go b/models/api/reporter.go index b596f704..2f001453 100644 --- a/models/api/reporter.go +++ b/models/api/reporter.go @@ -3,7 +3,7 @@ package api import ( "errors" "soarca/models/cacao" - cache_model "soarca/models/report" + cache_model "soarca/models/cache" ) type Status uint8 diff --git a/models/report/report.go b/models/cache/cache.go similarity index 98% rename from models/report/report.go rename to models/cache/cache.go index a261a1dd..f2187b7f 100644 --- a/models/report/report.go +++ b/models/cache/cache.go @@ -1,4 +1,4 @@ -package report +package cache import ( "soarca/models/cacao" diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index 02506a9e..d613e0f4 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -2,7 +2,7 @@ package reporter import ( api_model "soarca/models/api" - cache_model "soarca/models/report" + cache_model "soarca/models/cache" ) func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.PlaybookExecutionReport, error) { diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index e2c439d1..f917df3b 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -4,7 +4,7 @@ import ( "errors" "soarca/internal/reporter/downstream_reporter/cache" "soarca/models/cacao" - "soarca/models/report" + report "soarca/models/cache" "soarca/test/unittest/mocks/mock_utils/time" "testing" "time" From 4b0a21a7cdca42f3111dbeb0b229f3ac5411bdb4 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 12:16:31 +0200 Subject: [PATCH 08/40] add get executions base test --- routes/reporter/reporter_endpoints.go | 2 +- .../routes/reporter_api/reporter_api_test.go | 132 +++++++++++++++--- 2 files changed, 110 insertions(+), 24 deletions(-) diff --git a/routes/reporter/reporter_endpoints.go b/routes/reporter/reporter_endpoints.go index 8f7e3213..a8d374ca 100644 --- a/routes/reporter/reporter_endpoints.go +++ b/routes/reporter/reporter_endpoints.go @@ -11,7 +11,7 @@ import ( // GET /reporter/{execution-id} func Routes(route *gin.Engine, informer informer.IExecutionInformer) { executionInformer := NewExecutionInformer(informer) - report := route.Group("/report") + report := route.Group("/reporter") { report.GET("/", executionInformer.getExecutions) report.GET("/:id", executionInformer.getExecutionReport) diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index c53a764f..528cbc83 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -1,50 +1,136 @@ -package reporter_test +package reporter_api_test import ( - "bytes" - "fmt" - "io" + "encoding/json" "net/http" "net/http/httptest" - "os" + "soarca/internal/reporter/downstream_reporter/cache" + "soarca/models/cacao" + "soarca/routes/reporter" "testing" + "time" - "soarca/internal/decomposer" - "soarca/models/cacao" - "soarca/routes/trigger" - mock_decomposer_controller "soarca/test/unittest/mocks/mock_controller/decomposer" - "soarca/test/unittest/mocks/mock_decomposer" + "github.com/google/uuid" "github.com/gin-gonic/gin" "github.com/go-playground/assert/v2" + + mock_time "soarca/test/unittest/mocks/mock_utils/time" ) func TestGetExecutions(t *testing.T) { - jsonFile, err := os.Open("../playbook.json") + // Create real cache, create real reporter api object + // Do executions, test retrieval via api + + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time) + + 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}, + } + executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + expectedExecutions := []string{ + "6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "6ba7b810-9dad-11d1-80b4-00c04fd430c1", + "6ba7b810-9dad-11d1-80b4-00c04fd430c2", + } + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportWorkflowStart(executionId1, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportWorkflowStart(executionId2, playbook) if err != nil { - fmt.Println(err) t.Fail() } - defer jsonFile.Close() - byteValue, _ := io.ReadAll(jsonFile) app := gin.New() gin.SetMode(gin.DebugMode) - mock_decomposer := new(mock_decomposer.Mock_Decomposer) - mock_controller := new(mock_decomposer_controller.Mock_Controller) - mock_controller.On("NewDecomposer").Return(mock_decomposer) - playbook := cacao.Decode(byteValue) - mock_decomposer.On("Execute", *playbook).Return(&decomposer.ExecutionDetails{}, nil) recorder := httptest.NewRecorder() - trigger_api := trigger.New(mock_controller) - trigger.Routes(app, trigger_api) + reporter.Routes(app, cacheReporter) - request, err := http.NewRequest("POST", "/trigger/playbook", bytes.NewBuffer(byteValue)) + request, err := http.NewRequest("GET", "/reporter/", nil) if err != nil { t.Fail() } + app.ServeHTTP(recorder, request) + expectedByte, err := json.Marshal(expectedExecutions) + if err != nil { + t.Log("failed to decode expected struct to json") + t.Fail() + } + expectedString := string(expectedByte) + assert.Equal(t, expectedString, recorder.Body.String()) assert.Equal(t, 200, recorder.Code) - mock_decomposer.AssertExpectations(t) + + mock_time.AssertExpectations(t) } + +//fmt.Sprintf("/reporter/%s", executionId) From 35ad402f184d52eee238ab5b36b37d43c9d06633 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 13:47:03 +0200 Subject: [PATCH 09/40] add reporter api testing --- routes/reporter/reporter_api.go | 8 +- routes/reporter/reporter_parser.go | 20 ++- .../routes/reporter_api/reporter_api_test.go | 160 +++++++++++++++++- 3 files changed, 182 insertions(+), 6 deletions(-) diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index ecd7af7d..faffd01d 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -58,7 +58,13 @@ func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { return } - g.JSON(http.StatusOK, executionEntry) + executionEntryParsed, err := parseCachePlaybookEntry(executionEntry) + if err != nil { + log.Debug("Could not parse entry to reporter result model") + SendErrorResponse(g, http.StatusInternalServerError, "Could not parse execution report", "GET /report/{id}") + return + } + g.JSON(http.StatusOK, executionEntryParsed) } func SendErrorResponse(g *gin.Context, status int, message string, orginal_call string) { diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index d613e0f4..058ed473 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -11,6 +11,12 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P return api_model.PlaybookExecutionReport{}, err } + playbookError := cacheEntry.PlaybookResult + playbookErrorStr := "" + if playbookError != nil { + playbookErrorStr = playbookError.Error() + } + stepResults, err := parseCacheStepEntries(cacheEntry.StepResults) if err != nil { return api_model.PlaybookExecutionReport{}, err @@ -23,8 +29,8 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P Started: cacheEntry.Started.String(), Ended: cacheEntry.Ended.String(), Status: playbookStatus, - StatusText: cacheEntry.PlaybookResult.Error(), - Error: cacheEntry.PlaybookResult.Error(), + StatusText: playbookErrorStr, + Error: playbookErrorStr, StepResults: stepResults, RequestInterval: 5, } @@ -40,14 +46,20 @@ func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) ( return map[string]api_model.StepExecutionReport{}, err } + stepError := stepEntry.Error + stepErrorStr := "" + if stepError != nil { + stepErrorStr = stepError.Error() + } + parsedEntries[stepId] = api_model.StepExecutionReport{ ExecutionId: stepEntry.ExecutionId.String(), StepId: stepEntry.StepId, Started: stepEntry.Started.String(), Ended: stepEntry.Ended.String(), Status: stepStatus, - StatusText: stepEntry.Error.Error(), - Error: stepEntry.Error.Error(), + StatusText: stepErrorStr, + Error: stepErrorStr, Variables: stepEntry.Variables, } } diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 528cbc83..e0b0492c 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -2,9 +2,11 @@ package reporter_api_test import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "soarca/internal/reporter/downstream_reporter/cache" + api_model "soarca/models/api" "soarca/models/cacao" "soarca/routes/reporter" "testing" @@ -100,6 +102,7 @@ func TestGetExecutions(t *testing.T) { if err != nil { t.Fail() } + err = cacheReporter.ReportWorkflowStart(executionId1, playbook) if err != nil { t.Fail() @@ -133,4 +136,159 @@ func TestGetExecutions(t *testing.T) { mock_time.AssertExpectations(t) } -//fmt.Sprintf("/reporter/%s", executionId) +func TestGetExecutionReport(t *testing.T) { + // Create real cache, create real reporter api object + // Do executions, test retrieval via api + + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time) + + 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}, + } + executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepStart(executionId0, step1, cacao.NewVariables(expectedVariables)) + if err != nil { + t.Fail() + } + + err = cacheReporter.ReportWorkflowStart(executionId1, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportWorkflowStart(executionId2, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepEnd(executionId0, step1, cacao.NewVariables(expectedVariables), nil) + if err != nil { + t.Fail() + } + + app := gin.New() + gin.SetMode(gin.DebugMode) + + recorder := httptest.NewRecorder() + reporter.Routes(app, cacheReporter) + + expected := `{ + "Type":"execution_status", + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "PlaybookId":"test", + "Started":"2014-11-12 11:45:26.371 +0000 UTC", + "Ended":"0001-01-01 00:00:00 +0000 UTC", + "Status":"ongoing", + "StatusText":"", + "StepResults":{ + "action--test":{ + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "StepId":"action--test", + "Started":"2014-11-12 11:45:26.371 +0000 UTC", + "Ended":"2014-11-12 11:45:26.371 +0000 UTC", + "Status":"successfully_executed", + "StatusText":"", + "Error":"", + "Variables":{ + "var1":{ + "type":"string", + "name":"var1", + "value":"testing" + } + } + } + }, + "Error":"", + "RequestInterval":5 + }` + expectedData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal([]byte(expected), &expectedData) + if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") + t.Fail() + } + + request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) + if err != nil { + t.Log(err) + t.Fail() + } + app.ServeHTTP(recorder, request) + + receivedData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal([]byte(recorder.Body.String()), &receivedData) + if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") + t.Fail() + } + + assert.Equal(t, expectedData, receivedData) + + mock_time.AssertExpectations(t) +} From 16104405c21fb96d3e495a00b4658e418ce7ce29 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 14:17:18 +0200 Subject: [PATCH 10/40] split unittest and integration test on reporter api --- test/integration/api/reporter_api_test.go | 291 ++++++++++++++++++ .../mock_reporter/mock_downstream_reporter.go | 12 + .../routes/reporter_api/reporter_api_test.go | 258 ++++------------ 3 files changed, 356 insertions(+), 205 deletions(-) create mode 100644 test/integration/api/reporter_api_test.go diff --git a/test/integration/api/reporter_api_test.go b/test/integration/api/reporter_api_test.go new file mode 100644 index 00000000..42672894 --- /dev/null +++ b/test/integration/api/reporter_api_test.go @@ -0,0 +1,291 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "soarca/internal/reporter/downstream_reporter/cache" + api_model "soarca/models/api" + "soarca/models/cacao" + "soarca/routes/reporter" + mock_time "soarca/test/unittest/mocks/mock_utils/time" + "testing" + "time" + + "github.com/google/uuid" + + "github.com/gin-gonic/gin" + "github.com/go-playground/assert/v2" +) + +func TestGetExecutions(t *testing.T) { + + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time) + + 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}, + } + executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + expectedExecutions := []string{ + "6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "6ba7b810-9dad-11d1-80b4-00c04fd430c1", + "6ba7b810-9dad-11d1-80b4-00c04fd430c2", + } + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + + err = cacheReporter.ReportWorkflowStart(executionId1, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportWorkflowStart(executionId2, playbook) + if err != nil { + t.Fail() + } + + app := gin.New() + gin.SetMode(gin.DebugMode) + + recorder := httptest.NewRecorder() + reporter.Routes(app, cacheReporter) + + request, err := http.NewRequest("GET", "/reporter/", nil) + if err != nil { + t.Fail() + } + + app.ServeHTTP(recorder, request) + expectedByte, err := json.Marshal(expectedExecutions) + if err != nil { + t.Log("failed to decode expected struct to json") + t.Fail() + } + expectedString := string(expectedByte) + assert.Equal(t, expectedString, recorder.Body.String()) + assert.Equal(t, 200, recorder.Code) + + mock_time.AssertExpectations(t) +} + +func TestGetExecutionReport(t *testing.T) { + // Create real cache, create real reporter api object + // Do executions, test retrieval via api + + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time) + + 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}, + } + executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepStart(executionId0, step1, cacao.NewVariables(expectedVariables)) + if err != nil { + t.Fail() + } + + err = cacheReporter.ReportWorkflowStart(executionId1, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportWorkflowStart(executionId2, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepEnd(executionId0, step1, cacao.NewVariables(expectedVariables), nil) + if err != nil { + t.Fail() + } + + app := gin.New() + gin.SetMode(gin.DebugMode) + + recorder := httptest.NewRecorder() + reporter.Routes(app, cacheReporter) + + expected := `{ + "Type":"execution_status", + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "PlaybookId":"test", + "Started":"2014-11-12 11:45:26.371 +0000 UTC", + "Ended":"0001-01-01 00:00:00 +0000 UTC", + "Status":"ongoing", + "StatusText":"", + "StepResults":{ + "action--test":{ + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "StepId":"action--test", + "Started":"2014-11-12 11:45:26.371 +0000 UTC", + "Ended":"2014-11-12 11:45:26.371 +0000 UTC", + "Status":"successfully_executed", + "StatusText":"", + "Error":"", + "Variables":{ + "var1":{ + "type":"string", + "name":"var1", + "value":"testing" + } + } + } + }, + "Error":"", + "RequestInterval":5 + }` + expectedData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal([]byte(expected), &expectedData) + if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") + t.Fail() + } + + request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) + if err != nil { + t.Log(err) + t.Fail() + } + app.ServeHTTP(recorder, request) + + receivedData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal([]byte(recorder.Body.String()), &receivedData) + if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") + t.Fail() + } + + assert.Equal(t, expectedData, receivedData) + + mock_time.AssertExpectations(t) +} diff --git a/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go index 93149877..3292bba9 100644 --- a/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go +++ b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go @@ -3,6 +3,8 @@ package mock_reporter import ( "soarca/models/cacao" + cache_report "soarca/models/cache" + "github.com/google/uuid" "github.com/stretchr/testify/mock" ) @@ -28,3 +30,13 @@ func (reporter *Mock_Downstream_Reporter) ReportStepEnd(executionId uuid.UUID, s args := reporter.Called(executionId, step, stepResults, stepError) return args.Error(0) } + +func (reporter *Mock_Downstream_Reporter) GetExecutionsIds() []string { + args := reporter.Called() + return args.Get(0).([]string) +} + +func (reporter *Mock_Downstream_Reporter) GetExecutionReport(executionKey uuid.UUID) (cache_report.ExecutionEntry, error) { + args := reporter.Called(executionKey) + return args.Get(0).(cache_report.ExecutionEntry), args.Error(1) +} diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index e0b0492c..564df202 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -5,118 +5,27 @@ import ( "fmt" "net/http" "net/http/httptest" - "soarca/internal/reporter/downstream_reporter/cache" api_model "soarca/models/api" - "soarca/models/cacao" + cache_model "soarca/models/cache" "soarca/routes/reporter" + mock_ds_reporter "soarca/test/unittest/mocks/mock_reporter" "testing" - "time" "github.com/google/uuid" "github.com/gin-gonic/gin" "github.com/go-playground/assert/v2" - - mock_time "soarca/test/unittest/mocks/mock_utils/time" ) -func TestGetExecutions(t *testing.T) { - // Create real cache, create real reporter api object - // Do executions, test retrieval via api - - mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) - - 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}, - } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") - executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") - executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") - - layout := "2006-01-02T15:04:05.000Z" - str := "2014-11-12T11:45:26.371Z" - timeNow, _ := time.Parse(layout, str) - mock_time.On("Now").Return(timeNow) - - expectedExecutions := []string{ - "6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "6ba7b810-9dad-11d1-80b4-00c04fd430c1", - "6ba7b810-9dad-11d1-80b4-00c04fd430c2", - } - - err := cacheReporter.ReportWorkflowStart(executionId0, playbook) - if err != nil { - t.Fail() - } - - err = cacheReporter.ReportWorkflowStart(executionId1, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId2, playbook) - if err != nil { - t.Fail() - } +func TestGetExecutionsInvocation(t *testing.T) { + mock_cache_reporter := &mock_ds_reporter.Mock_Downstream_Reporter{} + mock_cache_reporter.On("GetExecutionsIds").Return([]string{}) app := gin.New() gin.SetMode(gin.DebugMode) recorder := httptest.NewRecorder() - reporter.Routes(app, cacheReporter) + reporter.Routes(app, mock_cache_reporter) request, err := http.NewRequest("GET", "/reporter/", nil) if err != nil { @@ -124,119 +33,66 @@ func TestGetExecutions(t *testing.T) { } app.ServeHTTP(recorder, request) - expectedByte, err := json.Marshal(expectedExecutions) - if err != nil { - t.Log("failed to decode expected struct to json") - t.Fail() - } - expectedString := string(expectedByte) + expectedString := "[]" assert.Equal(t, expectedString, recorder.Body.String()) assert.Equal(t, 200, recorder.Code) - mock_time.AssertExpectations(t) + mock_cache_reporter.AssertExpectations(t) } -func TestGetExecutionReport(t *testing.T) { - // Create real cache, create real reporter api object - // Do executions, test retrieval via api - - mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) - - 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", - } +func TestGetExecutionReportInvocation(t *testing.T) { + mock_cache_reporter := &mock_ds_reporter.Mock_Downstream_Reporter{} + app := gin.New() + gin.SetMode(gin.DebugMode) - 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}, + recorder := httptest.NewRecorder() + reporter.Routes(app, mock_cache_reporter) - Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, - } executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") - executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") - executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") - - layout := "2006-01-02T15:04:05.000Z" - str := "2014-11-12T11:45:26.371Z" - timeNow, _ := time.Parse(layout, str) - mock_time.On("Now").Return(timeNow) - err := cacheReporter.ReportWorkflowStart(executionId0, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportStepStart(executionId0, step1, cacao.NewVariables(expectedVariables)) + expectedCache := `{ + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "PlaybookId":"test", + "Started":"2014-11-12T11:45:26.371Z", + "Ended":"0001-01-01T00:00:00Z", + "StepResults":{ + "action--test":{ + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "StepId":"action--test", + "Started":"2014-11-12T11:45:26.371Z", + "Ended":"2014-11-12T11:45:26.371Z", + "Variables":{ + "var1":{ + "type":"string", + "name":"var1", + "value":"testing" + } + }, + "Status":0, + "Error":null + } + }, + "PlaybookResult":null, + "Status":2 + }` + expectedCacheData := cache_model.ExecutionEntry{} + err := json.Unmarshal([]byte(expectedCache), &expectedCacheData) if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") t.Fail() } - err = cacheReporter.ReportWorkflowStart(executionId1, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId2, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportStepEnd(executionId0, step1, cacao.NewVariables(expectedVariables), nil) + mock_cache_reporter.On("GetExecutionReport", executionId0).Return(expectedCacheData, nil) + + request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) if err != nil { + t.Log(err) t.Fail() } + app.ServeHTTP(recorder, request) - app := gin.New() - gin.SetMode(gin.DebugMode) - - recorder := httptest.NewRecorder() - reporter.Routes(app, cacheReporter) - - expected := `{ + expectedResponse := `{ "Type":"execution_status", "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", "PlaybookId":"test", @@ -265,21 +121,14 @@ func TestGetExecutionReport(t *testing.T) { "Error":"", "RequestInterval":5 }` - expectedData := api_model.PlaybookExecutionReport{} - err = json.Unmarshal([]byte(expected), &expectedData) + expectedResponseData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal([]byte(expectedResponse), &expectedResponseData) if err != nil { t.Log(err) t.Log("Could not parse data to JSON") t.Fail() } - request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) - if err != nil { - t.Log(err) - t.Fail() - } - app.ServeHTTP(recorder, request) - receivedData := api_model.PlaybookExecutionReport{} err = json.Unmarshal([]byte(recorder.Body.String()), &receivedData) if err != nil { @@ -288,7 +137,6 @@ func TestGetExecutionReport(t *testing.T) { t.Fail() } - assert.Equal(t, expectedData, receivedData) - - mock_time.AssertExpectations(t) + assert.Equal(t, expectedResponseData, receivedData) + mock_cache_reporter.AssertExpectations(t) } From efe5afdb0f0784a81da4a9dce9c2808e178d2398 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 14:24:48 +0200 Subject: [PATCH 11/40] trying to solve lint error cannot parse response comment --- models/api/reporter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/api/reporter.go b/models/api/reporter.go index 2f001453..cf91d48c 100644 --- a/models/api/reporter.go +++ b/models/api/reporter.go @@ -39,7 +39,7 @@ type StepExecutionReport struct { Status string StatusText string Error string - Variables cacao.Variables + Variables map[string]cacao.Variable // Make sure we can have a playbookID for playbook actions, and also // the execution ID for the invoked playbook } From df5e1e22db51415e28c8673066a32c301ad8a8de Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 14:30:06 +0200 Subject: [PATCH 12/40] solving lint error due to scheme specification --- routes/reporter/reporter_api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index faffd01d..4b67ae49 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -39,11 +39,11 @@ func (executionInformer *executionInformer) getExecutions(g *gin.Context) { // Returns this to the gin context as a PlaybookExecutionReport object at soarca/model/api/reporter // // @Summary gets information about an ongoing playbook execution -// @Schemes soarca/models/api/PlaybookExecutionEntry +// @Schemes soarca/models/api/PlaybookExecutionReport // @Description return execution information // @Tags reporter // @Produce json -// @success 200 PlaybookExecutionEntry +// @success 200 PlaybookExecutionReport // @error 400 // @Router /report/:id [GET] func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { From 6d25758722a754000039d76802239fc17ca6fb44 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 14:31:05 +0200 Subject: [PATCH 13/40] remove schemes specification --- routes/reporter/reporter_api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 4b67ae49..31435712 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -23,7 +23,7 @@ func NewExecutionInformer(informer informer.IExecutionInformer) *executionInform // Returns this to the gin context as a list if execution IDs in json format // // @Summary gets all the UUIDs for the executions that can be retireved -// @Schemes []list +// @Schemes // @Description return all stored executions // @Tags reporter // @Produce json @@ -39,7 +39,7 @@ func (executionInformer *executionInformer) getExecutions(g *gin.Context) { // Returns this to the gin context as a PlaybookExecutionReport object at soarca/model/api/reporter // // @Summary gets information about an ongoing playbook execution -// @Schemes soarca/models/api/PlaybookExecutionReport +// @Schemes // @Description return execution information // @Tags reporter // @Produce json From 9ef4e67170759f0dd39df89385c26f6dcd235813 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 14:33:32 +0200 Subject: [PATCH 14/40] remove scheme from success specification --- routes/reporter/reporter_api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 31435712..dae2b626 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -43,7 +43,7 @@ func (executionInformer *executionInformer) getExecutions(g *gin.Context) { // @Description return execution information // @Tags reporter // @Produce json -// @success 200 PlaybookExecutionReport +// @success 200 // @error 400 // @Router /report/:id [GET] func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { From b650e836da176bfb5cb305f56f1efe9020d6240a Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 14:35:36 +0200 Subject: [PATCH 15/40] add right scheme to response --- routes/reporter/reporter_api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index dae2b626..314bf518 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -43,7 +43,7 @@ func (executionInformer *executionInformer) getExecutions(g *gin.Context) { // @Description return execution information // @Tags reporter // @Produce json -// @success 200 +// @success 200 {object} api.PlaybookExecutionReport // @error 400 // @Router /report/:id [GET] func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { From de8265449e5126371a599737b0e37c66a78c9bdb Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 7 May 2024 14:41:18 +0200 Subject: [PATCH 16/40] fix lint error check in controller and recorder body bytes in tests --- internal/controller/controller.go | 5 ++++- test/integration/api/reporter_api_test.go | 2 +- test/unittest/routes/reporter_api/reporter_api_test.go | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 81406d52..2287b146 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -83,7 +83,10 @@ func (controller *Controller) NewDecomposer() decomposer.IDecomposer { // NOTE: Enrolling mainCache by default as reporter reporter := reporter.New([]downstreamReporter.IDownStreamReporter{}) downstreamReporters := []downstreamReporter.IDownStreamReporter{mainCache} - reporter.RegisterReporters(downstreamReporters) + err := reporter.RegisterReporters(downstreamReporters) + if err != nil { + log.Error("could not load main Cache as reporter for decomposer and executors") + } actionExecutor := action.New(capabilities, reporter) playbookActionExecutor := playbook_action.New(controller, controller, reporter) diff --git a/test/integration/api/reporter_api_test.go b/test/integration/api/reporter_api_test.go index 42672894..c174107e 100644 --- a/test/integration/api/reporter_api_test.go +++ b/test/integration/api/reporter_api_test.go @@ -278,7 +278,7 @@ func TestGetExecutionReport(t *testing.T) { app.ServeHTTP(recorder, request) receivedData := api_model.PlaybookExecutionReport{} - err = json.Unmarshal([]byte(recorder.Body.String()), &receivedData) + err = json.Unmarshal(recorder.Body.Bytes(), &receivedData) if err != nil { t.Log(err) t.Log("Could not parse data to JSON") diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 564df202..6c734861 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -130,7 +130,7 @@ func TestGetExecutionReportInvocation(t *testing.T) { } receivedData := api_model.PlaybookExecutionReport{} - err = json.Unmarshal([]byte(recorder.Body.String()), &receivedData) + err = json.Unmarshal(recorder.Body.Bytes(), &receivedData) if err != nil { t.Log(err) t.Log("Could not parse data to JSON") From 824b88ca47ae65ab6d4c7eeaf69f83521ee03d1d Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 10:42:29 +0200 Subject: [PATCH 17/40] change reporter documentation according to review --- docs/content/en/docs/core-components/api-reporter.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/content/en/docs/core-components/api-reporter.md b/docs/content/en/docs/core-components/api-reporter.md index be085d0d..52ad0806 100644 --- a/docs/content/en/docs/core-components/api-reporter.md +++ b/docs/content/en/docs/core-components/api-reporter.md @@ -39,7 +39,9 @@ None @startjson [ { - "executions": ["execution-id", "..."] + "executions": [ + {"execution_id" : "1", "playbook_id" : "a", "started" : "", "..." : "..."}, + "..."] } ] @endjson @@ -79,7 +81,6 @@ Response data model: ##### Step execution data |field |content |type | description | | ----------------- | --------------------- | ----------------- | ----------- | -|execution_id |UUID |string |The id of the execution of the playbook where the step resides |step_id |UUID |string |The id of the step being executed |started |timestamp |string |The time at which the execution of the step started |ended |timestamp |string |The time at which the execution of the step ended (if so) From 7f58da34cb6f7fdfd8336be2cbea106cb4e66e21 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 12:25:16 +0200 Subject: [PATCH 18/40] change get executions to return full executions information --- .../controller/informer/execution_informer.go | 2 +- .../downstream_reporter/cache/cache.go | 42 +++++- routes/reporter/reporter_api.go | 7 +- test/integration/api/reporter_api_test.go | 27 +++- .../mock_reporter/mock_downstream_reporter.go | 10 +- .../downstream_reporter/cache_test.go | 126 ++++++++++++------ .../routes/reporter_api/reporter_api_test.go | 2 +- 7 files changed, 157 insertions(+), 59 deletions(-) diff --git a/internal/controller/informer/execution_informer.go b/internal/controller/informer/execution_informer.go index f9851505..85d801ff 100644 --- a/internal/controller/informer/execution_informer.go +++ b/internal/controller/informer/execution_informer.go @@ -7,6 +7,6 @@ import ( ) type IExecutionInformer interface { - GetExecutionsIds() []string + GetExecutions() ([]cache.ExecutionEntry, error) GetExecutionReport(executionKey uuid.UUID) (cache.ExecutionEntry, error) } diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index cd98b760..6ce0d5f8 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -1,6 +1,7 @@ package cache import ( + "encoding/json" "errors" "fmt" "reflect" @@ -134,8 +135,6 @@ func (cacheReporter *Cache) ReportStepStart(executionId uuid.UUID, step cacao.St return errors.New("trying to report on the execution of a step for an already reported completed or failed execution") } - fmt.Println(executionEntry) - _, alreadyThere := executionEntry.StepResults[step.ID] if alreadyThere { log.Warning("a step execution was already reported for this step. overwriting.") @@ -186,10 +185,18 @@ func (cacheReporter *Cache) ReportStepEnd(executionId uuid.UUID, step cacao.Step return nil } -func (cacheReporter *Cache) GetExecutionsIds() []string { - executions := make([]string, len(cacheReporter.fifoRegister)) - _ = copy(executions, cacheReporter.fifoRegister) - return executions +func (cacheReporter *Cache) GetExecutions() ([]cache_report.ExecutionEntry, error) { + executions := make([]cache_report.ExecutionEntry, 0) + // NOTE: fetched via fifo register key reference as is ordered array, + // needed to test and report back ordered executions stored + for _, executionEntryKey := range cacheReporter.fifoRegister { + entry, err := cacheReporter.copyExecutionEntry(executionEntryKey) + if err != nil { + return []cache_report.ExecutionEntry{}, err + } + executions = append(executions, entry) + } + return executions, nil } func (cacheReporter *Cache) GetExecutionReport(executionKey uuid.UUID) (cache_report.ExecutionEntry, error) { @@ -201,3 +208,26 @@ func (cacheReporter *Cache) GetExecutionReport(executionKey uuid.UUID) (cache_re return report, nil } + +func (cacheReporter *Cache) copyExecutionEntry(executionKeyStr string) (cache_report.ExecutionEntry, error) { + // NOTE: Deep copy via JSON serialization and de-serialization, longer computation time than custom function + // might want to implement custom deep copy in future + origJSON, err := json.Marshal(cacheReporter.Cache[executionKeyStr]) + if err != nil { + return cache_report.ExecutionEntry{}, err + } + clone := cache_report.ExecutionEntry{} + if err = json.Unmarshal(origJSON, &clone); err != nil { + return cache_report.ExecutionEntry{}, err + } + return clone, nil +} + +func (cacheReporter *Cache) PrintCacheEntries() { + list := []string{} + for _, entry := range cacheReporter.Cache { + b, _ := json.Marshal(entry) + list = append(list, string(b)) + } + fmt.Println(list) +} diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 314bf518..0fb2505e 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -31,7 +31,12 @@ func NewExecutionInformer(informer informer.IExecutionInformer) *executionInform // @error 400 // @Router /report/ [GET] func (executionInformer *executionInformer) getExecutions(g *gin.Context) { - executions := executionInformer.informer.GetExecutionsIds() + executions, err := executionInformer.informer.GetExecutions() + if err != nil { + log.Debug("Could not get executions from informer") + SendErrorResponse(g, http.StatusInternalServerError, "Could not get executions from informer", "GET /report/") + return + } g.JSON(http.StatusOK, executions) } diff --git a/test/integration/api/reporter_api_test.go b/test/integration/api/reporter_api_test.go index c174107e..e0ea1697 100644 --- a/test/integration/api/reporter_api_test.go +++ b/test/integration/api/reporter_api_test.go @@ -8,6 +8,7 @@ import ( "soarca/internal/reporter/downstream_reporter/cache" api_model "soarca/models/api" "soarca/models/cacao" + cache_model "soarca/models/cache" "soarca/routes/reporter" mock_time "soarca/test/unittest/mocks/mock_utils/time" "testing" @@ -84,15 +85,33 @@ func TestGetExecutions(t *testing.T) { executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + executionIds := []uuid.UUID{ + executionId0, + executionId1, + executionId2, + } + layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" timeNow, _ := time.Parse(layout, str) mock_time.On("Now").Return(timeNow) - expectedExecutions := []string{ - "6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "6ba7b810-9dad-11d1-80b4-00c04fd430c1", - "6ba7b810-9dad-11d1-80b4-00c04fd430c2", + expectedStarted, _ := time.Parse(layout, str) + expectedEnded, _ := time.Parse(layout, "0001-01-01T00:00:00Z") + + expectedExecutions := []cache_model.ExecutionEntry{} + for _, executionId := range executionIds { + t.Log(executionId) + entry := cache_model.ExecutionEntry{ + ExecutionId: executionId, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + PlaybookResult: nil, + Status: 2, + } + expectedExecutions = append(expectedExecutions, entry) } err := cacheReporter.ReportWorkflowStart(executionId0, playbook) diff --git a/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go index 3292bba9..24263718 100644 --- a/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go +++ b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go @@ -3,7 +3,7 @@ package mock_reporter import ( "soarca/models/cacao" - cache_report "soarca/models/cache" + cache_model "soarca/models/cache" "github.com/google/uuid" "github.com/stretchr/testify/mock" @@ -31,12 +31,12 @@ func (reporter *Mock_Downstream_Reporter) ReportStepEnd(executionId uuid.UUID, s return args.Error(0) } -func (reporter *Mock_Downstream_Reporter) GetExecutionsIds() []string { +func (reporter *Mock_Downstream_Reporter) GetExecutions() ([]cache_model.ExecutionEntry, error) { args := reporter.Called() - return args.Get(0).([]string) + return args.Get(0).([]cache_model.ExecutionEntry), args.Error(1) } -func (reporter *Mock_Downstream_Reporter) GetExecutionReport(executionKey uuid.UUID) (cache_report.ExecutionEntry, error) { +func (reporter *Mock_Downstream_Reporter) GetExecutionReport(executionKey uuid.UUID) (cache_model.ExecutionEntry, error) { args := reporter.Called(executionKey) - return args.Get(0).(cache_report.ExecutionEntry), args.Error(1) + return args.Get(0).(cache_model.ExecutionEntry), args.Error(1) } diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index f917df3b..fce32640 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -4,7 +4,7 @@ import ( "errors" "soarca/internal/reporter/downstream_reporter/cache" "soarca/models/cacao" - report "soarca/models/cache" + cache_model "soarca/models/cache" "soarca/test/unittest/mocks/mock_utils/time" "testing" "time" @@ -81,23 +81,38 @@ func TestReportWorkflowStartFirst(t *testing.T) { timeNow, _ := time.Parse(layout, str) mock_time.On("Now").Return(timeNow) - expectedExecutionEntry := report.ExecutionEntry{ + expectedExecutionEntry := cache_model.ExecutionEntry{ ExecutionId: executionId0, PlaybookId: "test", - StepResults: map[string]report.StepResult{}, - Status: report.Ongoing, + StepResults: map[string]cache_model.StepResult{}, + Status: cache_model.Ongoing, Started: timeNow, Ended: time.Time{}, } - expectedExecutions := []string{"6ba7b810-9dad-11d1-80b4-00c04fd430c0"} err := cacheReporter.ReportWorkflowStart(executionId0, playbook) if err != nil { t.Fail() } + expectedStarted, _ := time.Parse(layout, "2014-11-12T11:45:26.371Z") + expectedEnded, _ := time.Parse(layout, "0001-01-01T00:00:00Z") + expectedExecutions := []cache_model.ExecutionEntry{ + { + ExecutionId: executionId0, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + PlaybookResult: nil, + Status: 2, + }, + } + + returnedExecutions, _ := cacheReporter.GetExecutions() + exec, err := cacheReporter.GetExecutionReport(executionId0) - assert.Equal(t, expectedExecutions, cacheReporter.GetExecutionsIds()) + assert.Equal(t, expectedExecutions, returnedExecutions) assert.Equal(t, expectedExecutionEntry.ExecutionId, exec.ExecutionId) assert.Equal(t, expectedExecutionEntry.PlaybookId, exec.PlaybookId) assert.Equal(t, expectedExecutionEntry.StepResults, exec.StepResults) @@ -185,29 +200,50 @@ func TestReportWorkflowStartFifo(t *testing.T) { timeNow, _ := time.Parse(layout, str) mock_time.On("Now").Return(timeNow) - expectedExecutionsFull := []string{ - "6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "6ba7b810-9dad-11d1-80b4-00c04fd430c1", - "6ba7b810-9dad-11d1-80b4-00c04fd430c2", - "6ba7b810-9dad-11d1-80b4-00c04fd430c3", - "6ba7b810-9dad-11d1-80b4-00c04fd430c4", - "6ba7b810-9dad-11d1-80b4-00c04fd430c5", - "6ba7b810-9dad-11d1-80b4-00c04fd430c6", - "6ba7b810-9dad-11d1-80b4-00c04fd430c7", - "6ba7b810-9dad-11d1-80b4-00c04fd430c8", - "6ba7b810-9dad-11d1-80b4-00c04fd430c9", - } - expectedExecutionsFifo := []string{ - "6ba7b810-9dad-11d1-80b4-00c04fd430c1", - "6ba7b810-9dad-11d1-80b4-00c04fd430c2", - "6ba7b810-9dad-11d1-80b4-00c04fd430c3", - "6ba7b810-9dad-11d1-80b4-00c04fd430c4", - "6ba7b810-9dad-11d1-80b4-00c04fd430c5", - "6ba7b810-9dad-11d1-80b4-00c04fd430c6", - "6ba7b810-9dad-11d1-80b4-00c04fd430c7", - "6ba7b810-9dad-11d1-80b4-00c04fd430c8", - "6ba7b810-9dad-11d1-80b4-00c04fd430c9", - "6ba7b810-9dad-11d1-80b4-00c04fd430ca", + executionIds := []uuid.UUID{ + executionId0, + executionId1, + executionId2, + executionId3, + executionId4, + executionId5, + executionId6, + executionId7, + executionId8, + executionId9, + executionId10, + } + + expectedStarted, _ := time.Parse(layout, "2014-11-12T11:45:26.371Z") + expectedEnded, _ := time.Parse(layout, "0001-01-01T00:00:00Z") + expectedExecutionsFull := []cache_model.ExecutionEntry{} + for _, executionId := range executionIds[:10] { + t.Log(executionId) + entry := cache_model.ExecutionEntry{ + ExecutionId: executionId, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + PlaybookResult: nil, + Status: 2, + } + expectedExecutionsFull = append(expectedExecutionsFull, entry) + } + t.Log("") + expectedExecutionsFifo := []cache_model.ExecutionEntry{} + for _, executionId := range executionIds[1:] { + t.Log(executionId) + entry := cache_model.ExecutionEntry{ + ExecutionId: executionId, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + PlaybookResult: nil, + Status: 2, + } + expectedExecutionsFifo = append(expectedExecutionsFifo, entry) } err := cacheReporter.ReportWorkflowStart(executionId0, playbook) @@ -251,13 +287,20 @@ func TestReportWorkflowStartFifo(t *testing.T) { t.Fail() } - assert.Equal(t, expectedExecutionsFull, cacheReporter.GetExecutionsIds()) + returnedExecutionsFull, _ := cacheReporter.GetExecutions() + t.Log("expected") + t.Log(expectedExecutionsFull) + t.Log("returned") + t.Log(returnedExecutionsFull) + assert.Equal(t, expectedExecutionsFull, returnedExecutionsFull) err = cacheReporter.ReportWorkflowStart(executionId10, playbook) if err != nil { t.Fail() } - assert.Equal(t, expectedExecutionsFifo, cacheReporter.GetExecutionsIds()) + + // returnedExecutionsFifo, _ := cacheReporter.GetExecutions() + // assert.Equal(t, expectedExecutionsFifo, returnedExecutionsFifo) mock_time.AssertExpectations(t) } @@ -329,8 +372,6 @@ func TestReportWorkflowEnd(t *testing.T) { timeNow, _ := time.Parse(layout, str) mock_time.On("Now").Return(timeNow) - expectedExecutions := []string{"6ba7b810-9dad-11d1-80b4-00c04fd430c0"} - err := cacheReporter.ReportWorkflowStart(executionId0, playbook) if err != nil { t.Fail() @@ -340,17 +381,20 @@ func TestReportWorkflowEnd(t *testing.T) { t.Fail() } - expectedExecutionEntry := report.ExecutionEntry{ + expectedExecutionEntry := cache_model.ExecutionEntry{ ExecutionId: executionId0, PlaybookId: "test", Started: timeNow, Ended: timeNow, - StepResults: map[string]report.StepResult{}, - Status: report.SuccessfullyExecuted, + StepResults: map[string]cache_model.StepResult{}, + Status: cache_model.SuccessfullyExecuted, } + expectedExecutions := []cache_model.ExecutionEntry{expectedExecutionEntry} + + returnedExecutions, _ := cacheReporter.GetExecutions() exec, err := cacheReporter.GetExecutionReport(executionId0) - assert.Equal(t, expectedExecutions, cacheReporter.GetExecutionsIds()) + assert.Equal(t, expectedExecutions, returnedExecutions) assert.Equal(t, expectedExecutionEntry.ExecutionId, exec.ExecutionId) assert.Equal(t, expectedExecutionEntry.PlaybookId, exec.PlaybookId) assert.Equal(t, expectedExecutionEntry.StepResults, exec.StepResults) @@ -435,13 +479,13 @@ func TestReportStepStartAndEnd(t *testing.T) { t.Fail() } - expectedStepStatus := report.StepResult{ + expectedStepStatus := cache_model.StepResult{ ExecutionId: executionId0, StepId: step1.ID, Started: timeNow, Ended: time.Time{}, Variables: cacao.NewVariables(expectedVariables), - Status: report.Ongoing, + Status: cache_model.Ongoing, Error: nil, } @@ -461,13 +505,13 @@ func TestReportStepStartAndEnd(t *testing.T) { t.Fail() } - expectedStepResult := report.StepResult{ + expectedStepResult := cache_model.StepResult{ ExecutionId: executionId0, StepId: step1.ID, Started: timeNow, Ended: timeNow, Variables: cacao.NewVariables(expectedVariables), - Status: report.SuccessfullyExecuted, + Status: cache_model.SuccessfullyExecuted, Error: nil, } diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 6c734861..8d6146bd 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -19,7 +19,7 @@ import ( func TestGetExecutionsInvocation(t *testing.T) { mock_cache_reporter := &mock_ds_reporter.Mock_Downstream_Reporter{} - mock_cache_reporter.On("GetExecutionsIds").Return([]string{}) + mock_cache_reporter.On("GetExecutions").Return([]cache_model.ExecutionEntry{}, nil) app := gin.New() gin.SetMode(gin.DebugMode) From 9e3bbc075a2b62aaeb0dd6666a034f8270621870 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 12:25:58 +0200 Subject: [PATCH 19/40] fix commented test check --- test/unittest/reporters/downstream_reporter/cache_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index fce32640..1e128e2d 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -299,8 +299,8 @@ func TestReportWorkflowStartFifo(t *testing.T) { t.Fail() } - // returnedExecutionsFifo, _ := cacheReporter.GetExecutions() - // assert.Equal(t, expectedExecutionsFifo, returnedExecutionsFifo) + returnedExecutionsFifo, _ := cacheReporter.GetExecutions() + assert.Equal(t, expectedExecutionsFifo, returnedExecutionsFifo) mock_time.AssertExpectations(t) } From b27bd8b076e5d02d49e727fe842777bba1a89418 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 12:27:06 +0200 Subject: [PATCH 20/40] remove unused function for cache --- internal/reporter/downstream_reporter/cache/cache.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 6ce0d5f8..39d96bbc 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -3,7 +3,6 @@ package cache import ( "encoding/json" "errors" - "fmt" "reflect" "soarca/logger" "soarca/models/cacao" @@ -222,12 +221,3 @@ func (cacheReporter *Cache) copyExecutionEntry(executionKeyStr string) (cache_re } return clone, nil } - -func (cacheReporter *Cache) PrintCacheEntries() { - list := []string{} - for _, entry := range cacheReporter.Cache { - b, _ := json.Marshal(entry) - list = append(list, string(b)) - } - fmt.Println(list) -} From e8266ed45921c723b0bf4b9a3b3c93ec3abe3426 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 12:43:35 +0200 Subject: [PATCH 21/40] inject cache size from controller --- internal/controller/controller.go | 13 +++++++++---- .../reporter/downstream_reporter/cache/cache.go | 6 +----- test/integration/api/reporter_api_test.go | 4 ++-- .../reporters/downstream_reporter/cache_test.go | 12 ++++++------ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 2287b146..c75e311d 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -24,8 +24,8 @@ import ( "soarca/logger" "soarca/utils" httpUtil "soarca/utils/http" - timeUtil "soarca/utils/time" "soarca/utils/stix/expression/comparison" + timeUtil "soarca/utils/time" downstreamReporter "soarca/internal/reporter/downstream_reporter" @@ -51,7 +51,9 @@ type Controller struct { var mainController = Controller{} -var mainCache = cache.New(&timeUtil.Time{}) +var mainCache = cache.Cache{} + +const defaultCacheSize int = 10 func (controller *Controller) NewDecomposer() decomposer.IDecomposer { ssh := new(ssh.SshCapability) @@ -82,7 +84,7 @@ func (controller *Controller) NewDecomposer() decomposer.IDecomposer { // NOTE: Enrolling mainCache by default as reporter reporter := reporter.New([]downstreamReporter.IDownStreamReporter{}) - downstreamReporters := []downstreamReporter.IDownStreamReporter{mainCache} + downstreamReporters := []downstreamReporter.IDownStreamReporter{&mainCache} err := reporter.RegisterReporters(downstreamReporters) if err != nil { log.Error("could not load main Cache as reporter for decomposer and executors") @@ -139,6 +141,9 @@ func Initialize() error { } } + cacheSize, _ := strconv.Atoi(utils.GetEnv("MAX_EXECUTIONS", strconv.Itoa(defaultCacheSize))) + mainCache = *cache.New(&timeUtil.Time{}, cacheSize) + errCore := initializeCore(app) if errCore != nil { @@ -183,7 +188,7 @@ func initializeCore(app *gin.Engine) error { // NOTE: Assuming that the cache is the main information mediator for // the reporter API - err = routes.Reporter(app, mainCache) + err = routes.Reporter(app, &mainCache) if err != nil { log.Error(err) return err diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 39d96bbc..722193cc 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -7,9 +7,7 @@ import ( "soarca/logger" "soarca/models/cacao" cache_report "soarca/models/cache" - "soarca/utils" itime "soarca/utils/time" - "strconv" "time" "github.com/google/uuid" @@ -23,7 +21,6 @@ func init() { } const MaxExecutions int = 10 -const MaxSteps int = 10 type Cache struct { Size int @@ -32,8 +29,7 @@ type Cache struct { fifoRegister []string // Used for O(1) FIFO cache management } -func New(timeUtil itime.ITime) *Cache { - maxExecutions, _ := strconv.Atoi(utils.GetEnv("MAX_EXECUTIONS", strconv.Itoa(MaxExecutions))) +func New(timeUtil itime.ITime, maxExecutions int) *Cache { return &Cache{ Size: maxExecutions, Cache: make(map[string]cache_report.ExecutionEntry), diff --git a/test/integration/api/reporter_api_test.go b/test/integration/api/reporter_api_test.go index e0ea1697..a39ddabc 100644 --- a/test/integration/api/reporter_api_test.go +++ b/test/integration/api/reporter_api_test.go @@ -23,7 +23,7 @@ import ( func TestGetExecutions(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", @@ -157,7 +157,7 @@ func TestGetExecutionReport(t *testing.T) { // Do executions, test retrieval via api mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index 1e128e2d..83e7619d 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -16,7 +16,7 @@ import ( func TestReportWorkflowStartFirst(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", @@ -125,7 +125,7 @@ func TestReportWorkflowStartFirst(t *testing.T) { func TestReportWorkflowStartFifo(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", @@ -307,7 +307,7 @@ func TestReportWorkflowStartFifo(t *testing.T) { func TestReportWorkflowEnd(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", @@ -406,7 +406,7 @@ func TestReportWorkflowEnd(t *testing.T) { func TestReportStepStartAndEnd(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", @@ -530,7 +530,7 @@ func TestReportStepStartAndEnd(t *testing.T) { func TestInvalidStepReportAfterExecutionEnd(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", @@ -619,7 +619,7 @@ func TestInvalidStepReportAfterExecutionEnd(t *testing.T) { func TestInvalidStepReportAfterStepEnd(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time) + cacheReporter := cache.New(mock_time, 10) expectedCommand := cacao.Command{ Type: "ssh", From b14a27fdf73abaf1aca39371f086c49ea7f9f9f1 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 12:45:28 +0200 Subject: [PATCH 22/40] add comment ref to cyentific workflow status repo in reporter api model --- models/api/reporter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/api/reporter.go b/models/api/reporter.go index cf91d48c..78955a3d 100644 --- a/models/api/reporter.go +++ b/models/api/reporter.go @@ -8,6 +8,8 @@ import ( type Status uint8 +// Reporter model adapted from https://github.com/cyentific-rni/workflow-status/blob/main/README.md + const ( SuccessfullyExecuted = "successfully_executed" Failed = "failed" From 3d38c2306f8ff98d824af060ce06b0c91cf36352 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 12:48:05 +0200 Subject: [PATCH 23/40] move reporter api init setup to reporter api file --- routes/reporter/init.go | 15 --------------- routes/reporter/reporter_api.go | 12 ++++++++++++ 2 files changed, 12 insertions(+), 15 deletions(-) delete mode 100644 routes/reporter/init.go diff --git a/routes/reporter/init.go b/routes/reporter/init.go deleted file mode 100644 index 29b1c513..00000000 --- a/routes/reporter/init.go +++ /dev/null @@ -1,15 +0,0 @@ -package reporter - -import ( - "reflect" - - "soarca/logger" -) - -var log *logger.Log - -type Empty struct{} - -func init() { - log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Info, "", logger.Json) -} diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 0fb2505e..4bfae3bb 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -7,8 +7,20 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + + "reflect" + + "soarca/logger" ) +var log *logger.Log + +type Empty struct{} + +func init() { + log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Info, "", logger.Json) +} + // A PlaybookController implements the playbook API endpoints is dependent on a database. type executionInformer struct { informer informer.IExecutionInformer From bda81e228d403d469d6f42a6954165c9bf2f3290 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 12:52:44 +0200 Subject: [PATCH 24/40] use error soarca package in reporter api --- routes/reporter/reporter_api.go | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 4bfae3bb..830fe78e 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -3,13 +3,13 @@ package reporter import ( "net/http" "soarca/internal/controller/informer" - "strconv" + + "reflect" + "soarca/routes/error" "github.com/gin-gonic/gin" "github.com/google/uuid" - "reflect" - "soarca/logger" ) @@ -46,7 +46,7 @@ func (executionInformer *executionInformer) getExecutions(g *gin.Context) { executions, err := executionInformer.informer.GetExecutions() if err != nil { log.Debug("Could not get executions from informer") - SendErrorResponse(g, http.StatusInternalServerError, "Could not get executions from informer", "GET /report/") + error.SendErrorResponse(g, http.StatusInternalServerError, "Could not get executions from informer", "GET /report/", "") return } g.JSON(http.StatusOK, executions) @@ -71,24 +71,15 @@ func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { executionEntry, err := executionInformer.informer.GetExecutionReport(uuid) if err != nil { log.Debug("Could not find execution for given id") - SendErrorResponse(g, http.StatusBadRequest, "Could not find execution for given ID", "GET /report/{id}") + error.SendErrorResponse(g, http.StatusBadRequest, "Could not find execution for given ID", "GET /report/{id}", "") return } executionEntryParsed, err := parseCachePlaybookEntry(executionEntry) if err != nil { log.Debug("Could not parse entry to reporter result model") - SendErrorResponse(g, http.StatusInternalServerError, "Could not parse execution report", "GET /report/{id}") + error.SendErrorResponse(g, http.StatusInternalServerError, "Could not parse execution report", "GET /report/{id}", "") return } g.JSON(http.StatusOK, executionEntryParsed) } - -func SendErrorResponse(g *gin.Context, status int, message string, orginal_call string) { - msg := gin.H{ - "status": strconv.Itoa(status), - "message": message, - "original-call": orginal_call, - } - g.JSON(status, msg) -} From b92772a447682da35e80315053224edc5f355810 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:05:16 +0200 Subject: [PATCH 25/40] change mustparse to parse in reporter api --- routes/reporter/reporter_api.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 830fe78e..6318c45f 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -66,7 +66,12 @@ func (executionInformer *executionInformer) getExecutions(g *gin.Context) { func (executionInformer *executionInformer) getExecutionReport(g *gin.Context) { id := g.Param("id") log.Trace("Trying to obtain execution for id: ", id) - uuid := uuid.MustParse(id) + uuid, err := uuid.Parse(id) + if err != nil { + log.Debug("Could not parse id parameter for request") + error.SendErrorResponse(g, http.StatusBadRequest, "Could not parse id parameter for request", "GET /report/{id}", "") + return + } executionEntry, err := executionInformer.informer.GetExecutionReport(uuid) if err != nil { From 6af48b8c8805bb4e3d3c236b516b7e935f07520e Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:07:41 +0200 Subject: [PATCH 26/40] change default request interval in reporter api to const var --- routes/reporter/reporter_parser.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index 058ed473..a2d9a263 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -5,6 +5,8 @@ import ( cache_model "soarca/models/cache" ) +const defaultRequestInterval int = 5 + func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.PlaybookExecutionReport, error) { playbookStatus, err := api_model.CacheStatusEnum2String(cacheEntry.Status) if err != nil { @@ -32,7 +34,7 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P StatusText: playbookErrorStr, Error: playbookErrorStr, StepResults: stepResults, - RequestInterval: 5, + RequestInterval: defaultRequestInterval, } return executionReport, nil } From b89e7c3bef8f4bd09530a4a99d2252cf29811da7 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:10:13 +0200 Subject: [PATCH 27/40] fix playbook error var use in reporter api --- routes/reporter/reporter_parser.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index a2d9a263..9298cbf9 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -13,10 +13,9 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P return api_model.PlaybookExecutionReport{}, err } - playbookError := cacheEntry.PlaybookResult playbookErrorStr := "" - if playbookError != nil { - playbookErrorStr = playbookError.Error() + if cacheEntry.PlaybookResult != nil { + playbookErrorStr = cacheEntry.PlaybookResult.Error() } stepResults, err := parseCacheStepEntries(cacheEntry.StepResults) From c2da441f99341b3fd5e1c3006e45a44c053ae35e Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:26:07 +0200 Subject: [PATCH 28/40] change uuid parse to mustparse in cache tests --- test/integration/api/reporter_api_test.go | 12 ++-- .../downstream_reporter/cache_test.go | 66 ++++--------------- 2 files changed, 18 insertions(+), 60 deletions(-) diff --git a/test/integration/api/reporter_api_test.go b/test/integration/api/reporter_api_test.go index a39ddabc..bdfc24d5 100644 --- a/test/integration/api/reporter_api_test.go +++ b/test/integration/api/reporter_api_test.go @@ -81,9 +81,9 @@ func TestGetExecutions(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") - executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") - executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") executionIds := []uuid.UUID{ executionId0, @@ -215,9 +215,9 @@ func TestGetExecutionReport(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") - executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") - executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index 83e7619d..25f5d37a 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -74,7 +74,7 @@ func TestReportWorkflowStartFirst(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" @@ -125,7 +125,7 @@ func TestReportWorkflowStartFirst(t *testing.T) { func TestReportWorkflowStartFifo(t *testing.T) { mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time, 10) + cacheReporter := cache.New(mock_time, 3) expectedCommand := cacao.Command{ Type: "ssh", @@ -183,17 +183,10 @@ func TestReportWorkflowStartFifo(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") - executionId1, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") - executionId2, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") - executionId3, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c3") - executionId4, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c4") - executionId5, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c5") - executionId6, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c6") - executionId7, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c7") - executionId8, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") - executionId9, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c9") - executionId10, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430ca") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + executionId3 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c3") layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" @@ -205,19 +198,12 @@ func TestReportWorkflowStartFifo(t *testing.T) { executionId1, executionId2, executionId3, - executionId4, - executionId5, - executionId6, - executionId7, - executionId8, - executionId9, - executionId10, } expectedStarted, _ := time.Parse(layout, "2014-11-12T11:45:26.371Z") expectedEnded, _ := time.Parse(layout, "0001-01-01T00:00:00Z") expectedExecutionsFull := []cache_model.ExecutionEntry{} - for _, executionId := range executionIds[:10] { + for _, executionId := range executionIds[:len(executionIds)-1] { t.Log(executionId) entry := cache_model.ExecutionEntry{ ExecutionId: executionId, @@ -258,34 +244,6 @@ func TestReportWorkflowStartFifo(t *testing.T) { if err != nil { t.Fail() } - err = cacheReporter.ReportWorkflowStart(executionId3, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId4, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId5, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId6, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId7, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId8, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId9, playbook) - if err != nil { - t.Fail() - } returnedExecutionsFull, _ := cacheReporter.GetExecutions() t.Log("expected") @@ -294,7 +252,7 @@ func TestReportWorkflowStartFifo(t *testing.T) { t.Log(returnedExecutionsFull) assert.Equal(t, expectedExecutionsFull, returnedExecutionsFull) - err = cacheReporter.ReportWorkflowStart(executionId10, playbook) + err = cacheReporter.ReportWorkflowStart(executionId3, playbook) if err != nil { t.Fail() } @@ -365,7 +323,7 @@ func TestReportWorkflowEnd(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" @@ -464,7 +422,7 @@ func TestReportStepStartAndEnd(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" timeNow, _ := time.Parse(layout, str) @@ -588,7 +546,7 @@ func TestInvalidStepReportAfterExecutionEnd(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" timeNow, _ := time.Parse(layout, str) @@ -677,7 +635,7 @@ func TestInvalidStepReportAfterStepEnd(t *testing.T) { Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") layout := "2006-01-02T15:04:05.000Z" str := "2014-11-12T11:45:26.371Z" timeNow, _ := time.Parse(layout, str) From bc7aa3a1761bce908aa5ebced19f547a22ec711e Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:31:17 +0200 Subject: [PATCH 29/40] separate mock cache from mock downstram reporter --- .../mocks/mock_reporter/mock_downstream_reporter.go | 12 ------------ .../routes/reporter_api/reporter_api_test.go | 6 +++--- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go index 24263718..93149877 100644 --- a/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go +++ b/test/unittest/mocks/mock_reporter/mock_downstream_reporter.go @@ -3,8 +3,6 @@ package mock_reporter import ( "soarca/models/cacao" - cache_model "soarca/models/cache" - "github.com/google/uuid" "github.com/stretchr/testify/mock" ) @@ -30,13 +28,3 @@ func (reporter *Mock_Downstream_Reporter) ReportStepEnd(executionId uuid.UUID, s args := reporter.Called(executionId, step, stepResults, stepError) return args.Error(0) } - -func (reporter *Mock_Downstream_Reporter) GetExecutions() ([]cache_model.ExecutionEntry, error) { - args := reporter.Called() - return args.Get(0).([]cache_model.ExecutionEntry), args.Error(1) -} - -func (reporter *Mock_Downstream_Reporter) GetExecutionReport(executionKey uuid.UUID) (cache_model.ExecutionEntry, error) { - args := reporter.Called(executionKey) - return args.Get(0).(cache_model.ExecutionEntry), args.Error(1) -} diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 8d6146bd..9c523c0b 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -8,7 +8,7 @@ import ( api_model "soarca/models/api" cache_model "soarca/models/cache" "soarca/routes/reporter" - mock_ds_reporter "soarca/test/unittest/mocks/mock_reporter" + mock_cache "soarca/test/unittest/mocks/mock_cache" "testing" "github.com/google/uuid" @@ -18,7 +18,7 @@ import ( ) func TestGetExecutionsInvocation(t *testing.T) { - mock_cache_reporter := &mock_ds_reporter.Mock_Downstream_Reporter{} + mock_cache_reporter := &mock_cache.Mock_Cache{} mock_cache_reporter.On("GetExecutions").Return([]cache_model.ExecutionEntry{}, nil) app := gin.New() @@ -41,7 +41,7 @@ func TestGetExecutionsInvocation(t *testing.T) { } func TestGetExecutionReportInvocation(t *testing.T) { - mock_cache_reporter := &mock_ds_reporter.Mock_Downstream_Reporter{} + mock_cache_reporter := &mock_cache.Mock_Cache{} app := gin.New() gin.SetMode(gin.DebugMode) From bb688c17cfe08fa413a3b53811d2c5e188eddf18 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:31:35 +0200 Subject: [PATCH 30/40] add file mock cache --- test/unittest/mocks/mock_cache/mock_cache.go | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 test/unittest/mocks/mock_cache/mock_cache.go diff --git a/test/unittest/mocks/mock_cache/mock_cache.go b/test/unittest/mocks/mock_cache/mock_cache.go new file mode 100644 index 00000000..fb86f4c5 --- /dev/null +++ b/test/unittest/mocks/mock_cache/mock_cache.go @@ -0,0 +1,42 @@ +package mock_cache + +import ( + "soarca/models/cacao" + + cache_model "soarca/models/cache" + + "github.com/google/uuid" + "github.com/stretchr/testify/mock" +) + +type Mock_Cache struct { + mock.Mock +} + +func (reporter *Mock_Cache) ReportWorkflowStart(executionId uuid.UUID, playbook cacao.Playbook) error { + args := reporter.Called(executionId, playbook) + return args.Error(0) +} +func (reporter *Mock_Cache) ReportWorkflowEnd(executionId uuid.UUID, playbook cacao.Playbook, workflowError error) error { + args := reporter.Called(executionId, playbook, workflowError) + return args.Error(0) +} + +func (reporter *Mock_Cache) ReportStepStart(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables) error { + args := reporter.Called(executionId, step, stepResults) + return args.Error(0) +} +func (reporter *Mock_Cache) ReportStepEnd(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables, stepError error) error { + args := reporter.Called(executionId, step, stepResults, stepError) + return args.Error(0) +} + +func (reporter *Mock_Cache) GetExecutions() ([]cache_model.ExecutionEntry, error) { + args := reporter.Called() + return args.Get(0).([]cache_model.ExecutionEntry), args.Error(1) +} + +func (reporter *Mock_Cache) GetExecutionReport(executionKey uuid.UUID) (cache_model.ExecutionEntry, error) { + args := reporter.Called(executionKey) + return args.Get(0).(cache_model.ExecutionEntry), args.Error(1) +} From 0d164037dc3c07c3434dc9d632a01ee005909add Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:33:27 +0200 Subject: [PATCH 31/40] remove unused fcns from cache mock --- test/unittest/mocks/mock_cache/mock_cache.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/test/unittest/mocks/mock_cache/mock_cache.go b/test/unittest/mocks/mock_cache/mock_cache.go index fb86f4c5..dd0d0d9e 100644 --- a/test/unittest/mocks/mock_cache/mock_cache.go +++ b/test/unittest/mocks/mock_cache/mock_cache.go @@ -1,8 +1,6 @@ package mock_cache import ( - "soarca/models/cacao" - cache_model "soarca/models/cache" "github.com/google/uuid" @@ -13,24 +11,6 @@ type Mock_Cache struct { mock.Mock } -func (reporter *Mock_Cache) ReportWorkflowStart(executionId uuid.UUID, playbook cacao.Playbook) error { - args := reporter.Called(executionId, playbook) - return args.Error(0) -} -func (reporter *Mock_Cache) ReportWorkflowEnd(executionId uuid.UUID, playbook cacao.Playbook, workflowError error) error { - args := reporter.Called(executionId, playbook, workflowError) - return args.Error(0) -} - -func (reporter *Mock_Cache) ReportStepStart(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables) error { - args := reporter.Called(executionId, step, stepResults) - return args.Error(0) -} -func (reporter *Mock_Cache) ReportStepEnd(executionId uuid.UUID, step cacao.Step, stepResults cacao.Variables, stepError error) error { - args := reporter.Called(executionId, step, stepResults, stepError) - return args.Error(0) -} - func (reporter *Mock_Cache) GetExecutions() ([]cache_model.ExecutionEntry, error) { args := reporter.Called() return args.Get(0).([]cache_model.ExecutionEntry), args.Error(1) From fee2f3a3cb1ac4361fda4ef6309132e10866af3b Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 13:39:41 +0200 Subject: [PATCH 32/40] move report api dynamic tests to unittest --- test/integration/api/reporter_api_test.go | 310 ------------------ .../reporter_api_invocation_test.go | 142 ++++++++ .../routes/reporter_api/reporter_api_test.go | 274 +++++++++++++--- 3 files changed, 363 insertions(+), 363 deletions(-) delete mode 100644 test/integration/api/reporter_api_test.go create mode 100644 test/unittest/routes/reporter_api/reporter_api_invocation_test.go diff --git a/test/integration/api/reporter_api_test.go b/test/integration/api/reporter_api_test.go deleted file mode 100644 index bdfc24d5..00000000 --- a/test/integration/api/reporter_api_test.go +++ /dev/null @@ -1,310 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "soarca/internal/reporter/downstream_reporter/cache" - api_model "soarca/models/api" - "soarca/models/cacao" - cache_model "soarca/models/cache" - "soarca/routes/reporter" - mock_time "soarca/test/unittest/mocks/mock_utils/time" - "testing" - "time" - - "github.com/google/uuid" - - "github.com/gin-gonic/gin" - "github.com/go-playground/assert/v2" -) - -func TestGetExecutions(t *testing.T) { - - mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time, 10) - - 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}, - } - executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") - executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") - executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") - - executionIds := []uuid.UUID{ - executionId0, - executionId1, - executionId2, - } - - layout := "2006-01-02T15:04:05.000Z" - str := "2014-11-12T11:45:26.371Z" - timeNow, _ := time.Parse(layout, str) - mock_time.On("Now").Return(timeNow) - - expectedStarted, _ := time.Parse(layout, str) - expectedEnded, _ := time.Parse(layout, "0001-01-01T00:00:00Z") - - expectedExecutions := []cache_model.ExecutionEntry{} - for _, executionId := range executionIds { - t.Log(executionId) - entry := cache_model.ExecutionEntry{ - ExecutionId: executionId, - PlaybookId: "test", - Started: expectedStarted, - Ended: expectedEnded, - StepResults: map[string]cache_model.StepResult{}, - PlaybookResult: nil, - Status: 2, - } - expectedExecutions = append(expectedExecutions, entry) - } - - err := cacheReporter.ReportWorkflowStart(executionId0, playbook) - if err != nil { - t.Fail() - } - - err = cacheReporter.ReportWorkflowStart(executionId1, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId2, playbook) - if err != nil { - t.Fail() - } - - app := gin.New() - gin.SetMode(gin.DebugMode) - - recorder := httptest.NewRecorder() - reporter.Routes(app, cacheReporter) - - request, err := http.NewRequest("GET", "/reporter/", nil) - if err != nil { - t.Fail() - } - - app.ServeHTTP(recorder, request) - expectedByte, err := json.Marshal(expectedExecutions) - if err != nil { - t.Log("failed to decode expected struct to json") - t.Fail() - } - expectedString := string(expectedByte) - assert.Equal(t, expectedString, recorder.Body.String()) - assert.Equal(t, 200, recorder.Code) - - mock_time.AssertExpectations(t) -} - -func TestGetExecutionReport(t *testing.T) { - // Create real cache, create real reporter api object - // Do executions, test retrieval via api - - mock_time := new(mock_time.MockTime) - cacheReporter := cache.New(mock_time, 10) - - 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}, - } - executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") - executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") - executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") - - layout := "2006-01-02T15:04:05.000Z" - str := "2014-11-12T11:45:26.371Z" - timeNow, _ := time.Parse(layout, str) - mock_time.On("Now").Return(timeNow) - - err := cacheReporter.ReportWorkflowStart(executionId0, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportStepStart(executionId0, step1, cacao.NewVariables(expectedVariables)) - if err != nil { - t.Fail() - } - - err = cacheReporter.ReportWorkflowStart(executionId1, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportWorkflowStart(executionId2, playbook) - if err != nil { - t.Fail() - } - err = cacheReporter.ReportStepEnd(executionId0, step1, cacao.NewVariables(expectedVariables), nil) - if err != nil { - t.Fail() - } - - app := gin.New() - gin.SetMode(gin.DebugMode) - - recorder := httptest.NewRecorder() - reporter.Routes(app, cacheReporter) - - expected := `{ - "Type":"execution_status", - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "PlaybookId":"test", - "Started":"2014-11-12 11:45:26.371 +0000 UTC", - "Ended":"0001-01-01 00:00:00 +0000 UTC", - "Status":"ongoing", - "StatusText":"", - "StepResults":{ - "action--test":{ - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "StepId":"action--test", - "Started":"2014-11-12 11:45:26.371 +0000 UTC", - "Ended":"2014-11-12 11:45:26.371 +0000 UTC", - "Status":"successfully_executed", - "StatusText":"", - "Error":"", - "Variables":{ - "var1":{ - "type":"string", - "name":"var1", - "value":"testing" - } - } - } - }, - "Error":"", - "RequestInterval":5 - }` - expectedData := api_model.PlaybookExecutionReport{} - err = json.Unmarshal([]byte(expected), &expectedData) - if err != nil { - t.Log(err) - t.Log("Could not parse data to JSON") - t.Fail() - } - - request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) - if err != nil { - t.Log(err) - t.Fail() - } - app.ServeHTTP(recorder, request) - - receivedData := api_model.PlaybookExecutionReport{} - err = json.Unmarshal(recorder.Body.Bytes(), &receivedData) - if err != nil { - t.Log(err) - t.Log("Could not parse data to JSON") - t.Fail() - } - - assert.Equal(t, expectedData, receivedData) - - mock_time.AssertExpectations(t) -} diff --git a/test/unittest/routes/reporter_api/reporter_api_invocation_test.go b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go new file mode 100644 index 00000000..9c523c0b --- /dev/null +++ b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go @@ -0,0 +1,142 @@ +package reporter_api_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + api_model "soarca/models/api" + cache_model "soarca/models/cache" + "soarca/routes/reporter" + mock_cache "soarca/test/unittest/mocks/mock_cache" + "testing" + + "github.com/google/uuid" + + "github.com/gin-gonic/gin" + "github.com/go-playground/assert/v2" +) + +func TestGetExecutionsInvocation(t *testing.T) { + mock_cache_reporter := &mock_cache.Mock_Cache{} + mock_cache_reporter.On("GetExecutions").Return([]cache_model.ExecutionEntry{}, nil) + + app := gin.New() + gin.SetMode(gin.DebugMode) + + recorder := httptest.NewRecorder() + reporter.Routes(app, mock_cache_reporter) + + request, err := http.NewRequest("GET", "/reporter/", nil) + if err != nil { + t.Fail() + } + + app.ServeHTTP(recorder, request) + expectedString := "[]" + assert.Equal(t, expectedString, recorder.Body.String()) + assert.Equal(t, 200, recorder.Code) + + mock_cache_reporter.AssertExpectations(t) +} + +func TestGetExecutionReportInvocation(t *testing.T) { + mock_cache_reporter := &mock_cache.Mock_Cache{} + app := gin.New() + gin.SetMode(gin.DebugMode) + + recorder := httptest.NewRecorder() + reporter.Routes(app, mock_cache_reporter) + + executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + + expectedCache := `{ + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "PlaybookId":"test", + "Started":"2014-11-12T11:45:26.371Z", + "Ended":"0001-01-01T00:00:00Z", + "StepResults":{ + "action--test":{ + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "StepId":"action--test", + "Started":"2014-11-12T11:45:26.371Z", + "Ended":"2014-11-12T11:45:26.371Z", + "Variables":{ + "var1":{ + "type":"string", + "name":"var1", + "value":"testing" + } + }, + "Status":0, + "Error":null + } + }, + "PlaybookResult":null, + "Status":2 + }` + expectedCacheData := cache_model.ExecutionEntry{} + err := json.Unmarshal([]byte(expectedCache), &expectedCacheData) + if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") + t.Fail() + } + + mock_cache_reporter.On("GetExecutionReport", executionId0).Return(expectedCacheData, nil) + + request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) + if err != nil { + t.Log(err) + t.Fail() + } + app.ServeHTTP(recorder, request) + + expectedResponse := `{ + "Type":"execution_status", + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "PlaybookId":"test", + "Started":"2014-11-12 11:45:26.371 +0000 UTC", + "Ended":"0001-01-01 00:00:00 +0000 UTC", + "Status":"ongoing", + "StatusText":"", + "StepResults":{ + "action--test":{ + "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "StepId":"action--test", + "Started":"2014-11-12 11:45:26.371 +0000 UTC", + "Ended":"2014-11-12 11:45:26.371 +0000 UTC", + "Status":"successfully_executed", + "StatusText":"", + "Error":"", + "Variables":{ + "var1":{ + "type":"string", + "name":"var1", + "value":"testing" + } + } + } + }, + "Error":"", + "RequestInterval":5 + }` + expectedResponseData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal([]byte(expectedResponse), &expectedResponseData) + if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") + t.Fail() + } + + receivedData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal(recorder.Body.Bytes(), &receivedData) + if err != nil { + t.Log(err) + t.Log("Could not parse data to JSON") + t.Fail() + } + + assert.Equal(t, expectedResponseData, receivedData) + mock_cache_reporter.AssertExpectations(t) +} diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 9c523c0b..76bbcee2 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -5,11 +5,14 @@ import ( "fmt" "net/http" "net/http/httptest" + "soarca/internal/reporter/downstream_reporter/cache" api_model "soarca/models/api" + "soarca/models/cacao" cache_model "soarca/models/cache" "soarca/routes/reporter" - mock_cache "soarca/test/unittest/mocks/mock_cache" + mock_time "soarca/test/unittest/mocks/mock_utils/time" "testing" + "time" "github.com/google/uuid" @@ -17,15 +20,119 @@ import ( "github.com/go-playground/assert/v2" ) -func TestGetExecutionsInvocation(t *testing.T) { - mock_cache_reporter := &mock_cache.Mock_Cache{} - mock_cache_reporter.On("GetExecutions").Return([]cache_model.ExecutionEntry{}, nil) +func TestGetExecutions(t *testing.T) { + + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time, 10) + + 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}, + } + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + + executionIds := []uuid.UUID{ + executionId0, + executionId1, + executionId2, + } + + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + expectedStarted, _ := time.Parse(layout, str) + expectedEnded, _ := time.Parse(layout, "0001-01-01T00:00:00Z") + + expectedExecutions := []cache_model.ExecutionEntry{} + for _, executionId := range executionIds { + t.Log(executionId) + entry := cache_model.ExecutionEntry{ + ExecutionId: executionId, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + PlaybookResult: nil, + Status: 2, + } + expectedExecutions = append(expectedExecutions, entry) + } + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + + err = cacheReporter.ReportWorkflowStart(executionId1, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportWorkflowStart(executionId2, playbook) + if err != nil { + t.Fail() + } app := gin.New() gin.SetMode(gin.DebugMode) recorder := httptest.NewRecorder() - reporter.Routes(app, mock_cache_reporter) + reporter.Routes(app, cacheReporter) request, err := http.NewRequest("GET", "/reporter/", nil) if err != nil { @@ -33,66 +140,119 @@ func TestGetExecutionsInvocation(t *testing.T) { } app.ServeHTTP(recorder, request) - expectedString := "[]" + expectedByte, err := json.Marshal(expectedExecutions) + if err != nil { + t.Log("failed to decode expected struct to json") + t.Fail() + } + expectedString := string(expectedByte) assert.Equal(t, expectedString, recorder.Body.String()) assert.Equal(t, 200, recorder.Code) - mock_cache_reporter.AssertExpectations(t) + mock_time.AssertExpectations(t) } -func TestGetExecutionReportInvocation(t *testing.T) { - mock_cache_reporter := &mock_cache.Mock_Cache{} - app := gin.New() - gin.SetMode(gin.DebugMode) +func TestGetExecutionReport(t *testing.T) { + // Create real cache, create real reporter api object + // Do executions, test retrieval via api - recorder := httptest.NewRecorder() - reporter.Routes(app, mock_cache_reporter) + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time, 10) - executionId0, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + expectedCommand := cacao.Command{ + Type: "ssh", + Command: "ssh ls -la", + } - expectedCache := `{ - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "PlaybookId":"test", - "Started":"2014-11-12T11:45:26.371Z", - "Ended":"0001-01-01T00:00:00Z", - "StepResults":{ - "action--test":{ - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "StepId":"action--test", - "Started":"2014-11-12T11:45:26.371Z", - "Ended":"2014-11-12T11:45:26.371Z", - "Variables":{ - "var1":{ - "type":"string", - "name":"var1", - "value":"testing" - } - }, - "Status":0, - "Error":null - } - }, - "PlaybookResult":null, - "Status":2 - }` - expectedCacheData := cache_model.ExecutionEntry{} - err := json.Unmarshal([]byte(expectedCache), &expectedCacheData) + 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}, + } + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") + executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") + + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepStart(executionId0, step1, cacao.NewVariables(expectedVariables)) if err != nil { - t.Log(err) - t.Log("Could not parse data to JSON") t.Fail() } - mock_cache_reporter.On("GetExecutionReport", executionId0).Return(expectedCacheData, nil) - - request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) + err = cacheReporter.ReportWorkflowStart(executionId1, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportWorkflowStart(executionId2, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepEnd(executionId0, step1, cacao.NewVariables(expectedVariables), nil) if err != nil { - t.Log(err) t.Fail() } - app.ServeHTTP(recorder, request) - expectedResponse := `{ + app := gin.New() + gin.SetMode(gin.DebugMode) + + recorder := httptest.NewRecorder() + reporter.Routes(app, cacheReporter) + + expected := `{ "Type":"execution_status", "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", "PlaybookId":"test", @@ -121,14 +281,21 @@ func TestGetExecutionReportInvocation(t *testing.T) { "Error":"", "RequestInterval":5 }` - expectedResponseData := api_model.PlaybookExecutionReport{} - err = json.Unmarshal([]byte(expectedResponse), &expectedResponseData) + expectedData := api_model.PlaybookExecutionReport{} + err = json.Unmarshal([]byte(expected), &expectedData) if err != nil { t.Log(err) t.Log("Could not parse data to JSON") t.Fail() } + request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) + if err != nil { + t.Log(err) + t.Fail() + } + app.ServeHTTP(recorder, request) + receivedData := api_model.PlaybookExecutionReport{} err = json.Unmarshal(recorder.Body.Bytes(), &receivedData) if err != nil { @@ -137,6 +304,7 @@ func TestGetExecutionReportInvocation(t *testing.T) { t.Fail() } - assert.Equal(t, expectedResponseData, receivedData) - mock_cache_reporter.AssertExpectations(t) + assert.Equal(t, expectedData, receivedData) + + mock_time.AssertExpectations(t) } From e0e5f337add1e1e96040047a3096cf983fe738df Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 15:21:03 +0200 Subject: [PATCH 33/40] align reporter api model with cyentific workflow status model --- .../en/docs/core-components/api-reporter.md | 3 + .../downstream_reporter/cache/cache.go | 18 ++ models/api/reporter.go | 22 +- models/cacao/cacao.go | 2 + models/cache/cache.go | 9 +- routes/reporter/reporter_parser.go | 24 +- .../downstream_reporter/cache_test.go | 271 ++++++++++++++++++ .../reporter_api_invocation_test.go | 11 +- .../routes/reporter_api/reporter_api_test.go | 9 +- 9 files changed, 346 insertions(+), 23 deletions(-) diff --git a/docs/content/en/docs/core-components/api-reporter.md b/docs/content/en/docs/core-components/api-reporter.md index 52ad0806..99920dfb 100644 --- a/docs/content/en/docs/core-components/api-reporter.md +++ b/docs/content/en/docs/core-components/api-reporter.md @@ -86,8 +86,11 @@ Response data model: |ended |timestamp |string |The time at which the execution of the step ended (if so) |status |execution-status-enum |string |The current [status](#execution-stataus) of the execution of this step |status_text |explanation |string |A natural language explanation of the current status or related info +|executed_by |entity-identifier |string |The entity executed the workflow step. This can be an organization, a team, a role, a defence component, etc. +|commands_b64 |list of base64 |list of string |A list of Base64 encodings of the commands that were invoked during the execution of a workflow step, including any values stemming from variables. These are the actual commands executed. |error |error |string |Error raised along the execution of the step |variables |cacao variables |dictionary |Map of [cacao variables](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/cs01/security-playbooks-v2.0-cs01.html#_Toc152256555) handled in the step (both in and out) with current values and definitions +|automated_execution | boolean |string |This property identifies if the workflow step was executed manually or automatically. It is either true or false. ##### Execution stataus Table from [Cyentific RNI workflow Status](https://github.com/cyentific-rni/workflow-status/blob/main/README.md#21-refined-execution-status-enumeration) diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 722193cc..74e8a0ce 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -1,6 +1,7 @@ package cache import ( + b64 "encoding/base64" "encoding/json" "errors" "reflect" @@ -135,14 +136,31 @@ func (cacheReporter *Cache) ReportStepStart(executionId uuid.UUID, step cacao.St log.Warning("a step execution was already reported for this step. overwriting.") } + // TODO: must test + commandsB64 := []string{} + isAutomated := true + for _, cmd := range step.Commands { + if cmd.Type == cacao.CommandTypeManual { + isAutomated = false + } + if cmd.CommandB64 != "" { + commandsB64 = append(commandsB64, cmd.CommandB64) + } else { + cmdB64 := b64.StdEncoding.EncodeToString([]byte(cmd.Command)) + commandsB64 = append(commandsB64, cmdB64) + } + } + newStepEntry := cache_report.StepResult{ ExecutionId: executionId, StepId: step.ID, Started: cacheReporter.timeUtil.Now(), Ended: time.Time{}, Variables: variables, + CommandsB64: commandsB64, Status: cache_report.Ongoing, Error: nil, + IsAutomated: isAutomated, } executionEntry.StepResults[step.ID] = newStepEntry return nil diff --git a/models/api/reporter.go b/models/api/reporter.go index 78955a3d..7d4e588e 100644 --- a/models/api/reporter.go +++ b/models/api/reporter.go @@ -18,6 +18,7 @@ const ( ClientSideError = "client_side_error" TimeoutError = "timeout_error" ExceptionConditionError = "exception_condition_error" + AwaitUserInput = "await_user_input" ) type PlaybookExecutionReport struct { @@ -34,14 +35,17 @@ type PlaybookExecutionReport struct { } type StepExecutionReport struct { - ExecutionId string - StepId string - Started string - Ended string - Status string - StatusText string - Error string - Variables map[string]cacao.Variable + ExecutionId string + StepId string + Started string + Ended string + Status string + StatusText string + ExecutedBy string + CommandsB64 []string + Error string + Variables map[string]cacao.Variable + AutomatedExecution string // Make sure we can have a playbookID for playbook actions, and also // the execution ID for the invoked playbook } @@ -62,6 +66,8 @@ func CacheStatusEnum2String(status cache_model.Status) (string, error) { return TimeoutError, nil case cache_model.ExceptionConditionError: return ExceptionConditionError, nil + case cache_model.AwaitUserInput: + return AwaitUserInput, nil default: return "", errors.New("unable to read execution information status") } diff --git a/models/cacao/cacao.go b/models/cacao/cacao.go index c0d36919..a0300fc8 100644 --- a/models/cacao/cacao.go +++ b/models/cacao/cacao.go @@ -17,6 +17,8 @@ const ( StepTypeWhileCondition = "while-condition" StepTypeSwitchCondition = "switch-condition" + CommandTypeManual = "manual" + AuthInfoOAuth2Type = "oauth2" AuthInfoHTTPBasicType = "http-basic" AuthInfoNotSet = "" diff --git a/models/cache/cache.go b/models/cache/cache.go index f2187b7f..263454eb 100644 --- a/models/cache/cache.go +++ b/models/cache/cache.go @@ -17,6 +17,7 @@ const ( ClientSideError TimeoutError ExceptionConditionError + AwaitUserInput ) type ExecutionEntry struct { @@ -36,7 +37,9 @@ type StepResult struct { Ended time.Time // Make sure we can have a playbookID for playbook actions, and also // the execution ID for the invoked playbook - Variables cacao.Variables - Status Status - Error error + CommandsB64 []string + Variables cacao.Variables + Status Status + Error error + IsAutomated bool } diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index 9298cbf9..323e4966 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -53,15 +53,23 @@ func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) ( stepErrorStr = stepError.Error() } + automatedExecution := "true" + if !stepEntry.IsAutomated { + automatedExecution = "false" + } + parsedEntries[stepId] = api_model.StepExecutionReport{ - ExecutionId: stepEntry.ExecutionId.String(), - StepId: stepEntry.StepId, - Started: stepEntry.Started.String(), - Ended: stepEntry.Ended.String(), - Status: stepStatus, - StatusText: stepErrorStr, - Error: stepErrorStr, - Variables: stepEntry.Variables, + ExecutionId: stepEntry.ExecutionId.String(), + StepId: stepEntry.StepId, + Started: stepEntry.Started.String(), + Ended: stepEntry.Ended.String(), + Status: stepStatus, + StatusText: stepErrorStr, + ExecutedBy: "soarca", + CommandsB64: stepEntry.CommandsB64, + Error: stepErrorStr, + Variables: stepEntry.Variables, + AutomatedExecution: automatedExecution, } } return parsedEntries, nil diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index 25f5d37a..6f0b3d3c 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -1,6 +1,7 @@ package cache_test import ( + b64 "encoding/base64" "errors" "soarca/internal/reporter/downstream_reporter/cache" "soarca/models/cacao" @@ -486,6 +487,276 @@ func TestReportStepStartAndEnd(t *testing.T) { mock_time.AssertExpectations(t) } +func TestReportStepStartCommandsEncoding(t *testing.T) { + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time, 10) + + expectedCommand1 := cacao.Command{ + Type: "manual", + CommandB64: b64.StdEncoding.EncodeToString([]byte("do ssh ls -la in the terminal")), + } + expectedCommand2 := 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{expectedCommand1, expectedCommand2}, + 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}, + } + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepStart(executionId0, step1, cacao.NewVariables(expectedVariables)) + if err != nil { + t.Fail() + } + + encodedCommand1 := expectedCommand1.CommandB64 + encodedCommand2 := b64.StdEncoding.EncodeToString([]byte(expectedCommand2.Command)) + expectedCommandsB64 := []string{encodedCommand1, encodedCommand2} + + expectedStepStatus := cache_model.StepResult{ + ExecutionId: executionId0, + StepId: step1.ID, + Started: timeNow, + Ended: time.Time{}, + Variables: cacao.NewVariables(expectedVariables), + Status: cache_model.Ongoing, + CommandsB64: expectedCommandsB64, + Error: nil, + IsAutomated: false, + } + + exec, err := cacheReporter.GetExecutionReport(executionId0) + stepStatus := exec.StepResults[step1.ID] + t.Log("stepStatus commands") + t.Log(stepStatus.CommandsB64) + t.Log("expectedStep commands") + t.Log(expectedStepStatus.CommandsB64) + assert.Equal(t, stepStatus.ExecutionId, expectedStepStatus.ExecutionId) + assert.Equal(t, stepStatus.StepId, expectedStepStatus.StepId) + assert.Equal(t, stepStatus.Started, expectedStepStatus.Started) + assert.Equal(t, stepStatus.Ended, expectedStepStatus.Ended) + assert.Equal(t, stepStatus.Variables, expectedStepStatus.Variables) + assert.Equal(t, stepStatus.Status, expectedStepStatus.Status) + assert.Equal(t, stepStatus.Error, expectedStepStatus.Error) + assert.Equal(t, stepStatus.CommandsB64, expectedStepStatus.CommandsB64) + assert.Equal(t, stepStatus.IsAutomated, expectedStepStatus.IsAutomated) + assert.Equal(t, err, nil) + + err = cacheReporter.ReportStepEnd(executionId0, step1, cacao.NewVariables(expectedVariables), nil) + if err != nil { + t.Fail() + } + + expectedStepResult := cache_model.StepResult{ + ExecutionId: executionId0, + StepId: step1.ID, + Started: timeNow, + Ended: timeNow, + Variables: cacao.NewVariables(expectedVariables), + Status: cache_model.SuccessfullyExecuted, + Error: nil, + } + + exec, err = cacheReporter.GetExecutionReport(executionId0) + stepResult := exec.StepResults[step1.ID] + assert.Equal(t, stepResult.ExecutionId, expectedStepResult.ExecutionId) + assert.Equal(t, stepResult.StepId, expectedStepResult.StepId) + assert.Equal(t, stepResult.Started, expectedStepResult.Started) + assert.Equal(t, stepResult.Ended, expectedStepResult.Ended) + assert.Equal(t, stepResult.Variables, expectedStepResult.Variables) + assert.Equal(t, stepResult.Status, expectedStepResult.Status) + assert.Equal(t, stepResult.Error, expectedStepResult.Error) + assert.Equal(t, err, nil) + mock_time.AssertExpectations(t) +} + +func TestReportStepStartManualCommand(t *testing.T) { + mock_time := new(mock_time.MockTime) + cacheReporter := cache.New(mock_time, 10) + + expectedCommand := cacao.Command{ + Type: "manual", + Command: "do ssh ls -la in the terminal", + } + + 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}, + } + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") + layout := "2006-01-02T15:04:05.000Z" + str := "2014-11-12T11:45:26.371Z" + timeNow, _ := time.Parse(layout, str) + mock_time.On("Now").Return(timeNow) + + err := cacheReporter.ReportWorkflowStart(executionId0, playbook) + if err != nil { + t.Fail() + } + err = cacheReporter.ReportStepStart(executionId0, step1, cacao.NewVariables(expectedVariables)) + if err != nil { + t.Fail() + } + + encodedCommand := b64.StdEncoding.EncodeToString([]byte(expectedCommand.Command)) + + expectedStepStatus := cache_model.StepResult{ + ExecutionId: executionId0, + StepId: step1.ID, + Started: timeNow, + Ended: time.Time{}, + Variables: cacao.NewVariables(expectedVariables), + Status: cache_model.Ongoing, + CommandsB64: []string{encodedCommand}, + Error: nil, + IsAutomated: false, + } + + exec, err := cacheReporter.GetExecutionReport(executionId0) + stepStatus := exec.StepResults[step1.ID] + assert.Equal(t, stepStatus.ExecutionId, expectedStepStatus.ExecutionId) + assert.Equal(t, stepStatus.StepId, expectedStepStatus.StepId) + assert.Equal(t, stepStatus.Started, expectedStepStatus.Started) + assert.Equal(t, stepStatus.Ended, expectedStepStatus.Ended) + assert.Equal(t, stepStatus.Variables, expectedStepStatus.Variables) + assert.Equal(t, stepStatus.Status, expectedStepStatus.Status) + assert.Equal(t, stepStatus.Error, expectedStepStatus.Error) + assert.Equal(t, stepStatus.CommandsB64, expectedStepStatus.CommandsB64) + assert.Equal(t, stepStatus.IsAutomated, expectedStepStatus.IsAutomated) + assert.Equal(t, err, nil) + + err = cacheReporter.ReportStepEnd(executionId0, step1, cacao.NewVariables(expectedVariables), nil) + if err != nil { + t.Fail() + } + + expectedStepResult := cache_model.StepResult{ + ExecutionId: executionId0, + StepId: step1.ID, + Started: timeNow, + Ended: timeNow, + Variables: cacao.NewVariables(expectedVariables), + Status: cache_model.SuccessfullyExecuted, + Error: nil, + } + + exec, err = cacheReporter.GetExecutionReport(executionId0) + stepResult := exec.StepResults[step1.ID] + assert.Equal(t, stepResult.ExecutionId, expectedStepResult.ExecutionId) + assert.Equal(t, stepResult.StepId, expectedStepResult.StepId) + assert.Equal(t, stepResult.Started, expectedStepResult.Started) + assert.Equal(t, stepResult.Ended, expectedStepResult.Ended) + assert.Equal(t, stepResult.Variables, expectedStepResult.Variables) + assert.Equal(t, stepResult.Status, expectedStepResult.Status) + assert.Equal(t, stepResult.Error, expectedStepResult.Error) + assert.Equal(t, err, nil) + mock_time.AssertExpectations(t) +} + func TestInvalidStepReportAfterExecutionEnd(t *testing.T) { mock_time := new(mock_time.MockTime) cacheReporter := cache.New(mock_time, 10) diff --git a/test/unittest/routes/reporter_api/reporter_api_invocation_test.go b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go index 9c523c0b..82173192 100644 --- a/test/unittest/routes/reporter_api/reporter_api_invocation_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go @@ -68,6 +68,8 @@ func TestGetExecutionReportInvocation(t *testing.T) { "value":"testing" } }, + "CommandsB64" : [], + "IsAutomated" : true, "Status":0, "Error":null } @@ -115,7 +117,10 @@ func TestGetExecutionReportInvocation(t *testing.T) { "name":"var1", "value":"testing" } - } + }, + "CommandsB64" : [], + "AutomatedExecution" : "true", + "ExecutedBy" : "soarca" } }, "Error":"", @@ -137,6 +142,10 @@ func TestGetExecutionReportInvocation(t *testing.T) { t.Fail() } + t.Log("expected response") + t.Log(expectedResponseData) + t.Log("received response") + t.Log(receivedData) assert.Equal(t, expectedResponseData, receivedData) mock_cache_reporter.AssertExpectations(t) } diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 76bbcee2..02b5743d 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -212,9 +212,9 @@ func TestGetExecutionReport(t *testing.T) { 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}, + Workflow: map[string]cacao.Step{step1.ID: step1, end.ID: end}, } + executionId0 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c0") executionId1 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c1") executionId2 := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c2") @@ -275,7 +275,10 @@ func TestGetExecutionReport(t *testing.T) { "name":"var1", "value":"testing" } - } + }, + "CommandsB64" : ["c3NoIGxzIC1sYQ=="], + "AutomatedExecution" : "true", + "ExecutedBy" : "soarca" } }, "Error":"", From c3cfbcf39419b9ed638ce8d0034c92ea30c632bc Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 15:58:34 +0200 Subject: [PATCH 34/40] implement status text --- models/api/reporter.go | 39 +++++++++++++++++++ routes/reporter/reporter_parser.go | 7 ++-- .../reporter_api_invocation_test.go | 12 +++--- .../routes/reporter_api/reporter_api_test.go | 4 +- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/models/api/reporter.go b/models/api/reporter.go index 7d4e588e..e8f19ac7 100644 --- a/models/api/reporter.go +++ b/models/api/reporter.go @@ -2,6 +2,7 @@ package api import ( "errors" + "fmt" "soarca/models/cacao" cache_model "soarca/models/cache" ) @@ -11,6 +12,9 @@ type Status uint8 // Reporter model adapted from https://github.com/cyentific-rni/workflow-status/blob/main/README.md const ( + ReportLevelPlaybook = "playbook" + ReportLevelStep = "step" + SuccessfullyExecuted = "successfully_executed" Failed = "failed" Ongoing = "ongoing" @@ -19,6 +23,15 @@ const ( TimeoutError = "timeout_error" ExceptionConditionError = "exception_condition_error" AwaitUserInput = "await_user_input" + + SuccessfullyExecutedText = "%s execution completed successfully" + FailedText = "something went wrong in the execution of this %s" + OngoingText = "this %s is currently being executed" + ServerSideErrorText = "there was a server-side problem with the execution of this %s" + ClientSideErrorText = "something in the data provided for this %s raised an issue" + TimeoutErrorText = "the execution of this %s timed out" + ExceptionConditionErrorText = "the execution of this %s raised a playbook exception" + AwaitUserInputText = "waiting for users to provide input for the %s execution" ) type PlaybookExecutionReport struct { @@ -72,3 +85,29 @@ func CacheStatusEnum2String(status cache_model.Status) (string, error) { return "", errors.New("unable to read execution information status") } } + +func GetCacheStatusText(status string, level string) (string, error) { + if level != ReportLevelPlaybook && level != ReportLevelStep { + return "", errors.New("invalid reporting level provided. use either 'playbook' or 'step'") + } + switch status { + case SuccessfullyExecuted: + return fmt.Sprintf(SuccessfullyExecutedText, level), nil + case Failed: + return fmt.Sprintf(FailedText, level), nil + case Ongoing: + return fmt.Sprintf(OngoingText, level), nil + case ServerSideError: + return fmt.Sprintf(ServerSideErrorText, level), nil + case ClientSideError: + return fmt.Sprintf(ClientSideErrorText, level), nil + case TimeoutError: + return fmt.Sprintf(TimeoutErrorText, level), nil + case ExceptionConditionError: + return fmt.Sprintf(ExceptionConditionErrorText, level), nil + case AwaitUserInput: + return fmt.Sprintf(AwaitUserInputText, level), nil + default: + return "", errors.New("unable to read execution information status") + } +} diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index 323e4966..eb2e54a6 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -12,7 +12,7 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P if err != nil { return api_model.PlaybookExecutionReport{}, err } - + playbookStatusText, err := api_model.GetCacheStatusText(playbookStatus, api_model.ReportLevelPlaybook) playbookErrorStr := "" if cacheEntry.PlaybookResult != nil { playbookErrorStr = cacheEntry.PlaybookResult.Error() @@ -30,7 +30,7 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P Started: cacheEntry.Started.String(), Ended: cacheEntry.Ended.String(), Status: playbookStatus, - StatusText: playbookErrorStr, + StatusText: playbookStatusText, Error: playbookErrorStr, StepResults: stepResults, RequestInterval: defaultRequestInterval, @@ -43,6 +43,7 @@ func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) ( for stepId, stepEntry := range cacheStepEntries { stepStatus, err := api_model.CacheStatusEnum2String(stepEntry.Status) + stepStatusText, err := api_model.GetCacheStatusText(stepStatus, api_model.ReportLevelStep) if err != nil { return map[string]api_model.StepExecutionReport{}, err } @@ -64,7 +65,7 @@ func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) ( Started: stepEntry.Started.String(), Ended: stepEntry.Ended.String(), Status: stepStatus, - StatusText: stepErrorStr, + StatusText: stepStatusText, ExecutedBy: "soarca", CommandsB64: stepEntry.CommandsB64, Error: stepErrorStr, diff --git a/test/unittest/routes/reporter_api/reporter_api_invocation_test.go b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go index 82173192..d1ea77cb 100644 --- a/test/unittest/routes/reporter_api/reporter_api_invocation_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go @@ -101,15 +101,15 @@ func TestGetExecutionReportInvocation(t *testing.T) { "Started":"2014-11-12 11:45:26.371 +0000 UTC", "Ended":"0001-01-01 00:00:00 +0000 UTC", "Status":"ongoing", - "StatusText":"", + "StatusText":"this playbook is currently being executed", "StepResults":{ "action--test":{ "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "StepId":"action--test", - "Started":"2014-11-12 11:45:26.371 +0000 UTC", - "Ended":"2014-11-12 11:45:26.371 +0000 UTC", - "Status":"successfully_executed", - "StatusText":"", + "StepId": "action--test", + "Started": "2014-11-12 11:45:26.371 +0000 UTC", + "Ended": "2014-11-12 11:45:26.371 +0000 UTC", + "Status": "successfully_executed", + "StatusText": "step execution completed successfully", "Error":"", "Variables":{ "var1":{ diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 02b5743d..3d63904c 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -259,7 +259,7 @@ func TestGetExecutionReport(t *testing.T) { "Started":"2014-11-12 11:45:26.371 +0000 UTC", "Ended":"0001-01-01 00:00:00 +0000 UTC", "Status":"ongoing", - "StatusText":"", + "StatusText":"this playbook is currently being executed", "StepResults":{ "action--test":{ "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", @@ -267,7 +267,7 @@ func TestGetExecutionReport(t *testing.T) { "Started":"2014-11-12 11:45:26.371 +0000 UTC", "Ended":"2014-11-12 11:45:26.371 +0000 UTC", "Status":"successfully_executed", - "StatusText":"", + "StatusText": "step execution completed successfully", "Error":"", "Variables":{ "var1":{ From 8d6197ee7ee7303c32ef33c5fef68ae6be61efb0 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Tue, 14 May 2024 16:08:52 +0200 Subject: [PATCH 35/40] fix lint error check --- routes/reporter/reporter_parser.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index eb2e54a6..1ffc36a6 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -13,6 +13,9 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P return api_model.PlaybookExecutionReport{}, err } playbookStatusText, err := api_model.GetCacheStatusText(playbookStatus, api_model.ReportLevelPlaybook) + if err != nil { + return api_model.PlaybookExecutionReport{}, err + } playbookErrorStr := "" if cacheEntry.PlaybookResult != nil { playbookErrorStr = cacheEntry.PlaybookResult.Error() @@ -43,6 +46,9 @@ func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) ( for stepId, stepEntry := range cacheStepEntries { stepStatus, err := api_model.CacheStatusEnum2String(stepEntry.Status) + if err != nil { + return map[string]api_model.StepExecutionReport{}, err + } stepStatusText, err := api_model.GetCacheStatusText(stepStatus, api_model.ReportLevelStep) if err != nil { return map[string]api_model.StepExecutionReport{}, err From 867f444df08921150327be0cc352fe265ba527d3 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Wed, 15 May 2024 10:50:14 +0200 Subject: [PATCH 36/40] add command types to cacao model --- models/cacao/cacao.go | 13 ++++++++++++- routes/playbook/playbook_api.go | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/models/cacao/cacao.go b/models/cacao/cacao.go index a0300fc8..a50034c2 100644 --- a/models/cacao/cacao.go +++ b/models/cacao/cacao.go @@ -17,7 +17,18 @@ const ( StepTypeWhileCondition = "while-condition" StepTypeSwitchCondition = "switch-condition" - CommandTypeManual = "manual" + CommandTypeManual = "manual" + CommandTypeBash = "bash" + CommandTypeCalderaCmd = "caldera-cmd" + CommandTypeElastic = "elastic" + CommandTypeHttpApi = "http-api" + CommandTypeJupyter = "jupyter" + CommandTypeKestrel = "kestrel" + CommandTypeOpenC2Http = "openc2-http" + CommandTypePowershell = "powershell" + CommandTypeSigma = "sigma" + CommandTypeSsh = "ssh" + CommandTypeYara = "yara" AuthInfoOAuth2Type = "oauth2" AuthInfoHTTPBasicType = "http-basic" diff --git a/routes/playbook/playbook_api.go b/routes/playbook/playbook_api.go index 393d61f7..1f5cb845 100644 --- a/routes/playbook/playbook_api.go +++ b/routes/playbook/playbook_api.go @@ -11,6 +11,13 @@ import ( "github.com/gin-gonic/gin" ) +// TODOs +// Add all command types to cacao model +// Change api parsing model to actual data types and add json bson strings for formatting +// Refactor reporter parsing in getting info from cache via now using new model +// Remove copy of cache entries and just pass the objects +// Remove Error from step cache report + // A PlaybookController implements the playbook API endpoints is dependent on a database. type playbookController struct { playbookRepo playbookrepository.IPlaybookRepository From 841c51a4a63e0768aab2a8b46efff3561dde826b Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Wed, 15 May 2024 11:47:18 +0200 Subject: [PATCH 37/40] change reporter api resutls model --- .../en/docs/core-components/api-reporter.md | 1 - .../downstream_reporter/cache/cache.go | 2 +- models/api/reporter.go | 41 ++++++------ models/cache/cache.go | 14 ++--- routes/playbook/playbook_api.go | 7 --- routes/reporter/reporter_api.go | 5 ++ routes/reporter/reporter_parser.go | 29 +++------ .../downstream_reporter/cache_test.go | 42 ++++++------- .../reporter_api_invocation_test.go | 38 ++++++------ .../routes/reporter_api/reporter_api_test.go | 62 ++++++++++--------- 10 files changed, 116 insertions(+), 125 deletions(-) diff --git a/docs/content/en/docs/core-components/api-reporter.md b/docs/content/en/docs/core-components/api-reporter.md index 99920dfb..6a26ad97 100644 --- a/docs/content/en/docs/core-components/api-reporter.md +++ b/docs/content/en/docs/core-components/api-reporter.md @@ -73,7 +73,6 @@ Response data model: |ended |timestamp |string |The time at which the execution of the playbook ended (if so) |status |execution-status-enum |string |The current [status](#execution-stataus) of the execution |status_text |explanation |string |A natural language explanation of the current status or related info -|error |error |string |Error raised along the execution of the playbook at execution level |step_results |step_results |dictionary |Map of step-id to related [step execution data](#step-execution-data) |request_interval |seconds |integer |Suggests the polling interval for the next request (default suggested is 5 seconds). diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 74e8a0ce..6bcd9f1b 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -110,7 +110,7 @@ func (cacheReporter *Cache) ReportWorkflowEnd(executionId uuid.UUID, playbook ca } if workflowError != nil { - executionEntry.PlaybookResult = workflowError + executionEntry.Error = workflowError executionEntry.Status = cache_report.Failed } else { executionEntry.Status = cache_report.SuccessfullyExecuted diff --git a/models/api/reporter.go b/models/api/reporter.go index e8f19ac7..0d6d73fc 100644 --- a/models/api/reporter.go +++ b/models/api/reporter.go @@ -5,6 +5,7 @@ import ( "fmt" "soarca/models/cacao" cache_model "soarca/models/cache" + "time" ) type Status uint8 @@ -35,30 +36,28 @@ const ( ) type PlaybookExecutionReport struct { - Type string - ExecutionId string - PlaybookId string - Started string - Ended string - Status string - StatusText string - StepResults map[string]StepExecutionReport - Error string - RequestInterval int + Type string `bson:"type" json:"type"` + ExecutionId string `bson:"execution_id" json:"execution_id"` + PlaybookId string `bson:"playbook_id" json:"playbook_id"` + Started time.Time `bson:"started" json:"started"` + Ended time.Time `bson:"ended" json:"ended"` + Status string `bson:"status" json:"status"` + StatusText string `bson:"status_text" json:"status_text"` + StepResults map[string]StepExecutionReport `bson:"step_results" json:"step_results"` + RequestInterval int `bson:"request_interval" json:"request_interval"` } type StepExecutionReport struct { - ExecutionId string - StepId string - Started string - Ended string - Status string - StatusText string - ExecutedBy string - CommandsB64 []string - Error string - Variables map[string]cacao.Variable - AutomatedExecution string + ExecutionId string `bson:"execution_id" json:"execution_id"` + StepId string `bson:"step_id" json:"step_id"` + Started time.Time `bson:"started" json:"started"` + Ended time.Time `bson:"ended" json:"ended"` + Status string `bson:"status" json:"status"` + StatusText string `bson:"status_text" json:"status_text"` + ExecutedBy string `bson:"executed_by" json:"executed_by"` + CommandsB64 []string `bson:"commands_b64" json:"commands_b64"` + Variables map[string]cacao.Variable `bson:"variables" json:"variables"` + AutomatedExecution bool `bson:"automated_execution" json:"automated_execution"` // Make sure we can have a playbookID for playbook actions, and also // the execution ID for the invoked playbook } diff --git a/models/cache/cache.go b/models/cache/cache.go index 263454eb..a678bfbc 100644 --- a/models/cache/cache.go +++ b/models/cache/cache.go @@ -21,13 +21,13 @@ const ( ) type ExecutionEntry struct { - ExecutionId uuid.UUID - PlaybookId string - Started time.Time - Ended time.Time - StepResults map[string]StepResult - PlaybookResult error - Status Status + ExecutionId uuid.UUID + PlaybookId string + Started time.Time + Ended time.Time + StepResults map[string]StepResult + Error error + Status Status } type StepResult struct { diff --git a/routes/playbook/playbook_api.go b/routes/playbook/playbook_api.go index 1f5cb845..393d61f7 100644 --- a/routes/playbook/playbook_api.go +++ b/routes/playbook/playbook_api.go @@ -11,13 +11,6 @@ import ( "github.com/gin-gonic/gin" ) -// TODOs -// Add all command types to cacao model -// Change api parsing model to actual data types and add json bson strings for formatting -// Refactor reporter parsing in getting info from cache via now using new model -// Remove copy of cache entries and just pass the objects -// Remove Error from step cache report - // A PlaybookController implements the playbook API endpoints is dependent on a database. type playbookController struct { playbookRepo playbookrepository.IPlaybookRepository diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 6318c45f..8c27e171 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -13,6 +13,11 @@ import ( "soarca/logger" ) +// TODOs +// Change api parsing model to actual data types and add json bson strings for formatting +// Refactor reporter parsing in getting info from cache via now using new model +// Remove copy of cache entries and just pass the objects + var log *logger.Log type Empty struct{} diff --git a/routes/reporter/reporter_parser.go b/routes/reporter/reporter_parser.go index 1ffc36a6..4d89da43 100644 --- a/routes/reporter/reporter_parser.go +++ b/routes/reporter/reporter_parser.go @@ -12,13 +12,13 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P if err != nil { return api_model.PlaybookExecutionReport{}, err } + playbookStatusText, err := api_model.GetCacheStatusText(playbookStatus, api_model.ReportLevelPlaybook) if err != nil { return api_model.PlaybookExecutionReport{}, err } - playbookErrorStr := "" - if cacheEntry.PlaybookResult != nil { - playbookErrorStr = cacheEntry.PlaybookResult.Error() + if cacheEntry.Error != nil { + playbookStatusText = playbookStatusText + " - error: " + cacheEntry.Error.Error() } stepResults, err := parseCacheStepEntries(cacheEntry.StepResults) @@ -30,11 +30,10 @@ func parseCachePlaybookEntry(cacheEntry cache_model.ExecutionEntry) (api_model.P Type: "execution_status", ExecutionId: cacheEntry.ExecutionId.String(), PlaybookId: cacheEntry.PlaybookId, - Started: cacheEntry.Started.String(), - Ended: cacheEntry.Ended.String(), + Started: cacheEntry.Started, + Ended: cacheEntry.Ended, Status: playbookStatus, StatusText: playbookStatusText, - Error: playbookErrorStr, StepResults: stepResults, RequestInterval: defaultRequestInterval, } @@ -54,29 +53,21 @@ func parseCacheStepEntries(cacheStepEntries map[string]cache_model.StepResult) ( return map[string]api_model.StepExecutionReport{}, err } - stepError := stepEntry.Error - stepErrorStr := "" - if stepError != nil { - stepErrorStr = stepError.Error() - } - - automatedExecution := "true" - if !stepEntry.IsAutomated { - automatedExecution = "false" + if stepEntry.Error != nil { + stepStatusText = stepStatusText + " - error: " + stepEntry.Error.Error() } parsedEntries[stepId] = api_model.StepExecutionReport{ ExecutionId: stepEntry.ExecutionId.String(), StepId: stepEntry.StepId, - Started: stepEntry.Started.String(), - Ended: stepEntry.Ended.String(), + Started: stepEntry.Started, + Ended: stepEntry.Ended, Status: stepStatus, StatusText: stepStatusText, ExecutedBy: "soarca", CommandsB64: stepEntry.CommandsB64, - Error: stepErrorStr, Variables: stepEntry.Variables, - AutomatedExecution: automatedExecution, + AutomatedExecution: stepEntry.IsAutomated, } } return parsedEntries, nil diff --git a/test/unittest/reporters/downstream_reporter/cache_test.go b/test/unittest/reporters/downstream_reporter/cache_test.go index 6f0b3d3c..3050a271 100644 --- a/test/unittest/reporters/downstream_reporter/cache_test.go +++ b/test/unittest/reporters/downstream_reporter/cache_test.go @@ -100,13 +100,13 @@ func TestReportWorkflowStartFirst(t *testing.T) { expectedEnded, _ := time.Parse(layout, "0001-01-01T00:00:00Z") expectedExecutions := []cache_model.ExecutionEntry{ { - ExecutionId: executionId0, - PlaybookId: "test", - Started: expectedStarted, - Ended: expectedEnded, - StepResults: map[string]cache_model.StepResult{}, - PlaybookResult: nil, - Status: 2, + ExecutionId: executionId0, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + Error: nil, + Status: 2, }, } @@ -207,13 +207,13 @@ func TestReportWorkflowStartFifo(t *testing.T) { for _, executionId := range executionIds[:len(executionIds)-1] { t.Log(executionId) entry := cache_model.ExecutionEntry{ - ExecutionId: executionId, - PlaybookId: "test", - Started: expectedStarted, - Ended: expectedEnded, - StepResults: map[string]cache_model.StepResult{}, - PlaybookResult: nil, - Status: 2, + ExecutionId: executionId, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + Error: nil, + Status: 2, } expectedExecutionsFull = append(expectedExecutionsFull, entry) } @@ -222,13 +222,13 @@ func TestReportWorkflowStartFifo(t *testing.T) { for _, executionId := range executionIds[1:] { t.Log(executionId) entry := cache_model.ExecutionEntry{ - ExecutionId: executionId, - PlaybookId: "test", - Started: expectedStarted, - Ended: expectedEnded, - StepResults: map[string]cache_model.StepResult{}, - PlaybookResult: nil, - Status: 2, + ExecutionId: executionId, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + Error: nil, + Status: 2, } expectedExecutionsFifo = append(expectedExecutionsFifo, entry) } diff --git a/test/unittest/routes/reporter_api/reporter_api_invocation_test.go b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go index d1ea77cb..abd5f288 100644 --- a/test/unittest/routes/reporter_api/reporter_api_invocation_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_invocation_test.go @@ -95,22 +95,21 @@ func TestGetExecutionReportInvocation(t *testing.T) { app.ServeHTTP(recorder, request) expectedResponse := `{ - "Type":"execution_status", - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "PlaybookId":"test", - "Started":"2014-11-12 11:45:26.371 +0000 UTC", - "Ended":"0001-01-01 00:00:00 +0000 UTC", - "Status":"ongoing", - "StatusText":"this playbook is currently being executed", - "StepResults":{ + "type":"execution_status", + "execution_id":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "playbook_id":"test", + "started":"2014-11-12T11:45:26.371Z", + "ended":"0001-01-01T00:00:00Z", + "status":"ongoing", + "status_text":"this playbook is currently being executed", + "step_results":{ "action--test":{ - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "StepId": "action--test", - "Started": "2014-11-12 11:45:26.371 +0000 UTC", - "Ended": "2014-11-12 11:45:26.371 +0000 UTC", - "Status": "successfully_executed", - "StatusText": "step execution completed successfully", - "Error":"", + "execution_id":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "step_id": "action--test", + "started": "2014-11-12T11:45:26.371Z", + "ended": "2014-11-12T11:45:26.371Z", + "status": "successfully_executed", + "status_text": "step execution completed successfully", "Variables":{ "var1":{ "type":"string", @@ -118,13 +117,12 @@ func TestGetExecutionReportInvocation(t *testing.T) { "value":"testing" } }, - "CommandsB64" : [], - "AutomatedExecution" : "true", - "ExecutedBy" : "soarca" + "commands_b64" : [], + "automated_execution" : true, + "executed_by" : "soarca" } }, - "Error":"", - "RequestInterval":5 + "request_interval":5 }` expectedResponseData := api_model.PlaybookExecutionReport{} err = json.Unmarshal([]byte(expectedResponse), &expectedResponseData) diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index 3d63904c..b3fb2663 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -103,13 +103,13 @@ func TestGetExecutions(t *testing.T) { for _, executionId := range executionIds { t.Log(executionId) entry := cache_model.ExecutionEntry{ - ExecutionId: executionId, - PlaybookId: "test", - Started: expectedStarted, - Ended: expectedEnded, - StepResults: map[string]cache_model.StepResult{}, - PlaybookResult: nil, - Status: 2, + ExecutionId: executionId, + PlaybookId: "test", + Started: expectedStarted, + Ended: expectedEnded, + StepResults: map[string]cache_model.StepResult{}, + Error: nil, + Status: 2, } expectedExecutions = append(expectedExecutions, entry) } @@ -253,36 +253,34 @@ func TestGetExecutionReport(t *testing.T) { reporter.Routes(app, cacheReporter) expected := `{ - "Type":"execution_status", - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "PlaybookId":"test", - "Started":"2014-11-12 11:45:26.371 +0000 UTC", - "Ended":"0001-01-01 00:00:00 +0000 UTC", - "Status":"ongoing", - "StatusText":"this playbook is currently being executed", - "StepResults":{ + "type":"execution_status", + "execution_id":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "playbook_id":"test", + "started":"2014-11-12T11:45:26.371Z", + "ended":"0001-01-01T00:00:00Z", + "status":"ongoing", + "status_text":"this playbook is currently being executed", + "step_results":{ "action--test":{ - "ExecutionId":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", - "StepId":"action--test", - "Started":"2014-11-12 11:45:26.371 +0000 UTC", - "Ended":"2014-11-12 11:45:26.371 +0000 UTC", - "Status":"successfully_executed", - "StatusText": "step execution completed successfully", - "Error":"", - "Variables":{ + "execution_id":"6ba7b810-9dad-11d1-80b4-00c04fd430c0", + "step_id":"action--test", + "started":"2014-11-12T11:45:26.371Z", + "ended":"2014-11-12T11:45:26.371Z", + "status":"successfully_executed", + "status_text": "step execution completed successfully", + "variables":{ "var1":{ "type":"string", "name":"var1", "value":"testing" } }, - "CommandsB64" : ["c3NoIGxzIC1sYQ=="], - "AutomatedExecution" : "true", - "ExecutedBy" : "soarca" + "commands_b64" : ["c3NoIGxzIC1sYQ=="], + "automated_execution" : true, + "executed_by" : "soarca" } }, - "Error":"", - "RequestInterval":5 + "request_interval":5 }` expectedData := api_model.PlaybookExecutionReport{} err = json.Unmarshal([]byte(expected), &expectedData) @@ -291,6 +289,12 @@ func TestGetExecutionReport(t *testing.T) { t.Log("Could not parse data to JSON") t.Fail() } + t.Log("expected") + b, err := json.MarshalIndent(expectedData, "", " ") + if err != nil { + fmt.Println(err) + } + fmt.Print(string(b)) request, err := http.NewRequest("GET", fmt.Sprintf("/reporter/%s", executionId0), nil) if err != nil { @@ -306,6 +310,8 @@ func TestGetExecutionReport(t *testing.T) { t.Log("Could not parse data to JSON") t.Fail() } + t.Log("received") + t.Log(receivedData) assert.Equal(t, expectedData, receivedData) From 99907713d73f017f0b6f2f499a90e9e8fce654d6 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Wed, 15 May 2024 11:52:57 +0200 Subject: [PATCH 38/40] remove deep copy of cache entries now passed by reference to api --- .../downstream_reporter/cache/cache.go | 22 ++++--------------- routes/reporter/reporter_api.go | 5 ----- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 6bcd9f1b..88f9835f 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -2,7 +2,6 @@ package cache import ( b64 "encoding/base64" - "encoding/json" "errors" "reflect" "soarca/logger" @@ -203,9 +202,10 @@ func (cacheReporter *Cache) GetExecutions() ([]cache_report.ExecutionEntry, erro // NOTE: fetched via fifo register key reference as is ordered array, // needed to test and report back ordered executions stored for _, executionEntryKey := range cacheReporter.fifoRegister { - entry, err := cacheReporter.copyExecutionEntry(executionEntryKey) - if err != nil { - return []cache_report.ExecutionEntry{}, err + // NOTE: cached executions are passed by reference, so they must not be modified + entry, present := cacheReporter.Cache[executionEntryKey] + if present != true { + return []cache_report.ExecutionEntry{}, errors.New("internal error. cache fifo register and cache executions mismatch.") } executions = append(executions, entry) } @@ -221,17 +221,3 @@ func (cacheReporter *Cache) GetExecutionReport(executionKey uuid.UUID) (cache_re return report, nil } - -func (cacheReporter *Cache) copyExecutionEntry(executionKeyStr string) (cache_report.ExecutionEntry, error) { - // NOTE: Deep copy via JSON serialization and de-serialization, longer computation time than custom function - // might want to implement custom deep copy in future - origJSON, err := json.Marshal(cacheReporter.Cache[executionKeyStr]) - if err != nil { - return cache_report.ExecutionEntry{}, err - } - clone := cache_report.ExecutionEntry{} - if err = json.Unmarshal(origJSON, &clone); err != nil { - return cache_report.ExecutionEntry{}, err - } - return clone, nil -} diff --git a/routes/reporter/reporter_api.go b/routes/reporter/reporter_api.go index 8c27e171..6318c45f 100644 --- a/routes/reporter/reporter_api.go +++ b/routes/reporter/reporter_api.go @@ -13,11 +13,6 @@ import ( "soarca/logger" ) -// TODOs -// Change api parsing model to actual data types and add json bson strings for formatting -// Refactor reporter parsing in getting info from cache via now using new model -// Remove copy of cache entries and just pass the objects - var log *logger.Log type Empty struct{} From 4c0c7361f45e92d0958aa0999a3675ee3fa283c6 Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Wed, 15 May 2024 12:05:05 +0200 Subject: [PATCH 39/40] fix lint bool comparison complaint --- internal/reporter/downstream_reporter/cache/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/reporter/downstream_reporter/cache/cache.go b/internal/reporter/downstream_reporter/cache/cache.go index 88f9835f..add6daf4 100644 --- a/internal/reporter/downstream_reporter/cache/cache.go +++ b/internal/reporter/downstream_reporter/cache/cache.go @@ -204,7 +204,7 @@ func (cacheReporter *Cache) GetExecutions() ([]cache_report.ExecutionEntry, erro for _, executionEntryKey := range cacheReporter.fifoRegister { // NOTE: cached executions are passed by reference, so they must not be modified entry, present := cacheReporter.Cache[executionEntryKey] - if present != true { + if !present { return []cache_report.ExecutionEntry{}, errors.New("internal error. cache fifo register and cache executions mismatch.") } executions = append(executions, entry) From 98b95c1914962011e5a06e220920f7d7d512cfdf Mon Sep 17 00:00:00 2001 From: "luca.morgese@tno.nl" Date: Wed, 15 May 2024 12:27:41 +0200 Subject: [PATCH 40/40] change reporting status int to status type in tests --- test/unittest/routes/reporter_api/reporter_api_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittest/routes/reporter_api/reporter_api_test.go b/test/unittest/routes/reporter_api/reporter_api_test.go index b3fb2663..de48c76f 100644 --- a/test/unittest/routes/reporter_api/reporter_api_test.go +++ b/test/unittest/routes/reporter_api/reporter_api_test.go @@ -109,7 +109,7 @@ func TestGetExecutions(t *testing.T) { Ended: expectedEnded, StepResults: map[string]cache_model.StepResult{}, Error: nil, - Status: 2, + Status: cache_model.Ongoing, } expectedExecutions = append(expectedExecutions, entry) }