From bd33dac909b83a5713fa6058c71a741b44cc9e42 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 3 Aug 2022 16:05:29 +0200 Subject: [PATCH] chg: [workflow] Refactoring and allow running workflow by ID --- app/Controller/Component/ACLComponent.php | 3 + app/Controller/WorkflowsController.php | 70 +++++++---- app/Lib/Tools/WorkflowFormatConverterTool.php | 2 +- app/Model/Workflow.php | 113 +++++++++++++++--- 4 files changed, 150 insertions(+), 38 deletions(-) diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php index 7dc4a1d87..3c5298f3e 100644 --- a/app/Controller/Component/ACLComponent.php +++ b/app/Controller/Component/ACLComponent.php @@ -778,6 +778,9 @@ class ACLComponent extends Component 'moduleView'=> [], 'toggleModule'=> [], 'checkGraph'=> [], + 'executeWorkflow'=> [], + 'debugToggleField'=> [], + 'massToggleField'=> [], ], 'workflowBlueprints' => [ 'add' => [], diff --git a/app/Controller/WorkflowsController.php b/app/Controller/WorkflowsController.php index 6d7917d09..d10c435bd 100644 --- a/app/Controller/WorkflowsController.php +++ b/app/Controller/WorkflowsController.php @@ -108,36 +108,47 @@ class WorkflowsController extends AppController $this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'view')); } - public function editor($trigger_id) + public function editor($id) { - $modules = $this->Workflow->getModulesByType(); - $trigger_ids = Hash::extract($modules['modules_trigger'], '{n}.id'); - if (!in_array($trigger_id, $trigger_ids)) { - return $this->__getFailResponseBasedOnContext( - [__('Unkown trigger %s', $trigger_id)], - null, - 'add', - $trigger_id, - ['controller' => 'workflows', 'action' => 'triggers'] - ); + $trigger_id = false; + $workflow = false; + if (is_numeric($id)) { + $workflow_id = $id; + } else { + $trigger_id = $id; } - $workflow = $this->Workflow->fetchWorkflowByTrigger($trigger_id, false); - if (empty($workflow)) { // Workflow do not exists yet. Create it. - $result = $this->Workflow->addWorkflow([ - 'name' => sprintf('Workflow for trigger %s', $trigger_id), - 'data' => $this->Workflow->genGraphDataForTrigger($trigger_id), - 'trigger_id' => $trigger_id, - ]); - if (!empty($result['errors'])) { + $modules = $this->Workflow->getModulesByType(); + if (!empty($trigger_id)) { + $trigger_ids = Hash::extract($modules['modules_trigger'], '{n}.id'); + if (!in_array($trigger_id, $trigger_ids)) { return $this->__getFailResponseBasedOnContext( - [__('Could not create workflow for trigger %s', $trigger_id), $result['errors']], + [__('Unkown trigger %s', $trigger_id)], null, 'add', $trigger_id, - ['controller' => 'workflows', 'action' => 'editor'] + ['controller' => 'workflows', 'action' => 'triggers'] ); } $workflow = $this->Workflow->fetchWorkflowByTrigger($trigger_id, false); + if (empty($workflow)) { // Workflow do not exists yet. Create it. + $result = $this->Workflow->addWorkflow([ + 'name' => sprintf('Workflow for trigger %s', $trigger_id), + 'data' => $this->Workflow->genGraphDataForTrigger($trigger_id), + 'trigger_id' => $trigger_id, + ]); + if (!empty($result['errors'])) { + return $this->__getFailResponseBasedOnContext( + [__('Could not create workflow for trigger %s', $trigger_id), $result['errors']], + null, + 'add', + $trigger_id, + ['controller' => 'workflows', 'action' => 'editor'] + ); + } + $workflow = $this->Workflow->fetchWorkflowByTrigger($trigger_id, false); + } + } else { + $workflow = $this->Workflow->fetchWorkflow($workflow_id); } $modules = $this->Workflow->attachNotificationToModules($modules, $workflow); $this->loadModel('WorkflowBlueprint'); @@ -148,6 +159,23 @@ class WorkflowsController extends AppController $this->set('workflowBlueprints', $workflowBlueprints); } + public function executeWorkflow($workflow_id) + { + $this->request->allowMethod(['post', 'put']); + $blockingErrors = []; + $data = $this->request->data; + $result = $this->Workflow->executeWorkflow($workflow_id, $data, $blockingErrors); + if (!empty($logging) && empty($result['success'])) { + $logging['message'] = !empty($logging['message']) ? $logging['message'] : __('Error while executing workflow.'); + $errorMessage = implode(', ', $blockingErrors); + $this->Workflow->loadLog()->createLogEntry('SYSTEM', $logging['action'], $logging['model'], $logging['id'], $logging['message'], __('Returned message: %s', $errorMessage)); + } + return $this->RestResponse->viewData([ + 'success' => $result['success'], + 'outcome' => $result['outcomeText'], + ], $this->response->type()); + } + public function triggers() { $triggers = $this->Workflow->getModulesByType('trigger'); diff --git a/app/Lib/Tools/WorkflowFormatConverterTool.php b/app/Lib/Tools/WorkflowFormatConverterTool.php index 56faa4c29..6e8782d77 100644 --- a/app/Lib/Tools/WorkflowFormatConverterTool.php +++ b/app/Lib/Tools/WorkflowFormatConverterTool.php @@ -74,7 +74,7 @@ class WorkflowFormatConverterTool $convertedAttribute = JSONConverterTool::convertAttribute($attribute, true); $convertedAttribute['Attribute']['_allTags'] = $allTags; if ($convertedAttribute['Attribute']['object_id'] != 0) { - $objectModel = ClassRegistry::init('Object'); + $objectModel = ClassRegistry::init('MispObject'); $object = $objectModel->fetchObjectSimple(self::$fakeSiteAdminUser, [ 'conditions' => [ 'Object.id' => $convertedAttribute['Attribute']['object_id'], diff --git a/app/Model/Workflow.php b/app/Model/Workflow.php index 33e20f76c..d142d38ea 100644 --- a/app/Model/Workflow.php +++ b/app/Model/Workflow.php @@ -4,6 +4,7 @@ App::uses('WorkflowGraphTool', 'Tools'); class WorkflowDuplicatedModuleIDException extends Exception {} class TriggerNotFoundException extends Exception {} +class ModuleNotFoundException extends Exception {} class WorkflowNotFoundException extends Exception {} class Workflow extends AppModel @@ -375,6 +376,36 @@ class Workflow extends AppModel return !$hasMultipleOutputConnection; } + /** + * executeWorkflow + * + * @param int $workflow_id + * @param array $data + * @param array $blockingErrors + * @return array + */ + public function executeWorkflow($workflow_id, array $data, array &$blockingErrors=[]): array + { + $this->loadAllWorkflowModules(); + + $workflow = $this->fetchWorkflow($workflow_id, true); + $graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data']; + $startNode = $this->workflowGraphTool->extractTriggerFromWorkflow($graphData, true); + $startNodeID = $startNode['id']; + $trigger_id = $startNode['data']['id']; + if ($startNode == -1) { + $blockingErrors[] = __('Invalid start node `%s`', $startNodeID); + return false; + } + + $triggerModule = $this->getModuleClassByType('trigger', $trigger_id, true); + if (!empty($triggerModule->disabled)) { + return true; + } + $result = $this->__runWorkflow($workflow, $triggerModule, $data, $startNodeID, $blockingErrors); + return $result; + } + /** * executeWorkflowForTrigger * @@ -436,27 +467,38 @@ class Workflow extends AppModel { $this->loadAllWorkflowModules(); - if (empty($this->loaded_modules['trigger'][$trigger_id])) { - throw new TriggerNotFoundException(__('Unknown trigger `%s`', $trigger_id)); - } - $trigger = $this->loaded_modules['trigger'][$trigger_id]; - if (!empty($trigger['disabled'])) { + $triggerModule = $this->getModuleClassByType('trigger', $trigger_id, true); + if (!empty($triggerModule->disabled)) { return true; } - $triggerModule = $this->loaded_classes['trigger'][$trigger_id]; $workflow = $this->fetchWorkflowByTrigger($trigger_id, true); if (empty($workflow)) { throw new WorkflowNotFoundException(__('Could not get workflow for trigger `%s`', $trigger_id)); } $graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data']; - $startNode = $this->workflowGraphTool->getNodeIdForTrigger($graphData, $trigger_id); - if ($startNode == -1) { - $blockingErrors[] = __('Invalid start node `%s`', $startNode); + $startNodeID = $this->workflowGraphTool->getNodeIdForTrigger($graphData, $trigger_id); + if ($startNodeID == -1) { + $blockingErrors[] = __('Invalid start node `%s`', $startNodeID); return false; } + $result = $this->__runWorkflow($workflow, $triggerModule, $data, $startNodeID, $blockingErrors); + return $result['success']; + } + + /** + * runWorkflow + * + * @param array $workflow + * @param $triggerModule + * @param array $data + * @param int $startNodeID + * @return array + */ + private function __runWorkflow(array $workflow, $triggerModule, array $data, $startNodeID, &$blockingErrors=[]): array + { $this->Log = ClassRegistry::init('Log'); - $message = __('Started executing workflow for trigger `%s` (%s)', $trigger_id, $workflow['Workflow']['id']); + $message = __('Started executing workflow for trigger `%s` (%s)', $triggerModule->id, $workflow['Workflow']['id']); $this->Log->createLogEntry('SYSTEM', 'execute_workflow', 'Workflow', $workflow['Workflow']['id'], $message); $this->__logToFile($workflow, $message); $workflow = $this->__incrementWorkflowExecutionCount($workflow); @@ -465,7 +507,7 @@ class Workflow extends AppModel $for_path = !empty($triggerModule->blocking) ? GraphWalker::PATH_TYPE_BLOCKING : GraphWalker::PATH_TYPE_NON_BLOCKING; $this->sendRequestToDebugEndpoint($workflow, [], '/init?type=' . $for_path, $data); - $blockingPathExecutionSuccess = $this->walkGraph($workflow, $startNode, $for_path, $data, $blockingErrors, $walkResult); + $blockingPathExecutionSuccess = $this->walkGraph($workflow, $startNodeID, $for_path, $data, $blockingErrors, $walkResult); $executionStoppedByStopModule = in_array('stop-execution', Hash::extract($walkResult, 'blocking_nodes.{n}.data.id')); if (empty($blockingPathExecutionSuccess)) { $message = __('Execution stopped. %s', PHP_EOL . implode(', ', $blockingErrors)); @@ -477,12 +519,16 @@ class Workflow extends AppModel } else if ($executionStoppedByStopModule) { $outcomeText = 'blocked'; } - $message = __('Finished executing workflow for trigger `%s` (%s). Outcome: %s', $trigger_id, $workflow['Workflow']['id'], $outcomeText); + $message = __('Finished executing workflow for trigger `%s` (%s). Outcome: %s', $triggerModule->id, $workflow['Workflow']['id'], $outcomeText); $this->Log->createLogEntry('SYSTEM', 'execute_workflow', 'Workflow', $workflow['Workflow']['id'], $message); $this->__logToFile($workflow, $message); $this->sendRequestToDebugEndpoint($workflow, [], '/end?outcome=' . $outcomeText, $walkResult); - return $blockingPathExecutionSuccess; + return [ + 'outcomeText' => $outcomeText, + 'walkResult' => $walkResult, + 'success' => $blockingPathExecutionSuccess, + ]; } /** @@ -635,20 +681,55 @@ class Workflow extends AppModel return $moduleClass; } - public function getModuleConfigByType($module_type, $id) + /** + * getModuleClassByType + * + * @param string $module_type + * @param string $id + * @param boolean $throwException + * @return + * @throws ModuleNotFoundException + */ + public function getModuleClassByType($module_type, $id, $throwException=false) { $this->loadAllWorkflowModules(); - $moduleClass = $this->loaded_modules[$module_type][$id] ?? null; + $moduleClass = $this->loaded_classes[$module_type][$id] ?? null; + if (is_null($moduleClass) && !empty($throwException)) { + if ($module_type == 'trigger') { + throw new TriggerNotFoundException(__('Unknown module `%s` for module type `%s`', $id, $module_type)); + } else { + throw new ModuleNotFoundException(__('Unknown module `%s` for module type `%s`', $id, $module_type)); + } + } return $moduleClass; } + /** + * getModuleConfigByType + * + * @param string $module_type + * @param string $id + * @param boolean $throwException + * @return array + * @throws ModuleNotFoundException + */ + public function getModuleConfigByType($module_type, $id, $throwException=false): array + { + $this->loadAllWorkflowModules(); + $moduleConfig = $this->loaded_modules[$module_type][$id] ?? null; + if (is_null($moduleConfig) && !empty($throwException)) { + throw new ModuleNotFoundException(__('Unknown module `%s` for module type `%s`', $id, $module_type)); + } + return $moduleConfig; + } + public function attachNotificationToModules(array $modules, array $workflow): array { $trigger_is_misp_core_format = false; $trigger_is_blocking = false; $trigger_id = $this->workflowGraphTool->extractTriggerFromWorkflow($workflow['Workflow']['data'], false); if (!empty($trigger_id)) { - $triggerClass = $this->loaded_classes['trigger'][$trigger_id]; + $triggerClass = $this->getModuleClassByType('trigger', $trigger_id, true); $trigger_is_misp_core_format = !empty($triggerClass->misp_core_format); $trigger_is_blocking = !empty($triggerClass->blocking); }