mirror of https://github.com/MISP/MISP
new: [workflow] Initial work on filtering modules - WiP
parent
6b1b080eec
commit
fd93ac9c5e
|
@ -101,6 +101,9 @@ class WorkflowsController extends AppController
|
|||
}
|
||||
}
|
||||
$this->CRUD->view($id, [
|
||||
'afterFind' => function($workflow) {
|
||||
return $this->Workflow->attachLabelToConnections($workflow);
|
||||
}
|
||||
]);
|
||||
if ($this->IndexFilter->isRest()) {
|
||||
return $this->restResponsePayload;
|
||||
|
@ -151,6 +154,7 @@ class WorkflowsController extends AppController
|
|||
} 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');
|
||||
|
|
|
@ -162,6 +162,12 @@ class GraphWalker
|
|||
} else if ($node['data']['id'] == 'concurrent-task') {
|
||||
$this->_evaluateConcurrentTask($node, $roamingData, $outputs['output_1']);
|
||||
return ['output_1' => []];
|
||||
} else if ($node['data']['id'] == 'generic-filter-data') {
|
||||
$this->_evaluateFilterAddLogic($node, $roamingData, $outputs['output_1']);
|
||||
return ['output_1' => []];
|
||||
} else if ($node['data']['id'] == 'generic-filter-reset') {
|
||||
$this->_evaluateFilterRemoveLogic($node, $roamingData, $outputs['output_1']);
|
||||
return ['output_1' => []];
|
||||
} else {
|
||||
$useFirstOutput = $this->_evaluateCustomLogicCondition($node, $roamingData);
|
||||
return $useFirstOutput ? ['output_1' => $outputs['output_1']] : ['output_2' => $outputs['output_2']];
|
||||
|
@ -175,6 +181,18 @@ class GraphWalker
|
|||
return $result;
|
||||
}
|
||||
|
||||
private function _evaluateFilterAddLogic($node, WorkflowRoamingData $roamingData): bool
|
||||
{
|
||||
$result = $this->WorkflowModel->executeNode($node, $roamingData);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function _evaluateFilterRemoveLogic($node, WorkflowRoamingData $roamingData): bool
|
||||
{
|
||||
$result = $this->WorkflowModel->executeNode($node, $roamingData);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function _evaluateCustomLogicCondition($node, WorkflowRoamingData $roamingData): bool
|
||||
{
|
||||
$result = $this->WorkflowModel->executeNode($node, $roamingData);
|
||||
|
@ -357,6 +375,50 @@ class WorkflowGraphTool
|
|||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* extractFilterNodesFromWorkflow Return the list of generic-filter-data's id (or full module) that are included in the workflow
|
||||
*
|
||||
* @param array $workflow
|
||||
* @param bool $fullNode
|
||||
* @return array
|
||||
*/
|
||||
public static function extractFilterNodesFromWorkflow(array $graphData, bool $fullNode = false): array
|
||||
{
|
||||
$nodes = [];
|
||||
foreach ($graphData as $node) {
|
||||
if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'generic-filter-data') {
|
||||
if (!empty($fullNode)) {
|
||||
$nodes[] = $node;
|
||||
} else {
|
||||
$nodes[] = $node['data']['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* extractResetFilterFromWorkflow Return the list of generic-filter-reset's id (or full module) that are included in the workflow
|
||||
*
|
||||
* @param array $workflow
|
||||
* @param bool $fullNode
|
||||
* @return array
|
||||
*/
|
||||
public static function extractResetFilterFromWorkflow(array $graphData, bool $fullNode = false): array
|
||||
{
|
||||
$nodes = [];
|
||||
foreach ($graphData as $node) {
|
||||
if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'generic-filter-reset') {
|
||||
if (!empty($fullNode)) {
|
||||
$nodes[] = $node;
|
||||
} else {
|
||||
$nodes[] = $node['data']['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* isAcyclic Return if the graph contains a cycle
|
||||
*
|
||||
|
|
|
@ -1290,6 +1290,67 @@ class Workflow extends AppModel
|
|||
return $data;
|
||||
}
|
||||
|
||||
public function getLabelsForConnections($workflow, $trigger_id): array
|
||||
{
|
||||
$graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data'];
|
||||
$startNodeID = $this->workflowGraphTool->getNodeIdForTrigger($graphData, $trigger_id);
|
||||
if ($startNodeID == -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$connections = [];
|
||||
|
||||
$filterNodes = $this->workflowGraphTool->extractFilterNodesFromWorkflow($graphData, true);
|
||||
$filterNodeIDToLabel = Hash::combine($filterNodes, '{n}.id', '{n}.data.indexed_params.filtering-label');
|
||||
$resetFilterNodes = $this->workflowGraphTool->extractResetFilterFromWorkflow($graphData, true);
|
||||
$resetFilterNodeIDToLabel = Hash::combine($resetFilterNodes, '{n}.id', '{n}.data.indexed_params.filtering-label');
|
||||
$roamingData = $this->workflowGraphTool->getRoamingData();
|
||||
$graphWalker = $this->workflowGraphTool->getWalkerIterator($graphData, $this, $startNodeID, GraphWalker::PATH_TYPE_INCLUDE_LOGIC, $roamingData);
|
||||
foreach ($graphWalker as $graphNode) {
|
||||
$node = $graphNode['node'];
|
||||
$nodeID = $node['id'];
|
||||
$parsedPathList = GraphWalker::parsePathList($graphNode['path_list']);
|
||||
foreach ($parsedPathList as $pathEntry) {
|
||||
if (!empty($filterNodeIDToLabel[$pathEntry['source_id']])) {
|
||||
$connections[$nodeID][] = $filterNodeIDToLabel[$pathEntry['source_id']];
|
||||
}
|
||||
if (!empty($resetFilterNodeIDToLabel[$pathEntry['source_id']])) {
|
||||
if ($resetFilterNodeIDToLabel[$pathEntry['source_id']] == 'all') {
|
||||
$connections[$nodeID] = [];
|
||||
} else {
|
||||
$connections[$nodeID] = array_values(array_diff($connections[$nodeID], [$resetFilterNodeIDToLabel[$pathEntry['source_id']]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $connections;
|
||||
}
|
||||
|
||||
public function attachLabelToConnections($workflow, $trigger_id=null): array
|
||||
{
|
||||
$graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data'];
|
||||
if (is_null($trigger_id)) {
|
||||
$startNode = $this->workflowGraphTool->extractTriggerFromWorkflow($graphData, true);
|
||||
$trigger_id = $startNode['data']['id'];
|
||||
}
|
||||
$labelsByNodes = $this->getLabelsForConnections($workflow, $trigger_id);
|
||||
foreach ($graphData as $i => $node) {
|
||||
if (!empty($labelsByNodes[$node['id']])) {
|
||||
foreach ($node['inputs'] as $inputName => $inputs) {
|
||||
foreach ($inputs['connections'] as $j => $connection) {
|
||||
$workflow['Workflow']['data'][$i]['inputs'][$inputName]['connections'][$j]['labels'] = array_map(function($label) {
|
||||
return [
|
||||
'id' => Inflector::variable($label),
|
||||
'name' => $label,
|
||||
'variant' => 'info',
|
||||
];
|
||||
}, $labelsByNodes[$node['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $workflow;
|
||||
}
|
||||
/**
|
||||
* moduleSattelesExecution Executes a module using the provided configuration and returns back the result
|
||||
*
|
||||
|
|
|
@ -298,3 +298,20 @@ class WorkflowBaseLogicModule extends WorkflowBaseModule
|
|||
class WorkflowBaseActionModule extends WorkflowBaseModule
|
||||
{
|
||||
}
|
||||
|
||||
class WorkflowFilteringLogicModule extends WorkflowBaseLogicModule
|
||||
{
|
||||
public $blocking = false;
|
||||
public $inputs = 1;
|
||||
public $outputs = 2;
|
||||
|
||||
protected function _genFilteringLabels(): array
|
||||
{
|
||||
$names = ['A', 'B', 'C', 'D', 'E', 'F'];
|
||||
$labels = [];
|
||||
foreach ($names as $name) {
|
||||
$labels[$name] = __('Label %s', $name);
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
include_once APP . 'Model/WorkflowModules/WorkflowBaseModule.php';
|
||||
|
||||
class Module_generic_filter_data extends WorkflowFilteringLogicModule
|
||||
{
|
||||
public $id = 'generic-filter-data';
|
||||
public $name = 'Filter :: Generic';
|
||||
public $description = 'Generic data filtering block. The module filters incoming data and forward the matching data to its output.';
|
||||
public $icon = 'filter';
|
||||
public $inputs = 1;
|
||||
public $outputs = 1;
|
||||
// public $html_template = 'filter-add';
|
||||
public $params = [];
|
||||
|
||||
private $operators = [
|
||||
'in' => 'In',
|
||||
'not_in' => 'Not in',
|
||||
'equals' => 'Equals',
|
||||
'not_equals' => 'Not equals',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->params = [
|
||||
[
|
||||
'id' => 'filtering-label',
|
||||
'label' => __('Filtering Label'),
|
||||
'type' => 'select',
|
||||
'options' => $this->_genFilteringLabels(),
|
||||
],
|
||||
[
|
||||
'id' => 'selector',
|
||||
'label' => __('Data selector'),
|
||||
'type' => 'input',
|
||||
'placeholder' => 'Event._AttributeFlattened.{n}',
|
||||
],
|
||||
[
|
||||
'id' => 'value',
|
||||
'label' => __('Value'),
|
||||
'type' => 'input',
|
||||
'placeholder' => 'tlp:red',
|
||||
],
|
||||
[
|
||||
'id' => 'operator',
|
||||
'label' => __('Operator'),
|
||||
'type' => 'select',
|
||||
'default' => 'in',
|
||||
'options' => $this->operators,
|
||||
],
|
||||
[
|
||||
'id' => 'hash_path',
|
||||
'label' => __('Hash path'),
|
||||
'type' => 'input',
|
||||
'placeholder' => 'Tag.name',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors=[]): bool
|
||||
{
|
||||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
$selector = $params['selector']['value'];
|
||||
$path = $params['hash_path']['value'];
|
||||
$operator = $params['operator']['value'];
|
||||
$value = $params['value']['value'];
|
||||
$rData = $roamingData->getData();
|
||||
|
||||
$applyFilterFunction = function ($element) use ($value, $operator, $path) {
|
||||
$selectedData = Hash::extract($element, $path);
|
||||
return $this->evaluateCondition($selectedData, $operator, $value);
|
||||
};
|
||||
$filteredData = Hash::apply($rData, $selector, $applyFilterFunction);
|
||||
debug($filteredData);
|
||||
$newRData = $filteredData;
|
||||
$newRData['_unfilteredData'] = $rData;
|
||||
$roamingData->setData($newRData);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
include_once APP . 'Model/WorkflowModules/WorkflowBaseModule.php';
|
||||
|
||||
class Module_generic_filter_reset extends WorkflowFilteringLogicModule
|
||||
{
|
||||
public $id = 'generic-filter-reset';
|
||||
public $name = 'Filter :: Remove filter';
|
||||
public $description = 'Reset filtering';
|
||||
public $icon = 'redo-alt';
|
||||
public $inputs = 1;
|
||||
public $outputs = 1;
|
||||
// public $html_template = 'filter-remove';
|
||||
public $params = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->params = [
|
||||
[
|
||||
'id' => 'filtering-label',
|
||||
'label' => __('Filtering Label to remove'),
|
||||
'type' => 'select',
|
||||
'options' => ['all' => __('All filters')] + $this->_genFilteringLabels(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors=[]): bool
|
||||
{
|
||||
parent::exec($node, $roamingData, $errors);
|
||||
$rData = $roamingData->getData();
|
||||
$newRData = $rData['_unfilteredData'];
|
||||
$roamingData->setData($newRData);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -665,4 +665,15 @@
|
|||
|
||||
.drawflow .drawflow-node.block-type-concurrent > .outputs > .output_1::after {
|
||||
content: "\f074";
|
||||
}
|
||||
|
||||
.drawflow svg.connection .connection-label-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 3px;
|
||||
width: fit-content;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #ffffffaa;
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
|
@ -129,6 +129,9 @@ var dotBlock_error = doT.template(' \
|
|||
</div> \
|
||||
</div>')
|
||||
|
||||
var dotBlock_connectionLabel = doT.template(' \
|
||||
<span class="label label-{{=it.variant}}" id="{{=it.id}}">{{=it.name}}</span>')
|
||||
|
||||
var classBySeverity = {
|
||||
'info': 'info',
|
||||
'warning': 'warning',
|
||||
|
@ -771,7 +774,11 @@ function loadWorkflow(workflow) {
|
|||
Object.values(workflow.data).forEach(function (node) {
|
||||
for (var input_name in node.inputs) {
|
||||
node.inputs[input_name].connections.forEach(function (connection) {
|
||||
editor.addConnection(connection.node, node.id, connection.input, input_name)
|
||||
connection.labels = connection.labels === undefined ? [] : connection.labels;
|
||||
var labels = connection.labels.map(function(labelConf) {
|
||||
return dotBlock_connectionLabel(labelConf)
|
||||
})
|
||||
editor.addConnection(connection.node, node.id, connection.input, input_name, labels)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -868,7 +875,7 @@ function addNodesFromBlueprint(workflowBlueprint, cursorPosition) {
|
|||
left: (node.pos_x - minX) * editor.zoom + cursorPosition.left,
|
||||
}
|
||||
if (all_modules_by_id[node.data.id] === undefined) {
|
||||
var errorMessage = 'Invalid ' + node.data.module_data.module_type + ' module id `' + node.data.module_data.id + '` (' + node.id + ')'
|
||||
var errorMessage = 'Invalid ' + node.data.module_type + ' module id `' + node.data.id + '` (' + node.id + ')'
|
||||
var html = window['dotBlock_error']({
|
||||
error: errorMessage,
|
||||
data: JSON.stringify(node.data.indexed_params, null, 2)
|
||||
|
@ -883,20 +890,20 @@ function addNodesFromBlueprint(workflowBlueprint, cursorPosition) {
|
|||
node.data,
|
||||
html
|
||||
)
|
||||
return
|
||||
} else {
|
||||
additionalData = {
|
||||
indexed_params: node.data.indexed_params,
|
||||
saved_filters: node.data.saved_filters,
|
||||
}
|
||||
addNode(all_modules_by_id[node.data.id], position, additionalData)
|
||||
}
|
||||
additionalData = {
|
||||
indexed_params: node.data.indexed_params,
|
||||
saved_filters: node.data.saved_filters,
|
||||
}
|
||||
addNode(all_modules_by_id[node.data.id], position, additionalData)
|
||||
oldNewIDMapping[node.id] = editor.nodeId - 1
|
||||
newNodes.push(getNodeHtmlByID(editor.nodeId - 1)) // nodeId is incremented as soon as a new node is created
|
||||
})
|
||||
workflowBlueprint.data.forEach(function (node) {
|
||||
Object.keys(node.outputs).forEach(function (outputName) {
|
||||
var newNode = Object.assign({}, all_modules_by_id[node.data.id])
|
||||
if (newNode.outputs > 0) { // make sure the module configuration didn't change in regards of the outputs
|
||||
var outputCount = all_modules_by_id[node.data.id] !== undefined ? all_modules_by_id[node.data.id].outputs : Object.keys(node.outputs).length
|
||||
if (outputCount > 0) { // make sure the module configuration didn't change in regards of the outputs
|
||||
node.outputs[outputName].connections.forEach(function (connection) {
|
||||
if (oldNewIDMapping[connection.node] !== undefined) {
|
||||
editor.addConnection(
|
||||
|
|
Loading…
Reference in New Issue