new: [workflows:execute_module] Added stateless node execution

pull/8574/head
Sami Mokaddem 2022-09-05 10:40:44 +02:00
parent db8edffb2b
commit 121c1270fb
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
5 changed files with 294 additions and 32 deletions

View File

@ -784,6 +784,7 @@ class ACLComponent extends Component
'executeWorkflow'=> [],
'debugToggleField'=> [],
'massToggleField'=> [],
'moduleStatelessExecution'=> [],
],
'workflowBlueprints' => [
'add' => [],

View File

@ -13,6 +13,7 @@ class WorkflowsController extends AppController
{
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');
@ -425,4 +426,13 @@ class WorkflowsController extends AppController
];
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'];
$result = $this->Workflow->moduleStatelessExecution($module_id, $input_data, $param_data);
return $this->RestResponse->viewData($result, 'json');
}
}

View File

@ -552,38 +552,7 @@ class Workflow extends AppModel
'executed_nodes' => [],
'blocked_paths' => [],
];
$this->Organisation = ClassRegistry::init('Organisation');
$hostOrg = $this->Organisation->find('first', [
'recursive' => -1,
'conditions' => [
'id' => Configure::read('MISP.host_org_id')
],
]);
if (!empty($hostOrg)) {
$userForWorkflow = [
'email' => 'SYSTEM',
'id' => 0,
'org_id' => $hostOrg['Organisation']['id'],
'Role' => ['perm_site_admin' => 1],
'Organisation' => $hostOrg['Organisation']
];
} else {
$this->User = ClassRegistry::init('User');
$userForWorkflow = $this->User->find('first', [
'recursive' => -1,
'conditions' => [
'Role.perm_site_admin' => 1,
'User.disabled' => 0
],
'contain' => [
'Organisation' => ['fields' => ['name']],
'Role' => ['fields' => ['*']],
],
'fields' => ['User.org_id', 'User.id', 'User.email'],
]);
$userForWorkflow['Server'] = [];
$userForWorkflow = $this->User->rearrangeToAuthForm($userForWorkflow);
}
$userForWorkflow = $this->getUserForWorkflow();
if (empty($userForWorkflow)) {
$errors[] = __('Could not find a valid user to run the workflow. Please set setting `MISP.host_org_id` or make sure a valid site_admin user exists.');
return false;
@ -626,6 +595,43 @@ class Workflow extends AppModel
return true;
}
public function getUserForWorkflow(): array
{
$this->Organisation = ClassRegistry::init('Organisation');
$hostOrg = $this->Organisation->find('first', [
'recursive' => -1,
'conditions' => [
'id' => Configure::read('MISP.host_org_id')
],
]);
if (!empty($hostOrg)) {
$userForWorkflow = [
'email' => 'SYSTEM',
'id' => 0,
'org_id' => $hostOrg['Organisation']['id'],
'Role' => ['perm_site_admin' => 1],
'Organisation' => $hostOrg['Organisation']
];
} else {
$this->User = ClassRegistry::init('User');
$userForWorkflow = $this->User->find('first', [
'recursive' => -1,
'conditions' => [
'Role.perm_site_admin' => 1,
'User.disabled' => 0
],
'contain' => [
'Organisation' => ['fields' => ['name']],
'Role' => ['fields' => ['*']],
],
'fields' => ['User.org_id', 'User.id', 'User.email'],
]);
$userForWorkflow['Server'] = [];
$userForWorkflow = $this->User->rearrangeToAuthForm($userForWorkflow);
return $userForWorkflow;
}
}
public function executeNode(array $node, WorkflowRoamingData $roamingData, array &$errors=[]): bool
{
$roamingData->setCurrentNode($node['id']);
@ -1262,6 +1268,56 @@ class Workflow extends AppModel
return $data;
}
/**
* moduleSattelesExecution Executes a module using the provided configuration and returns back the result
*
* @param string $module_id
* @param string|array $input_data
* @param array $param_data
* @return array
*/
public function moduleStatelessExecution(string $module_id, $input_data=[], array $param_data=[]): array
{
$result = [];
$input_data = !empty($input_data) ? $input_data : [];
$eventPublishTrigger = $this->getModuleClassByType('trigger', 'event-publish');
$data = $this->__normalizeDataForTrigger($eventPublishTrigger, $input_data);
$module_config = $this->getModuleByID($module_id);
$node = $this->genNodeFromConfig($module_config, $param_data);
$module_class = $this->getModuleClass($node);
$user_for_workflow = $this->getUserForWorkflow();
if (empty($user_for_workflow)) {
$result['error'][] = __('Could not find a valid user to run the workflow. Please set setting `MISP.host_org_id` or make sure a valid site_admin user exists.');
return $result;
}
$roaming_data = $this->workflowGraphTool->getRoamingData($user_for_workflow, $data);
$errors = [];
$success = $module_class->exec($node, $roaming_data, $errors);
$result['success'] = $success;
$result['errors'] = $errors;
return $result;
}
public function genNodeFromConfig(array $module_config, $indexed_params): array
{
$node = [
'id' => 1,
'name' => $module_config['name'],
'data' => [
'id' => $module_config['id'],
'name' => $module_config['name'],
'module_type' => $module_config['module_type'],
'module_version' => $module_config['version'],
'indexed_params' => $indexed_params,
'saved_filters' => $module_config['saved_filters'],
'module_data' => $module_config,
],
'inputs' => [],
'outputs' => [],
];
return $node;
}
/**
* hasPathWarnings
*

View File

@ -0,0 +1,193 @@
<?php
$type_mapper = [
'picker' => 'dropdown',
];
?>
<h3><?= __('Stateless module execution') ?></h3>
<div class="row">
<div class="span6">
<h5><?= __('Module parameters') ?></h5>
<div>
<?php
$formFields = array_map(function ($param) use ($type_mapper) {
$param['field'] = $param['id'];
$param['class'] = 'span6';
$param['type'] = $type_mapper[$param['type']] ?? $param['type'];
if (!empty($param['options']) && array_keys($param['options']) === range(0, count($param['options']) - 1)) {
// Sequential arrays should be keyed with their value
if (!empty($param['options'])) {
if (isset($param['options'][0]['name']) && isset($param['options'][0]['value'])) {
$tmp = [];
foreach ($param['options'] as $option) {
$tmp[$option['value']] = $option['name'];
};
$param['options'] = $tmp;
} else {
$param['options'] = array_combine($param['options'], $param['options']);
}
}
}
return $param;
}, $module['params']);
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'skip_side_menu' => true,
'title' => ' ',
'fields' => $formFields,
'submit' => [
'no_submit' => true,
'action' => $this->request->params['action'],
'ajaxSubmit' => 'submitGenericFormInPlace();'
]
]
]);
?>
</div>
</div>
<div class="span6">
<h5><?= __('Input data') ?></h5>
<div>
<?php
$formFields = [
[
'field' => 'module_input_data',
'type' => 'textarea',
'class' => 'span6',
]
];
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'skip_side_menu' => true,
'title' => ' ',
'fields' => $formFields,
'submit' => [
'no_submit' => true,
'action' => $this->request->params['action'],
'ajaxSubmit' => 'submitGenericFormInPlace();'
]
]
]);
?>
</div>
</div>
</div>
<div>
<button id="run-module" class="btn btn-primary">
<span class="fa fa-spin fa-spinner loading-span hidden"></span>
<?= __('Execute module') ?>
</button>
</div>
<div class="row" style="margin-top: 10px;">
<div class="span9">
<div style="margin: 0.5em 0;">
<strong><?= __('Execution result:') ?></strong>
<span id="executionResultStatus" class="label"><?= __('none') ?></span>
</div>
<pre id="executionResultText"><?= __('- not executed -') ?></pre>
<div id="executionResultHtml"></div>
</div>
</div>
<script>
var module = <?= JsonTool::encode($module) ?>;
var $runModuleBtn = $('#run-module')
var $executionResultStatus = $('#executionResultStatus')
var $executionResultText = $('#executionResultText')
var $formParams = $('#WorkflowModuleViewForm')
var $inputData = $('#WorkflowModuleInputData')
$(document).ready(function() {
$runModuleBtn.click(submitModuleExecution)
$('select[multiple]').chosen()
})
function submitModuleExecution() {
var data = collectData()
performRequest(data)
}
function toggleLoading($button, loading) {
if (loading) {
$button
.prop('disabled', true)
.find('.loading-span').show()
} else {
$button
.prop('disabled', false)
.find('.loading-span').hide()
}
}
function collectData() {
var formData = new FormData($formParams[0])
var indexedParams = {}
Array.from(formData.keys()).forEach(function(fullFieldName) {
var myRegexp = new RegExp(/data\[Workflow\]\[(\w+)\](\[\])*/, 'g');
var match = myRegexp.exec(fullFieldName);
if (match != null) {
fieldName = match[1]
isMultiple = match[2] !== undefined
if (isMultiple) {
indexedParams[fieldName] = formData.getAll(fullFieldName)
} else {
indexedParams[fieldName] = formData.get(fullFieldName)
}
}
})
return {
module_indexed_param: indexedParams,
input_data: $inputData.val(),
}
}
function showExecutionResult(jqXHR, result) {
$executionResultStatus
.text(jqXHR.status + ' [' + jqXHR.duration + ' ms]')
.removeClass(['label-success', 'label-important'])
.addClass(jqXHR.status == 200 ? 'label-success' : 'label-important')
if (typeof result === 'object') {
$executionResultText.text(JSON.stringify(result, '', 4));
} else {
$('#executionResultHtml').html(result);
// $executionResultText.text(result);
}
}
function performRequest(data) {
url = '<?= $baseurl ?>/workflows/moduleStatelessExecution/<?= h($module['id']) ?>'
var start = new Date().getTime();
$.ajax({
data: data,
beforeSend: function() {
toggleLoading($runModuleBtn, true)
},
success: function(result, textStatus, jqXHR) {
jqXHR.duration = (new Date().getTime() - start);
if (result) {
showExecutionResult(jqXHR, result)
}
},
error: function(jqXHR, _, _) {
jqXHR.duration = (new Date().getTime() - start);
errorThrown = jqXHR.responseJSON ? jqXHR.responseJSON.errors : (jqXHR.responseText ? jqXHR.responseText : jqXHR.statusText)
showExecutionResult(jqXHR, errorThrown)
},
complete: function() {
toggleLoading($runModuleBtn, false)
},
type: 'post',
url: url
})
}
</script>
<style>
.loading-span {
margin-right: 5px;
margin-left: 0px;
line-height: 20px;
}
</style>

View File

@ -5,6 +5,7 @@ if ($data['module_type'] == 'trigger') {
['element' => 'Workflows/executionPath', 'element_params' => ['workflow' => $data['Workflow']]],
];
}
$append[] = ['element' => 'Workflows/execute_module', 'element_params' => ['module' => $data]];
echo $this->element(
'genericElements/SingleViews/single_view',
[
@ -54,6 +55,7 @@ echo $this->element(
],
[
'key' => __('Module Parameters'),
'class' => 'restrict-height',
'type' => 'json',
'path' => 'params',
],