diff --git a/docs/content/en/docs/core-components/executer.md b/docs/content/en/docs/core-components/executer.md index e6d1200f..0ccd56d5 100644 --- a/docs/content/en/docs/core-components/executer.md +++ b/docs/content/en/docs/core-components/executer.md @@ -202,7 +202,11 @@ The result of the step comparison will be returned to the decomposer. A result c ### While condition executor The if-condition executor will process a cacao while-condition step and determine it's output. -The result of the step comparison will be returned to the decomposer. A result can be either a next step id and/or error status. +The result of the step comparison will be returned to the decomposer. A result can be either a next step id and/or error status. Only STIX comparison expressions are implemented at this time. + +{{% alert title="Warning" color="warning" %}} +Note only [Comparison Expression](http://docs.oasis-open.org/cti/stix/v2.0/cs01/part5-stix-patterning/stix-v2.0-cs01-part5-stix-patterning.html#_Toc496717749) are implemented for all CACAO variable types. +{{% /alert %}} ### Parallel step executor The parallel executor will execute the parallel step. This wil be done in sequence to simplify implementation. As parallel steps must not be depended on each other sequential execution is possible. Later this will be changed. \ No newline at end of file diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 757ac379..48b21264 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -15,6 +15,7 @@ import ( "soarca/internal/capability/ssh" "soarca/internal/decomposer" "soarca/internal/executors/action" + "soarca/internal/executors/condition" "soarca/internal/executors/playbook_action" "soarca/internal/fin/protocol" "soarca/internal/guid" @@ -22,6 +23,7 @@ import ( "soarca/logger" "soarca/utils" httpUtil "soarca/utils/http" + "soarca/utils/stix/expression/comparison" downstreamReporter "soarca/internal/reporter/downstream_reporter" @@ -78,8 +80,14 @@ func (controller *Controller) NewDecomposer() decomposer.IDecomposer { actionExecutor := action.New(capabilities, reporter) playbookActionExecutor := playbook_action.New(controller, controller, reporter) + stixComparison := comparison.New() + conditionExecutor := condition.New(stixComparison, reporter) guid := new(guid.Guid) - decompose := decomposer.New(actionExecutor, playbookActionExecutor, guid, reporter) + decompose := decomposer.New(actionExecutor, + playbookActionExecutor, + conditionExecutor, + guid, + reporter) return decompose } diff --git a/internal/decomposer/decomposer.go b/internal/decomposer/decomposer.go index c33dc3c0..a1ee6545 100644 --- a/internal/decomposer/decomposer.go +++ b/internal/decomposer/decomposer.go @@ -7,6 +7,7 @@ import ( "soarca/internal/executors" "soarca/internal/executors/action" + "soarca/internal/executors/condition" "soarca/internal/guid" "soarca/internal/reporter" "soarca/logger" @@ -39,12 +40,16 @@ func init() { func New(actionExecutor action.IExecuter, playbookActionExecutor executors.IPlaybookExecuter, - guid guid.IGuid, reporter reporter.IWorkflowReporter) *Decomposer { + condition condition.IExecuter, + guid guid.IGuid, + reporter reporter.IWorkflowReporter) *Decomposer { return &Decomposer{actionExecutor: actionExecutor, playbookActionExecutor: playbookActionExecutor, + conditionExecutor: condition, guid: guid, - reporter: reporter} + reporter: reporter, + } } type Decomposer struct { @@ -52,6 +57,7 @@ type Decomposer struct { details ExecutionDetails actionExecutor action.IExecuter playbookActionExecutor executors.IPlaybookExecuter + conditionExecutor condition.IExecuter guid guid.IGuid reporter reporter.IWorkflowReporter } @@ -157,6 +163,15 @@ func (decomposer *Decomposer) ExecuteStep(step cacao.Step, scopeVariables cacao. return decomposer.actionExecutor.Execute(metadata, actionMetadata) case cacao.StepTypePlaybookAction: return decomposer.playbookActionExecutor.Execute(metadata, step, variables) + case cacao.StepTypeIfCondition: + stepId, branch, err := decomposer.conditionExecutor.Execute(metadata, step, variables) + if err != nil { + return cacao.NewVariables(), err + } + if branch { + return decomposer.ExecuteBranch(stepId, variables) + } + return variables, nil default: // NOTE: This currently silently handles unknown step types. Should we return an error instead? return cacao.NewVariables(), nil diff --git a/internal/executors/condition/condition.go b/internal/executors/condition/condition.go new file mode 100644 index 00000000..2ddb4b00 --- /dev/null +++ b/internal/executors/condition/condition.go @@ -0,0 +1,76 @@ +package condition + +import ( + "errors" + "fmt" + "reflect" + "soarca/internal/reporter" + "soarca/logger" + "soarca/models/cacao" + "soarca/models/execution" + "soarca/utils/stix/expression/comparison" +) + +var component = reflect.TypeOf(Executor{}).PkgPath() +var log *logger.Log + +func init() { + log = logger.Logger(component, logger.Info, "", logger.Json) +} + +func New(comparison comparison.IComparison, + reporter reporter.IStepReporter) *Executor { + return &Executor{comparison: comparison, + reporter: reporter} +} + +type IExecuter interface { + Execute(metadata execution.Metadata, + step cacao.Step, variables cacao.Variables) (string, bool, error) +} + +type Executor struct { + comparison comparison.IComparison + reporter reporter.IStepReporter +} + +func (executor *Executor) Execute(meta execution.Metadata, step cacao.Step, variables cacao.Variables) (string, bool, error) { + + if step.Type != cacao.StepTypeIfCondition { + err := errors.New("the provided step type is not compatible with this executor") + log.Error(err) + return step.OnFailure, false, err + } + + executor.reporter.ReportStepStart(meta.ExecutionId, step, variables) + + result, err := executor.comparison.Evaluate(step.Condition, variables) + + // We are reporting early to not have double reporting + executor.reporter.ReportStepEnd(meta.ExecutionId, + step, + variables, + err) + + if err != nil { + log.Error(err) + return "", false, err + } + + log.Debug("the result was: ", fmt.Sprint(result)) + + if result { + if step.OnTrue != "" { + log.Trace("will return on true step ", step.OnTrue) + return step.OnTrue, true, nil + } + } else { + if step.OnFalse != "" { + log.Trace("will return on false step ", step.OnFalse) + return step.OnFalse, true, nil + } + } + log.Trace("will return on completion step ", step.OnCompletion) + + return step.OnCompletion, false, nil +} diff --git a/test/unittest/decomposer/decomposer_test.go b/test/unittest/decomposer/decomposer_test.go index a2fe38db..90ec49a9 100644 --- a/test/unittest/decomposer/decomposer_test.go +++ b/test/unittest/decomposer/decomposer_test.go @@ -10,6 +10,7 @@ import ( "soarca/models/cacao" "soarca/models/execution" "soarca/test/unittest/mocks/mock_executor" + mock_condition_executor "soarca/test/unittest/mocks/mock_executor/condition" mock_playbook_action_executor "soarca/test/unittest/mocks/mock_executor/playbook_action" "soarca/test/unittest/mocks/mock_guid" "soarca/test/unittest/mocks/mock_reporter" @@ -21,6 +22,7 @@ import ( func TestExecutePlaybook(t *testing.T) { mock_action_executor := new(mock_executor.Mock_Action_Executor) mock_playbook_action_executor := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) + mock_condition_executor := new(mock_condition_executor.Mock_Condition) uuid_mock := new(mock_guid.Mock_Guid) mock_reporter := new(mock_reporter.Mock_Reporter) @@ -37,7 +39,9 @@ func TestExecutePlaybook(t *testing.T) { decomposer := decomposer.New(mock_action_executor, mock_playbook_action_executor, - uuid_mock, mock_reporter) + mock_condition_executor, + uuid_mock, + mock_reporter) step1 := cacao.Step{ Type: "action", @@ -117,6 +121,7 @@ func TestExecutePlaybook(t *testing.T) { func TestExecutePlaybookMultiStep(t *testing.T) { mock_action_executor := new(mock_executor.Mock_Action_Executor) mock_playbook_action_executor := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) + mock_condition_executor := new(mock_condition_executor.Mock_Condition) uuid_mock := new(mock_guid.Mock_Guid) mock_reporter := new(mock_reporter.Mock_Reporter) @@ -144,7 +149,9 @@ func TestExecutePlaybookMultiStep(t *testing.T) { decomposer := decomposer.New(mock_action_executor, mock_playbook_action_executor, - uuid_mock, mock_reporter) + mock_condition_executor, + uuid_mock, + mock_reporter) step1 := cacao.Step{ Type: "action", @@ -262,6 +269,7 @@ Test with an Empty OnCompletion will result in not executing the step. func TestExecuteEmptyMultiStep(t *testing.T) { mock_action_executor2 := new(mock_executor.Mock_Action_Executor) mock_playbook_action_executor2 := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) + mock_condition_executor := new(mock_condition_executor.Mock_Condition) uuid_mock2 := new(mock_guid.Mock_Guid) mock_reporter := new(mock_reporter.Mock_Reporter) @@ -288,7 +296,9 @@ func TestExecuteEmptyMultiStep(t *testing.T) { decomposer2 := decomposer.New(mock_action_executor2, mock_playbook_action_executor2, - uuid_mock2, mock_reporter) + mock_condition_executor, + uuid_mock2, + mock_reporter) step1 := cacao.Step{ Type: "ssh", @@ -332,6 +342,7 @@ Test with an not occuring on completion id will result in not executing the step func TestExecuteIllegalMultiStep(t *testing.T) { mock_action_executor2 := new(mock_executor.Mock_Action_Executor) mock_playbook_action_executor2 := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) + mock_condition_executor := new(mock_condition_executor.Mock_Condition) uuid_mock2 := new(mock_guid.Mock_Guid) mock_reporter := new(mock_reporter.Mock_Reporter) @@ -348,7 +359,9 @@ func TestExecuteIllegalMultiStep(t *testing.T) { decomposer2 := decomposer.New(mock_action_executor2, mock_playbook_action_executor2, - uuid_mock2, mock_reporter) + mock_condition_executor, + uuid_mock2, + mock_reporter) step1 := cacao.Step{ Type: "action", @@ -386,6 +399,7 @@ func TestExecuteIllegalMultiStep(t *testing.T) { func TestExecutePlaybookAction(t *testing.T) { mock_action_executor := new(mock_executor.Mock_Action_Executor) mock_playbook_action_executor := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) + mock_condition_executor := new(mock_condition_executor.Mock_Condition) uuid_mock := new(mock_guid.Mock_Guid) mock_reporter := new(mock_reporter.Mock_Reporter) expectedVariables := cacao.Variable{ @@ -396,7 +410,9 @@ func TestExecutePlaybookAction(t *testing.T) { decomposer := decomposer.New(mock_action_executor, mock_playbook_action_executor, - uuid_mock, mock_reporter) + mock_condition_executor, + uuid_mock, + mock_reporter) step1 := cacao.Step{ Type: "playbook-action", @@ -444,3 +460,177 @@ func TestExecutePlaybookAction(t *testing.T) { assert.Equal(t, found, true) assert.Equal(t, value.Value, "value") } + +func TestExecuteIfCondition(t *testing.T) { + + mock_action_executor := new(mock_executor.Mock_Action_Executor) + mock_playbook_action_executor := new(mock_playbook_action_executor.Mock_PlaybookActionExecutor) + mock_condition_executor := new(mock_condition_executor.Mock_Condition) + uuid_mock := new(mock_guid.Mock_Guid) + mock_reporter := new(mock_reporter.Mock_Reporter) + expectedVariables := cacao.Variable{ + Type: "string", + Name: "__var1__", + Value: "testing", + } + + // returned from step + expectedVariables2 := cacao.Variable{ + Type: "string", + Name: "__var2__", + Value: "testing2", + } + + expectedCommand := cacao.Command{ + Type: "ssh", + Command: "ssh ls -la", + } + + expectedTarget := cacao.AgentTarget{ + Name: "sometarget", + AuthInfoIdentifier: "id", + } + + expectedAgent := cacao.AgentTarget{ + Type: "soarca", + Name: "soarca-ssh", + } + + decomposer := decomposer.New(mock_action_executor, + mock_playbook_action_executor, + mock_condition_executor, + uuid_mock, + mock_reporter) + + end := cacao.Step{ + Type: cacao.StepTypeEnd, + ID: "end--test", + Name: "end step", + } + + endTrue := cacao.Step{ + Type: cacao.StepTypeEnd, + ID: "end--true", + Name: "end branch true step", + } + + stepTrue := cacao.Step{ + Type: cacao.StepTypeAction, + ID: "action--step-true", + Name: "ssh-tests", + Commands: []cacao.Command{expectedCommand}, + Targets: []string{expectedTarget.ID}, + StepVariables: cacao.NewVariables(expectedVariables), + OnCompletion: endTrue.ID, + } + + endFalse := cacao.Step{ + Type: cacao.StepTypeEnd, + ID: "end--false", + Name: "end branch false step", + } + + stepFalse := cacao.Step{ + Type: cacao.StepTypeAction, + ID: "action--step-false", + Name: "ssh-tests", + Commands: []cacao.Command{expectedCommand}, + Targets: []string{expectedTarget.ID}, + StepVariables: cacao.NewVariables(expectedVariables), + OnCompletion: endFalse.ID, + } + + stepCompletion := cacao.Step{ + Type: cacao.StepTypeAction, + ID: "action--step-completion", + Name: "ssh-tests", + Commands: []cacao.Command{expectedCommand}, + Targets: []string{expectedTarget.ID}, + StepVariables: cacao.NewVariables(expectedVariables), + OnCompletion: end.ID, + } + + stepIf := cacao.Step{ + Type: cacao.StepTypeIfCondition, + ID: "if-condition--test", + Name: "if condition", + StepVariables: cacao.NewVariables(expectedVariables), + Condition: "__var1__:value = testing", + OnTrue: stepTrue.ID, + OnFalse: stepFalse.ID, + OnCompletion: stepCompletion.ID, + } + + start := cacao.Step{ + Type: cacao.StepTypeStart, + ID: "start--test", + Name: "start step", + OnCompletion: stepIf.ID, + } + + playbook := cacao.Playbook{ + ID: "test", + Type: "test", + Name: "playbook-test", + WorkflowStart: start.ID, + Workflow: map[string]cacao.Step{start.ID: start, + stepIf.ID: stepIf, + stepTrue.ID: stepTrue, + stepFalse.ID: stepFalse, + stepCompletion.ID: stepCompletion, + end.ID: end, + endTrue.ID: endTrue, + endFalse.ID: endFalse}, + AgentDefinitions: map[string]cacao.AgentTarget{expectedAgent.ID: expectedAgent}, + TargetDefinitions: map[string]cacao.AgentTarget{expectedTarget.ID: expectedTarget}, + } + + executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + metaStepIf := execution.Metadata{ExecutionId: executionId, PlaybookId: "test", StepId: stepIf.ID} + + uuid_mock.On("New").Return(executionId) + mock_reporter.On("ReportWorkflowStart", executionId, playbook).Return() + + mock_condition_executor.On("Execute", + metaStepIf, + stepIf, + cacao.NewVariables(expectedVariables)).Return(stepTrue.ID, true, nil) + + stepTrueDetails := action.PlaybookStepMetadata{ + Step: stepTrue, + Targets: playbook.TargetDefinitions, + Auth: playbook.AuthenticationInfoDefinitions, + Agent: expectedAgent, + Variables: cacao.NewVariables(expectedVariables), + } + + metaStepTrue := execution.Metadata{ExecutionId: executionId, PlaybookId: "test", StepId: stepTrue.ID} + + mock_action_executor.On("Execute", + metaStepTrue, + stepTrueDetails).Return(cacao.NewVariables(expectedVariables2), nil) + + stepCompletionDetails := action.PlaybookStepMetadata{ + Step: stepCompletion, + Targets: playbook.TargetDefinitions, + Auth: playbook.AuthenticationInfoDefinitions, + Agent: expectedAgent, + Variables: cacao.NewVariables(expectedVariables, expectedVariables2), + } + + metaStepCompletion := execution.Metadata{ExecutionId: executionId, PlaybookId: "test", StepId: stepCompletion.ID} + + mock_action_executor.On("Execute", + metaStepCompletion, + stepCompletionDetails).Return(cacao.NewVariables(), nil) + mock_reporter.On("ReportWorkflowEnd", executionId, playbook, nil).Return() + details, err := decomposer.Execute(playbook) + uuid_mock.AssertExpectations(t) + fmt.Println(err) + assert.Equal(t, err, nil) + assert.Equal(t, details.ExecutionId, executionId) + mock_reporter.AssertExpectations(t) + mock_condition_executor.AssertExpectations(t) + mock_action_executor.AssertExpectations(t) + +} diff --git a/test/unittest/executor/condition/condition_executor_test.go b/test/unittest/executor/condition/condition_executor_test.go new file mode 100644 index 00000000..d9073f94 --- /dev/null +++ b/test/unittest/executor/condition/condition_executor_test.go @@ -0,0 +1,110 @@ +package condition_test + +import ( + "errors" + "soarca/internal/executors/condition" + "soarca/models/cacao" + "soarca/models/execution" + "soarca/test/unittest/mocks/mock_reporter" + mock_stix "soarca/test/unittest/mocks/mock_utils/stix" + "testing" + + "github.com/go-playground/assert/v2" + "github.com/google/uuid" +) + +func TestExecuteConditionTrue(t *testing.T) { + mock_stix := new(mock_stix.MockStix) + mock_reporter := new(mock_reporter.Mock_Reporter) + + conditionExecutior := condition.New(mock_stix, mock_reporter) + + executionId := uuid.New() + + meta := execution.Metadata{ExecutionId: executionId, + PlaybookId: "1", + StepId: "2"} + + step := cacao.Step{Type: cacao.StepTypeIfCondition, + Condition: "a = a", + OnTrue: "3", + OnFalse: "4"} + vars := cacao.NewVariables() + + mock_reporter.On("ReportStepStart", executionId, step, vars) + mock_stix.On("Evaluate", "a = a", vars).Return(true, nil) + mock_reporter.On("ReportStepEnd", executionId, step, vars, nil) + + nextStepId, goToBranch, err := conditionExecutior.Execute(meta, step, vars) + assert.Equal(t, nil, err) + assert.Equal(t, true, goToBranch) + assert.Equal(t, "3", nextStepId) + + mock_reporter.AssertExpectations(t) + mock_stix.AssertExpectations(t) + +} + +func TestExecuteConditionFalse(t *testing.T) { + mock_stix := new(mock_stix.MockStix) + mock_reporter := new(mock_reporter.Mock_Reporter) + + conditionExecutior := condition.New(mock_stix, mock_reporter) + + executionId := uuid.New() + + meta := execution.Metadata{ExecutionId: executionId, + PlaybookId: "1", + StepId: "2"} + + step := cacao.Step{Type: cacao.StepTypeIfCondition, + Condition: "a = a", + OnTrue: "3", + OnFalse: "4"} + vars := cacao.NewVariables() + + mock_reporter.On("ReportStepStart", executionId, step, vars) + mock_stix.On("Evaluate", "a = a", vars).Return(false, nil) + mock_reporter.On("ReportStepEnd", executionId, step, vars, nil) + + nextStepId, goToBranch, err := conditionExecutior.Execute(meta, step, vars) + assert.Equal(t, nil, err) + assert.Equal(t, true, goToBranch) + assert.Equal(t, "4", nextStepId) + + mock_reporter.AssertExpectations(t) + mock_stix.AssertExpectations(t) +} + +func TestExecuteConditionError(t *testing.T) { + mock_stix := new(mock_stix.MockStix) + mock_reporter := new(mock_reporter.Mock_Reporter) + + conditionExecutior := condition.New(mock_stix, mock_reporter) + + executionId := uuid.New() + + meta := execution.Metadata{ExecutionId: executionId, + PlaybookId: "1", + StepId: "2"} + + step := cacao.Step{Type: cacao.StepTypeIfCondition, + Condition: "a = a", + OnTrue: "3", + OnFalse: "4"} + vars := cacao.NewVariables() + + evaluationError := errors.New("some ds error") + + mock_reporter.On("ReportStepStart", executionId, step, vars) + mock_stix.On("Evaluate", "a = a", vars).Return(false, evaluationError) + mock_reporter.On("ReportStepEnd", executionId, step, vars, evaluationError) + + nextStepId, goToBranch, err := conditionExecutior.Execute(meta, step, vars) + assert.Equal(t, evaluationError, err) + assert.Equal(t, false, goToBranch) + assert.Equal(t, "", nextStepId) + + mock_reporter.AssertExpectations(t) + mock_stix.AssertExpectations(t) +} diff --git a/test/unittest/mocks/mock_executor/condition/condition_executor.go b/test/unittest/mocks/mock_executor/condition/condition_executor.go new file mode 100644 index 00000000..3774c2c6 --- /dev/null +++ b/test/unittest/mocks/mock_executor/condition/condition_executor.go @@ -0,0 +1,19 @@ +package mock_condition_executor + +import ( + "soarca/models/cacao" + "soarca/models/execution" + + "github.com/stretchr/testify/mock" +) + +type Mock_Condition struct { + mock.Mock +} + +func (executer *Mock_Condition) Execute(metadata execution.Metadata, + step cacao.Step, + variables cacao.Variables) (string, bool, error) { + args := executer.Called(metadata, step, variables) + return args.String(0), args.Bool(1), args.Error(2) +} diff --git a/test/unittest/mocks/mock_utils/stix/mock_stix.go b/test/unittest/mocks/mock_utils/stix/mock_stix.go new file mode 100644 index 00000000..c0fec892 --- /dev/null +++ b/test/unittest/mocks/mock_utils/stix/mock_stix.go @@ -0,0 +1,20 @@ +package mock_stix + +import ( + "soarca/models/cacao" + + "github.com/stretchr/testify/mock" +) + +type MockStix struct { + mock.Mock +} + +type MockHttpRequest struct { + mock.Mock +} + +func (stix *MockStix) Evaluate(expression string, vars cacao.Variables) (bool, error) { + args := stix.Called(expression, vars) + return args.Bool(0), args.Error(1) +} diff --git a/test/unittest/utils/stix/expression/comparison/comparison_test.go b/test/unittest/utils/stix/expression/comparison/comparison_test.go new file mode 100644 index 00000000..ef4b9591 --- /dev/null +++ b/test/unittest/utils/stix/expression/comparison/comparison_test.go @@ -0,0 +1,342 @@ +package comparison_test + +import ( + "errors" + "soarca/models/cacao" + "soarca/utils/stix/expression/comparison" + "testing" + + "github.com/go-playground/assert/v2" +) + +func TestStringEquals(t *testing.T) { + + stix := comparison.New() + + var1 := cacao.Variable{Type: cacao.VariableTypeString} + var1.Value = "a" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = a", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + result, err = stix.Evaluate("__var1__:value = b", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value = 1", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value > b", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value < b", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value <= b", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value >= b", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("a = b", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, errors.New("comparisons can only contain 3 parts as per STIX specification")) + result, err = stix.Evaluate("a = b c", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, errors.New("comparisons can only contain 3 parts as per STIX specification")) + +} + +func TestIntEquals(t *testing.T) { + stix := comparison.New() + + var1 := cacao.Variable{Type: cacao.VariableTypeLong} + var1.Value = "1000" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = 1000", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + result, err = stix.Evaluate("__var1__:value = 9999", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value = 10000", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value > 999", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value < 1001", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value <= 1000", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value >= 1000", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value >= a", vars) + assert.Equal(t, result, false) + assert.NotEqual(t, err, nil) + + result, err = stix.Evaluate("a = b", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, errors.New("comparisons can only contain 3 parts as per STIX specification")) + result, err = stix.Evaluate("a = b c", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, errors.New("comparisons can only contain 3 parts as per STIX specification")) +} + +func TestFloatEquals(t *testing.T) { + stix := comparison.New() + + var1 := cacao.Variable{Type: cacao.VariableTypeFloat} + var1.Value = "1000.0" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = 1000.0", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value = 1000.000000000000000000001", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value = 1000.000001", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, false) + + result, err = stix.Evaluate("__var1__:value = 9999", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value = 10000", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value > 999", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value < 1001", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value <= 1000", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value >= 1000", vars) + assert.Equal(t, result, true) + assert.Equal(t, err, nil) + + result, err = stix.Evaluate("__var1__:value >= a", vars) + assert.Equal(t, result, false) + assert.NotEqual(t, err, nil) + + result, err = stix.Evaluate("a = b", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, errors.New("comparisons can only contain 3 parts as per STIX specification")) + result, err = stix.Evaluate("a = b c", vars) + assert.Equal(t, result, false) + assert.Equal(t, err, errors.New("comparisons can only contain 3 parts as per STIX specification")) +} + +func TestIp4AddressEquals(t *testing.T) { + stix := comparison.New() + var1 := cacao.Variable{Type: cacao.VariableTypeIpv4Address} + var1.Value = "10.0.0.30" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = 10.0.0.30", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value IN 10.0.0.0/8", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value IN 10.30.0.0/16", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, false) + + result, err = stix.Evaluate("__var1__:value != 10.0.0.31", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) +} + +func TestIp6AddressEquals(t *testing.T) { + stix := comparison.New() + var1 := cacao.Variable{Type: cacao.VariableTypeIpv6Address} + var1.Value = "2001:db8::1" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = 2001:db8::1", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value IN 2001:db8::1/64", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value IN 2001:db81::1/64", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, false) + + result, err = stix.Evaluate("__var1__:value != 2001:db8::2", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) +} + +func TestMacAddressEquals(t *testing.T) { + stix := comparison.New() + var1 := cacao.Variable{Type: cacao.VariableTypeMacAddress} + var1.Value = "BC-24-11-00-00-01" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = BC-24-11-00-00-01", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + var2 := cacao.Variable{Type: cacao.VariableTypeMacAddress} + var2.Value = "BC:24:11:00:00:01" + var2.Name = "__var2__" + vars2 := cacao.NewVariables(var2) + + result, err = stix.Evaluate("__var2__:value = BC:24:11:00:00:01", vars2) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + // Mixed notations + result, err = stix.Evaluate("__var1__:value = BC:24:11:00:00:01", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value > BC:24:11:00:00:00", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value < BC:24:11:00:00:02", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) +} + +func TestHashEquals(t *testing.T) { + stix := comparison.New() + md5 := cacao.Variable{Type: cacao.VariableTypeMd5Has} + md5.Value = "d41d8cd98f00b204e9800998ecf8427e" + md5.Name = "__md5__" + + sha1 := cacao.Variable{Type: cacao.VariableTypeMd5Has} + sha1.Value = "da39a3ee5e6b4b0d3255bfef95601890afd80709" + sha1.Name = "__sha1__" + + sha224 := cacao.Variable{Type: cacao.VariableTypeHash} + sha224.Value = "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f" + sha224.Name = "__sha224__" + + sha256 := cacao.Variable{Type: cacao.VariableTypeSha256} + sha256.Value = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + sha256.Name = "__sha256__" + + sha384 := cacao.Variable{Type: cacao.VariableTypeHash} + sha384.Value = "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" + sha384.Name = "__sha384__" + + sha512 := cacao.Variable{Type: cacao.VariableTypeHash} + sha512.Value = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" + sha512.Name = "__sha512__" + + vars := cacao.NewVariables(md5, sha1, sha224, sha256, sha384, sha512) + + result, err := stix.Evaluate("__md5__:value = d41d8cd98f00b204e9800998ecf8427e", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__sha1__:value = da39a3ee5e6b4b0d3255bfef95601890afd80709", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__sha224__:value = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__sha256__:value = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__sha384__:value = 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__sha512__:value = cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + +} + +func TestUriEquals(t *testing.T) { + stix := comparison.New() + + var1 := cacao.Variable{Type: cacao.VariableTypeUri} + var1.Value = "https://google.com" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = https://google.com", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value = https://example.com", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, false) + + result, err = stix.Evaluate("__var1__:value > https://example.com", vars) + assert.Equal(t, err, errors.New("operator: "+">"+" not valid or implemented")) + assert.Equal(t, result, false) + +} + +func TestUuidEquals(t *testing.T) { + stix := comparison.New() + + var1 := cacao.Variable{Type: cacao.VariableTypeUuid} + var1.Value = "ec887691-9a21-4ccf-8fae-360c13a819d1" + var1.Name = "__var1__" + vars := cacao.NewVariables(var1) + + result, err := stix.Evaluate("__var1__:value = ec887691-9a21-4ccf-8fae-360c13a819d1", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value = ec887691-9a21-4ccf-8fae-360c13a819d2", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, false) + + result, err = stix.Evaluate("__var1__:value != ec887691-9a21-4ccf-8fae-360c13a819d2", vars) + assert.Equal(t, err, nil) + assert.Equal(t, result, true) + + result, err = stix.Evaluate("__var1__:value > ec887691-9a21-4ccf-8fae-360c13a819d2", vars) + assert.Equal(t, err, errors.New("operator: "+">"+" not valid or implemented")) + assert.Equal(t, result, false) + +} diff --git a/utils/stix/expression/comparison/comparison.go b/utils/stix/expression/comparison/comparison.go new file mode 100644 index 00000000..0b16bd6b --- /dev/null +++ b/utils/stix/expression/comparison/comparison.go @@ -0,0 +1,334 @@ +package comparison + +import ( + "errors" + "fmt" + "net" + "net/url" + "reflect" + "soarca/logger" + "soarca/models/cacao" + "strconv" + "strings" + + "github.com/google/uuid" +) + +const ( + Equal = "=" + NotEqual = "!=" + Greater = ">" + Less = "<" + LessOrEqual = "<=" + GreaterOrEqual = ">=" + In = "IN" + Like = "LIKE" + Matches = "MATCHES" + IsSubset = "ISSUBSET" + IsSuperSet = "ISSUPERSET" +) + +var ( + component = reflect.TypeOf(Comparison{}).PkgPath() + log *logger.Log +) + +func init() { + log = logger.Logger(component, logger.Info, "", logger.Json) +} + +type IComparison interface { + Evaluate(string, cacao.Variables) (bool, error) +} + +func New() *Comparison { + return &Comparison{} +} + +type Comparison struct{} + +func (s *Comparison) Evaluate(expression string, vars cacao.Variables) (bool, error) { + parts := strings.Split(expression, " ") + if len(parts) != 3 { + err := errors.New("comparisons can only contain 3 parts as per STIX specification") + return false, err + } + + usedVariable, err := findVariable(parts[0], vars) + if err != nil { + return false, err + } + + parts[0] = vars.Interpolate(parts[0]) + + log.Trace("the interpolated expression is: ", parts) + + switch usedVariable.Type { + case cacao.VariableTypeBool: + return boolCompare(parts) + case cacao.VariableTypeString: + return stringCompare(parts) + case cacao.VariableTypeInt: + return numberCompare(parts) + case cacao.VariableTypeLong: + return numberCompare(parts) + case cacao.VariableTypeFloat: + return floatCompare(parts) + case cacao.VariableTypeIpv4Address: + return compareIp(parts) + case cacao.VariableTypeIpv6Address: + return compareIp(parts) + case cacao.VariableTypeMacAddress: + return compareMac(parts) + case cacao.VariableTypeHash: + // Just compare the hash strings + return compareHash(parts) + case cacao.VariableTypeMd5Has: + // Just compare the hash strings + return compareHash(parts) + case cacao.VariableTypeSha256: + // Just compare the hash strings + return compareHash(parts) + case cacao.VariableTypeHexString: + return stringCompare(parts) + case cacao.VariableTypeUri: + return compareUri(parts) + case cacao.VariableTypeUuid: + return compareUuid(parts) + default: + err := errors.New("variable type is not a cacao variable type") + return false, err + } + +} + +func findVariable(variable string, vars cacao.Variables) (cacao.Variable, error) { + for key, value := range vars { + replacementKey := fmt.Sprint(key, ":value") + if strings.Contains(variable, replacementKey) { + return value, nil + } + + } + return cacao.Variable{}, nil +} + +func stringCompare(parts []string) (bool, error) { + + lhs := parts[0] + comparator := parts[1] + rhs := parts[2] + + switch comparator { + case Equal: + return strings.Compare(lhs, rhs) == 0, nil + case NotEqual: + return strings.Compare(lhs, rhs) != 0, nil + case Greater: + return strings.Compare(lhs, rhs) == 1, nil + case Less: + return strings.Compare(lhs, rhs) == -1, nil + case LessOrEqual: + return strings.Compare(lhs, rhs) <= 0, nil + case GreaterOrEqual: + return strings.Compare(lhs, rhs) >= 0, nil + case In: + return strings.Contains(lhs, rhs), nil + default: + err := errors.New("operator: " + comparator + " not valid or implemented") + return false, err + } +} + +func numberCompare(parts []string) (bool, error) { + lhs, err := strconv.Atoi(parts[0]) + + if err != nil { + return false, err + } + comparator := parts[1] + rhs, err := strconv.Atoi(parts[2]) + if err != nil { + return false, err + } + + switch comparator { + case Equal: + return lhs == rhs, nil + case NotEqual: + return lhs != rhs, nil + case Greater: + return lhs > rhs, nil + case Less: + return lhs < rhs, nil + case LessOrEqual: + return lhs <= rhs, nil + case GreaterOrEqual: + return lhs >= rhs, nil + default: + err := errors.New("operator: " + comparator + " not valid or implemented") + return false, err + } +} + +func floatCompare(parts []string) (bool, error) { + lhs, err := strconv.ParseFloat(parts[0], 64) + + if err != nil { + return false, err + } + comparator := parts[1] + rhs, err := strconv.ParseFloat(parts[2], 64) + if err != nil { + return false, err + } + + switch comparator { + case Equal: + return lhs == rhs, nil + case NotEqual: + return lhs != rhs, nil + case Greater: + return lhs > rhs, nil + case Less: + return lhs < rhs, nil + case LessOrEqual: + return lhs <= rhs, nil + case GreaterOrEqual: + return lhs >= rhs, nil + default: + err := errors.New("operator: " + comparator + " not valid or implemented") + return false, err + } +} + +func boolCompare(parts []string) (bool, error) { + lhs, err := strconv.ParseBool(parts[0]) + if err != nil { + return false, err + } + comparator := parts[1] + rhs, err := strconv.ParseBool(parts[2]) + if err != nil { + return false, err + } + switch comparator { + case Equal: + return lhs == rhs, nil + case NotEqual: + return lhs != rhs, nil + default: + err := errors.New("operator: " + comparator + " not valid or implemented") + return false, err + } +} + +func compareIp(parts []string) (bool, error) { + lhsIp := net.ParseIP(parts[0]) + + comparator := parts[1] + rhsIp := net.ParseIP(parts[2]) + switch comparator { + case Equal: + return lhsIp.Equal(rhsIp), nil + case NotEqual: + return !lhsIp.Equal(rhsIp), nil + case In: + _, rhsNet, err := net.ParseCIDR(parts[2]) + if err != nil { + return false, err + } + return rhsNet.Contains(lhsIp), err + default: + err := errors.New("operator: " + comparator + " not valid or implemented") + return false, err + } +} + +// Validate if they are hardware MAC addresses and do a string compare +func compareMac(parts []string) (bool, error) { + lhs, err := net.ParseMAC(parts[0]) + if err != nil { + return false, err + } + + rhs, err := net.ParseMAC(parts[2]) + if err != nil { + return false, err + } + + newParts := []string{lhs.String(), parts[1], rhs.String()} + + return stringCompare(newParts) +} + +func compareHash(parts []string) (bool, error) { + lhs := parts[0] + rhs := parts[2] + if len(lhs) != len(rhs) { + log.Warning("hash lengths do not match") + } + + switch len(lhs) { + case 32: + log.Trace("MD5 type hash") + case 40: + log.Trace("SHA1 type hash") + case 56: + log.Trace("SHA224 type hash") + case 64: + log.Trace("SHA256 type hash") + case 96: + log.Trace("SHA384 type hash") + case 128: + log.Trace("SHA512 type hash") + default: + log.Warning("unknown hash length of: " + fmt.Sprint(len(lhs))) + } + + return stringCompare(parts) +} + +func compareUri(parts []string) (bool, error) { + lhs, err := url.Parse(parts[0]) + if err != nil { + return false, err + } + comparator := parts[1] + rhs, err := url.Parse(parts[2]) + if err != nil { + return false, err + } + + switch comparator { + case Equal: + return lhs.String() == rhs.String(), nil + case NotEqual: + return lhs.String() != rhs.String(), nil + default: + err := errors.New("operator: " + comparator + " not valid or implemented") + return false, err + } +} + +func compareUuid(parts []string) (bool, error) { + lhs, err := uuid.Parse(parts[0]) + if err != nil { + return false, err + } + comparator := parts[1] + rhs, err := uuid.Parse(parts[2]) + if err != nil { + return false, err + } + + switch comparator { + case Equal: + return lhs == rhs, nil + case NotEqual: + return lhs != rhs, nil + default: + err := errors.New("operator: " + comparator + " not valid or implemented") + return false, err + } +}