MISP/app/Controller/WorkflowsController.php

472 lines
20 KiB
PHP

<?php
App::uses('AppController', 'Controller');
class WorkflowsController extends AppController
{
public $components = array(
'RequestHandler'
);
private $toggleableFields = ['enabled'];
public function beforeFilter()
{
parent::beforeFilter();
$this->Security->unlockedActions[] = 'checkGraph';
$this->Security->unlockedActions[] = 'moduleStatelessExecution';
$requirementErrors = [];
if (empty(Configure::read('MISP.background_jobs'))) {
$requirementErrors[] = __('Background workers must be enabled to use workflows');
$this->render('error');
}
if (empty(Configure::read('Plugin.Workflow_enable'))) {
$requirementErrors[] = __('The workflow plugin must be enabled to use workflows. Go to `/servers/serverSettings/Plugin` the enable the `Plugin.Workflow` setting');
$this->render('error');
}
try {
$this->Workflow->setupRedisWithException();
} catch (Exception $e) {
$requirementErrors[] = $e->getMessage();
}
if (!empty($requirementErrors)) {
$this->set('requirementErrors', $requirementErrors);
$this->render('error');
}
}
public function index()
{
$params = [
'filters' => ['name', 'uuid'],
'quickFilters' => ['name', 'uuid'],
];
$this->CRUD->index($params);
if ($this->IndexFilter->isRest()) {
return $this->restResponsePayload;
}
$this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'index'));
}
public function rebuildRedis()
{
$this->Workflow->rebuildRedis();
}
public function edit($id)
{
$this->set('id', $id);
$savedWorkflow = $this->Workflow->fetchWorkflow($id);
if ($this->request->is('post') || $this->request->is('put')) {
$newWorkflow = $this->request->data;
$newWorkflow['Workflow']['data'] = JsonTool::decode($newWorkflow['Workflow']['data']);
$newWorkflow = $this->__applyDataFromSavedWorkflow($newWorkflow, $savedWorkflow);
$result = $this->Workflow->editWorkflow($newWorkflow);
$redirectTarget = ['action' => 'view', $id];
if (!empty($result['errors'])) {
return $this->__getFailResponseBasedOnContext($result['errors'], null, 'edit', $this->Workflow->id, $redirectTarget);
} else {
$successMessage = __('Workflow saved.');
$savedWorkflow = $result['saved'];
$savedWorkflow = $this->Workflow->attachLabelToConnections($savedWorkflow);
return $this->__getSuccessResponseBasedOnContext($successMessage, $savedWorkflow, 'edit', false, $redirectTarget);
}
} else {
$savedWorkflow['Workflow']['data'] = JsonTool::encode($savedWorkflow['Workflow']['data']);
$this->request->data = $savedWorkflow;
}
$this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'edit'));
$this->render('add');
}
public function delete($id)
{
$params = [
];
$this->CRUD->delete($id, $params);
if ($this->IndexFilter->isRest()) {
return $this->restResponsePayload;
}
}
public function view($id)
{
$filters = $this->IndexFilter->harvestParameters(['format']);
if (!empty($filters['format'])) {
if ($filters['format'] == 'dot') {
$dot = $this->Workflow->getDotNotation($id);
return $this->RestResponse->viewData($dot, $this->response->type());
} else if ($filters['format'] == 'mermaid') {
$mermaid = $this->Workflow->getMermaid($id);
return $this->RestResponse->viewData($mermaid, $this->response->type());
}
}
$this->CRUD->view($id, [
'afterFind' => function($workflow) {
return $this->Workflow->attachLabelToConnections($workflow);
}
]);
if ($this->IndexFilter->isRest()) {
return $this->restResponsePayload;
}
$this->set('id', $id);
$this->set('menuData', array('menuList' => 'workflows', 'menuItem' => 'view'));
}
public function editor($id)
{
$trigger_id = false;
$workflow = false;
if (is_numeric($id)) {
$workflow_id = $id;
} else {
$trigger_id = $id;
}
$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(
[__('Unkown trigger %s', $trigger_id)],
null,
'add',
$trigger_id,
['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);
}
$workflow = $this->Workflow->attachLabelToConnections($workflow, $trigger_id);
$modules = $this->Workflow->attachNotificationToModules($modules, $workflow);
$this->loadModel('WorkflowBlueprint');
$workflowBlueprints = $this->WorkflowBlueprint->find('all');
$workflowBlueprints = array_map(function($blueprint) {
return $this->WorkflowBlueprint->attachModuleDataToBlueprint($blueprint);
}, $workflowBlueprints);
$this->set('selectedWorkflow', $workflow);
$this->set('workflowTriggerId', $trigger_id);
$this->set('modules', $modules);
$this->set('workflowBlueprints', $workflowBlueprints);
}
public function executeWorkflow($workflow_id)
{
if ($this->request->is('post') || $this->request->is('put')) {
$blockingErrors = [];
if (!JsonTool::isValid($this->request->data['Workflow']['data'])) {
return $this->RestResponse->viewData([
'success' => false,
'outcome' => __('Invalid JSON'),
], $this->response->type());
}
$data = JsonTool::decode($this->request->data['Workflow']['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());
}
$this->render('ajax/executeWorkflow');
}
public function triggers()
{
$triggers = $this->Workflow->getModulesByType('trigger');
$triggers = $this->Workflow->attachWorkflowToTriggers($triggers);
$scopes = array_unique(Hash::extract($triggers, '{n}.scope'));
sort($scopes);
$filters = $this->IndexFilter->harvestParameters(['scope', 'enabled', 'blocking']);
if (!empty($filters['scope'])) {
$triggers = array_filter($triggers, function($trigger) use ($filters) {
return $trigger['scope'] === $filters['scope'];
});
}
if (isset($filters['enabled'])) {
$triggers = array_filter($triggers, function($trigger) use ($filters) {
return $trigger['disabled'] != $filters['enabled'];
});
}
if (isset($filters['blocking'])) {
$triggers = array_filter($triggers, function($trigger) use ($filters) {
return $trigger['blocking'] == $filters['blocking'];
});
}
App::uses('CustomPaginationTool', 'Tools');
$customPagination = new CustomPaginationTool();
$customPagination->truncateAndPaginate($triggers, $this->params, 'Workflow', true);
if ($this->_isRest()) {
return $this->RestResponse->viewData($triggers, $this->response->type());
}
$this->set('data', $triggers);
$this->set('scopes', $scopes);
$this->set('filters', $filters);
$this->set('menuData', ['menuList' => 'workflows', 'menuItem' => 'index_trigger']);
}
public function moduleIndex()
{
$modules = $this->Workflow->getModulesByType();
$errorWhileLoading = $this->Workflow->getModuleLoadingError();
$this->Module = ClassRegistry::init('Module');
$mispModules = $this->Module->getModules('Action');
$this->set('module_service_error', !is_array($mispModules));
$filters = $this->IndexFilter->harvestParameters(['type', 'actiontype', 'enabled']);
$moduleType = $filters['type'] ?? 'action';
$actionType = $filters['actiontype'] ?? 'all';
$enabledState = $filters['enabled'] ?? false;
if ($moduleType == 'all' || $moduleType == 'custom') {
$data = array_merge(
$modules["modules_action"],
$modules["modules_logic"]
);
} else {
$data = $modules["modules_{$moduleType}"];
}
if ($actionType == 'mispmodule') {
$data = array_filter($data, function($module) {
return !empty($module['is_misp_module']);
});
} else if ($actionType == 'blocking') {
$data = array_filter($data, function ($module) {
return !empty($module['blocking']);
});
} else if ($moduleType == 'custom') {
$data = array_filter($data, function ($module) {
return !empty($module['is_custom']);
});
}
if ($enabledState !== false) {
$moduleType = !empty($enabledState) ? 'enabled' : 'disabled';
$data = array_filter($data, function ($module) use ($enabledState) {
return !empty($enabledState) ? empty($module['disabled']) : !empty($module['disabled']);
});
}
if ($this->_isRest()) {
return $this->RestResponse->viewData($data, $this->response->type());
}
App::uses('CustomPaginationTool', 'Tools');
$customPagination = new CustomPaginationTool();
$params = $customPagination->createPaginationRules($data, $this->passedArgs, 'Workflow');
$params = $customPagination->applyRulesOnArray($data, $params, 'Workflow');
$params['options'] = array_merge($params['options'], $filters);
$this->params['paging'] = [$this->modelClass => $params];
$this->set('data', $data);
$this->set('indexType', $moduleType);
$this->set('actionType', $actionType);
$this->set('errorWhileLoading', $errorWhileLoading);
$this->set('menuData', ['menuList' => 'workflows', 'menuItem' => 'index_module']);
}
public function moduleView($module_id)
{
$module = $this->Workflow->getModuleByID($module_id);
if (empty($module)) {
throw new NotFoundException(__('Invalid trigger ID'));
}
$is_trigger = $module['module_type'] == 'trigger';
if ($is_trigger) {
$module = $this->Workflow->attachWorkflowToTriggers([$module])[0];
$module['listening_workflows'] = $this->Workflow->getListeningWorkflowForTrigger($module);
}
if ($this->_isRest()) {
return $this->RestResponse->viewData($module, $this->response->type());
}
if (!isset($module['Workflow']))
$module['Workflow'] = ['counter' => false, 'id' => false];
$this->set('data', $module);
$this->set('menuData', ['menuList' => 'workflows', 'menuItem' => 'view_module']);
}
public function toggleModule($module_id, $enabled, $is_trigger=false)
{
$this->request->allowMethod(['post', 'put']);
$saved = $this->Workflow->toggleModule($module_id, $enabled, $is_trigger);
if ($saved) {
return $this->__getSuccessResponseBasedOnContext(
__('%s module %s', ($enabled ? 'Enabled' : 'Disabled'), $module_id),
null,
'toggle_module',
$module_id,
['action' => (!empty($is_trigger) ? 'triggers' : 'moduleIndex')]
);
} else {
return $this->__getFailResponseBasedOnContext(
__('Could not %s module %s', ($enabled ? 'Enabled' : 'Disabled'), $module_id),
null,
'toggle_module',
$module_id,
['action' => (!empty($is_trigger) ? 'triggers' : 'moduleIndex')]
);
}
}
public function debugToggleField($workflow_id, $enabled)
{
if (!$this->request->is('ajax')) {
throw new MethodNotAllowedException(__('This action is available via AJAX only.'));
}
$this->layout = false;
$this->render('ajax/getDebugToggleField');
if ($this->request->is('post') || $this->request->is('put')) {
$success = $this->Workflow->toggleDebug($workflow_id, $enabled);
if (!empty($success)) {
return $this->__getSuccessResponseBasedOnContext(
__('%s debug mode', ($enabled ? __('Enabled') : __('Disabled'))),
null,
'toggle_debug',
$workflow_id,
['action' => 'triggers']
);
} else {
return $this->__getFailResponseBasedOnContext(
__('Could not %s debug mode', ($enabled ? __('enable') : __('disable'))),
null,
'toggle_debug',
$workflow_id,
['action' => 'triggers']
);
}
}
}
public function massToggleField($fieldName, $enabled, $is_trigger=false)
{
if (!in_array($fieldName, $this->toggleableFields)) {
throw new MethodNotAllowedException(__('The field `%s` cannot be toggled', $fieldName));
}
if ($this->request->is('post') || $this->request->is('put')) {
$module_ids = JsonTool::decode($this->request->data['Workflow']['module_ids']);
$enabled_count = $this->Workflow->toggleModules($module_ids, $enabled, $is_trigger);
if (!empty($enabled_count)) {
return $this->__getSuccessResponseBasedOnContext(
__('%s %s modules', ($enabled ? 'Enabled' : 'Disabled'), $enabled_count),
null,
'toggle_module',
$module_ids,
['action' => (!empty($is_trigger) ? 'triggers' : 'moduleIndex')]
);
} else {
return $this->__getFailResponseBasedOnContext(
__('Could not %s modules', ($enabled ? 'enable' : 'disable')),
null,
'toggle_module',
$module_ids,
['action' => (!empty($is_trigger) ? 'triggers' : 'moduleIndex')]
);
}
}
}
private function __getSuccessResponseBasedOnContext($message, $data = null, $action = '', $id = false, $redirect = array())
{
if ($this->_isRest()) {
if (!is_null($data)) {
return $this->RestResponse->viewData($data, $this->response->type());
} else {
return $this->RestResponse->saveSuccessResponse('Workflow', $action, $id, false, $message);
}
} elseif ($this->request->is('ajax')) {
return $this->RestResponse->saveSuccessResponse('Workflow', $action, $id, false, $message, $data);
} else {
$this->Flash->success($message);
$this->redirect($redirect);
}
return;
}
private function __getFailResponseBasedOnContext($message, $data = null, $action = '', $id = false, $redirect = array())
{
if (is_array($message)) {
$message = implode(', ', $message);
}
if ($this->_isRest()) {
if ($data !== null) {
return $this->RestResponse->viewData($data, $this->response->type());
} else {
return $this->RestResponse->saveFailResponse('Workflow', $action, $id, $message);
}
} elseif ($this->request->is('ajax')) {
return $this->RestResponse->saveFailResponse('Workflow', $action, $id, $message, false, $data);
} else {
$this->Flash->error($message);
$this->redirect($redirect);
}
}
private function __applyDataFromSavedWorkflow($newWorkflow, $savedWorkflow)
{
if (!isset($newWorkflow['Workflow'])) {
$newWorkflow = ['Workflow' => $newWorkflow];
}
$ignoreFieldList = ['id', 'uuid'];
foreach (Workflow::CAPTURE_FIELDS_EDIT as $field) {
if (!in_array($field, $ignoreFieldList) && isset($newWorkflow['Workflow'][$field])) {
$savedWorkflow['Workflow'][$field] = $newWorkflow['Workflow'][$field];
}
}
return $savedWorkflow;
}
public function checkGraph()
{
$this->request->allowMethod(['post']);
$graphData = JsonTool::decode($this->request->data['graph']);
$cycles = [];
$isAcyclic = $this->Workflow->workflowGraphTool->isAcyclic($graphData, $cycles);
$edgesMultipleOutput = [];
$hasMultipleOutputConnection = $this->Workflow->workflowGraphTool->hasMultipleOutputConnection($graphData, $edgesMultipleOutput);
$edgesWarnings = [];
$hasPathWarnings = $this->Workflow->hasPathWarnings($graphData, $edgesWarnings);
$data = [
'is_acyclic' => [
'is_acyclic' => $isAcyclic,
'cycles' => $cycles,
],
'multiple_output_connection' => [
'has_multiple_output_connection' => $hasMultipleOutputConnection,
'edges' => $edgesMultipleOutput,
],
'path_warnings' => [
'has_path_warnings' => $hasPathWarnings,
'edges' => $edgesWarnings,
],
];
return $this->RestResponse->viewData($data, 'json');
}
public function moduleStatelessExecution($module_id)
{
$this->request->allowMethod(['post']);
$input_data = JsonTool::decode($this->request->data['input_data']);
$param_data = $this->request->data['module_indexed_param'];
$convert_data = $this->request->data['convert_data'];
$result = $this->Workflow->moduleStatelessExecution($module_id, $input_data, $param_data, $convert_data);
return $this->RestResponse->viewData($result, 'json');
}
}