mirror of https://github.com/MISP/MISP
new: [workflows:execute_module] Added stateless node execution
parent
db8edffb2b
commit
121c1270fb
|
@ -784,6 +784,7 @@ class ACLComponent extends Component
|
|||
'executeWorkflow'=> [],
|
||||
'debugToggleField'=> [],
|
||||
'massToggleField'=> [],
|
||||
'moduleStatelessExecution'=> [],
|
||||
],
|
||||
'workflowBlueprints' => [
|
||||
'add' => [],
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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>
|
|
@ -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',
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue