var dotBlock_default = doT.template(' \
\
\
\ {{? it.module_data.icon }} \ \ {{?}} \ {{? it.module_data.icon_path }} \ Icon of {{=it.module_data.name}} \ {{?}} \ \ {{=it.module_data.name}} \ \ {{? it.module_data.is_misp_module }} \ \ {{?}} \ {{? it.module_data.blocking }} \ \ \ \ {{?}} \ \ \ {{=it._node_notification_html}} \ \ \ \ {{=it._node_filter_html}} \ \ \
\
{{=it.module_data.description}}
\ {{=it._node_param_html}} \
\
') var dotBlock_trigger = doT.template(' \
\
\
\ {{? it.module_data.icon }} \ \ {{?}} \ {{? it.module_data.icon_path }} \ Icon of {{=it.module_data.name}} \ {{?}} \ \ {{=it.module_data.name}} \ \ \ {{? it.module_data.blocking }} \ \ \ Blocking \ \ {{?}} \ {{? it.module_data.misp_core_format }} \ \ MISP Core format \ \ {{?}} \ \ {{=it._node_notification_html}} \ \ \
\
\
') var dotBlock_if = doT.template(' \
\
\
\ {{? it.module_data.icon }} \ \ {{?}} \ {{? it.module_data.icon_path }} \ Icon of {{=it.module_data.name}} \ {{?}} \ \ {{=it.module_data.name}} \ \ \ \ {{=it._node_notification_html}} \ \ \ \ \ \
\ {{=it._node_param_html}} \
\
') var dotBlock_concurrent = doT.template(' \
\
\
\ {{? it.module_data.icon }} \ \ {{?}} \ {{? it.module_data.icon_path }} \ Icon of {{=it.module_data.name}} \ {{?}} \ \ {{=it.module_data.name}} \ \ \ \ {{=it._node_notification_html}} \ \ \ \ \ \
\ {{=it._node_param_html}} \
{{=it.module_data.description}}
\
\
') var dotBlock_error = doT.template(' \
\
\
{{=it.error}}
\
Data:
\ \
\
') var dotBlock_connectionLabel = doT.template(' \ {{=it.name}}') var classBySeverity = { 'info': 'info', 'warning': 'warning', 'error': 'danger', } var iconBySeverity = { 'info': 'fa-times-circle', 'warning': 'fa-exclamation-triangle', 'error': 'fa-exclamation-circle', } var severities = ['info', 'warning', 'error'] var haspathQuickPickMenuElementSelector = [ { 'name': 'All Attributes', 'path': 'Event._AttributeFlattened.{n}' }, { 'name': 'All tags attached to all Attributes', 'path': 'Event._AttributeFlattened.{n}.Tag.{n}.name' }, { 'name': 'All tags attached to the Event', 'path': 'Event.Tag.{n}.name' }, ] var haspathQuickPickMenuSubElementSelector = [ { 'name': 'Attribute type', 'path': 'type' }, { 'name': 'All tags', 'path': 'Tag.{n}.name' }, { 'name': 'Warnings from warninglists', 'path': 'warnings.{n}.warninglist_category' }, { 'name': 'Feed correlation', 'path': 'Feed.{n}.name' }, { 'name': 'All enrichments', 'path': 'enrichment.{n}' }, ] var workflow_id = 0 var contentChanged = false var lastModified = 0 var graphPooler function sanitizeObject(obj) { var newObj = {} for (var key of Object.keys(obj)) { var newVal = $('

').text(obj[key]).html() newObj[key] = newVal } return newObj } function initDrawflow() { workflow_id = $drawflow.data('workflowid') editor = new Drawflow($drawflow[0]); editor.start(); editor.on('nodeCreated', function() { invalidateContentCache() }) editor.on('nodeRemoved', function () { invalidateContentCache() }) editor.on('frameNodeCreated', function (framenodeUuid) { invalidateContentCache() var frameNodeFullUuid = 'framenode-' + framenodeUuid $('#' + frameNodeFullUuid).find('.drawflow-framenode-text').dblclick(function() { var existingText = $(this).text() var newText = prompt('Edit frame node text', existingText) editor.updateFramenode(frameNodeFullUuid, {text: newText}) }) }) editor.on('frameNodeUpdated', function () { invalidateContentCache() }) editor.on('frameNodeRemoved', function () { invalidateContentCache() }) editor.on('nodeDataChanged', invalidateContentCache) editor.on('nodeMoved', invalidateContentCache) editor.on('connectionCreated', function() { invalidateContentCache() if (!editor.isLoading) { graphPooler.do() } }) editor.on('connectionRemoved', function() { invalidateContentCache() if (!editor.isLoading) { graphPooler.do() } }) editor.on('keydown', function (evt) { if (evt.keyCode == 67 && $drawflow.is(evt.target)) { editor.fitCanvas() } if (evt.keyCode == 83 && evt.ctrlKey && $drawflow.is(evt.target)) { saveWorkflow() evt.preventDefault() } if (evt.keyCode == 46 && $drawflow.is(evt.target)) { deleteSelectedNodes(true) } if (evt.keyCode == 68 && evt.ctrlKey && $drawflow.is(evt.target)) { duplicateSelection() evt.preventDefault() } if (evt.keyCode == 70 && $drawflow.is(evt.target)) { createFrameNodeForSelected() } }) editor.translate_to = function (x, y) { this.canvas_x = x; this.canvas_y = y; let storedZoom = this.zoom; this.zoom = 1; this.precanvas.style.transform = "translate(" + this.canvas_x + "px, " + this.canvas_y + "px) scale(" + this.zoom + ")"; this.zoom = storedZoom; this.zoom_last_value = 1; this.zoom_refresh(); } editor.fitCanvas = function () { editor.translate_to(0, 0) editor.zoom = 1 editor.zoom_min = 0.3 editor.zoom_refresh() var sidebarWidth = 340 var parentOffsetY = editor.precanvas.parentElement.getBoundingClientRect().top var editor_bcr = editor.container.getBoundingClientRect() var offset_x = (editor_bcr.width + sidebarWidth) / 2 var offset_y = (editor_bcr.height - parentOffsetY) / 2 var canvasCentroid = getCanvasCentroid() var calc_zoom = Math.min(1, Math.min( (editor_bcr.width - sidebarWidth) / ((canvasCentroid.maxX - canvasCentroid.minX) + sidebarWidth), editor_bcr.height / ((canvasCentroid.maxY - canvasCentroid.minY) - parentOffsetY) ), ) // Zoom out if needed calc_zoom = calc_zoom > 0 ? calc_zoom : 1 calc_zoom = calc_zoom * 0.95 offset_x += 100 * (1 / calc_zoom) // dirty fix to offset the position relative to the sidebar offset_y -= 100 * (1 / calc_zoom) // dirty fix to slightly move the graph up editor.translate_to( offset_x - canvasCentroid.centroidX, offset_y - canvasCentroid.centroidY ) editor.zoom = calc_zoom editor.zoom_refresh() } $('#block-tabs a').click(function (e) { e.preventDefault(); $(this).tab('show'); }) $chosenBlocks.chosen({width: '320px'}) .on('change', function (evt, param) { var selection = param.selected var selected_module = all_modules_by_id[selection] var canvasBR = $canvas[0].getBoundingClientRect() var position = { top: canvasBR.height / 2 - canvasBR.top, left: canvasBR.left + canvasBR.width / 2 } if ($(this).hasClass('blueprint-select')) { addWorkflowBlueprint(selection) } else { addNode(selected_module, position) } }); $('.sidebar-workflow-block').each(function() { var $block = $(this) $block.data('module', all_modules_by_id[$block[0].id]) if ($(this).data('module').disabled) { $(this).addClass('disabled') } $(this).draggable({ helper: "clone", scroll: false, disabled: $(this).data('module').disabled, start: function (event, ui) { }, stop: function (event, ui) { } }); }) $('.sidebar-workflow-blueprints').each(function () { var $block = $(this) $block.data('blueprint', all_workflow_blueprints_by_id[$block[0].id]) $(this).draggable({ helper: "clone", scroll: false, start: function (event, ui) { }, stop: function (event, ui) { } }); }) $canvas.droppable({ drop: function (event, ui) { if (event.pageX < 340) { // dirty hack to avoid drops on the sidebar return } ui.position.top += 96 // take padding/marging/position into account if (ui.draggable.data('blueprint')) { addWorkflowBlueprint(ui.draggable.data('blueprint').WorkflowBlueprint.id, ui.position) } else { var node = addNode(ui.draggable.data('module'), ui.position) var fakeUi = { draggable: ui.helper, position: { left: ui.helper[0].getBoundingClientRect().left, top: ui.helper[0].getBoundingClientRect().top } } var link = getLinkUnderElement(fakeUi) if (link !== undefined) { insertNodeOnLink(node, link) } } }, activate: function (event, ui) { var $pathsWithClass = $() ui.helper.mousemove(function (event) { var fakeUi = { draggable: $(this), position: {left: this.getBoundingClientRect().left, top: this.getBoundingClientRect().top} } var links = getLinkUnderElement(fakeUi) $pathsWithClass.removeClass('link-hover-for-insertion') if (links) { $paths = $(links).find('path') $paths.addClass('link-hover-for-insertion') $pathsWithClass = $paths } }) }, }); graphPooler = new TaskScheduler(checkGraphProperties, { interval: 10000, slowInterval: 60000, }) filterModules($blockFilterGroup.find('button.active')[0]) fetchAndLoadWorkflow().then(function() { graphPooler.start(undefined) editor.fitCanvas() // block contextual menu for trigger blocks $canvas.find('.canvas-workflow-block').on('contextmenu', function (evt) { var selectedNode = getSelectedNode() if (selectedNode !== undefined && selectedNode.data.module_data.module_type == 'trigger') { evt.stopPropagation(); evt.preventDefault(); } }) }) $saveWorkflowButton.click(saveWorkflow) $importWorkflowButton.click(importWorkflow) $exportWorkflowButton.click(exportWorkflow) $toggleWorkflowButton.click(enabledDebugMode) $runWorkflowButton.click(runWorkflow) $blockModal .on('show', function (evt) { var selectedNode = getSelectedNode() buildModalForBlock(selectedNode.id, selectedNode) }) .on('shown', function (evt) { afterModalShowCallback() }) $blockModalDeleteButton.click(function() { if (confirm('Are you sure you want to remove this block?')) { deleteSelectedNode() } $blockModal.modal('hide') }) $drawflow.on('mousedown', function (evt) { if (evt.shiftKey) { editor.editor_selected = false // evt.stopPropagation() } }) editor.on('nodeCreated', function(node_id) { $drawflow.find('#node-'+node_id).on('mousedown', function (evt) { var selected_ids = selection.getSelection().map(function(e) { return e.id.slice(5) }) if (selected_ids.indexOf(this.id.slice(5)) !== -1) { editor.node_selected = this // Allow moving multiple nodes from any nodes of the selection } }) }) editor.last_x = 0; editor.last_y = 0; editor.on('mouseMove', function(coordinates) { // Credit: https://github.com/jerosoler/Drawflow/issues/322#issuecomment-1133036432 if (selection.getSelection() && editor.drag) { selection.getSelection().forEach(function(node) { var node_id = node.id.slice(5) if (node_id != editor.node_selected.id.slice(5)) { // Drawflow default behavior will also move the node var xnew = (editor.last_x - coordinates.x) * editor.precanvas.clientWidth / (editor.precanvas.clientWidth * editor.zoom) var ynew = (editor.last_y - coordinates.y) * editor.precanvas.clientHeight / (editor.precanvas.clientHeight * editor.zoom) node.style.top = (node.offsetTop - ynew) + 'px' node.style.left = (node.offsetLeft - xnew) + 'px' editor.drawflow.drawflow[editor.module].data[node_id].pos_x = (node.offsetLeft - xnew) editor.drawflow.drawflow[editor.module].data[node_id].pos_y = (node.offsetTop - ynew) editor.updateConnectionNodes(node.id); } }) } editor.last_x = coordinates.x editor.last_y = coordinates.y }) editor.on('nodeSelected', function(node_id) { $controlDuplicateButton.removeClass('disabled') $controlFrameNodeButton.removeClass('disabled') $controlDeleteButton.removeClass('disabled') $controlSaveBlocksLi.removeClass('disabled') $controlEditBlocksLiContainer.removeClass('disabled').find('.dropdown-menu') selection.select([getNodeHtmlByID(node_id)]) }) editor.on('nodeUnselected', function() { selection.getSelection().forEach(function (el) { el.classList.remove('selected') }) selection.clearSelection() $controlDuplicateButton.addClass('disabled') $controlFrameNodeButton.addClass('disabled') $controlDeleteButton.addClass('disabled') $controlSaveBlocksLi.addClass('disabled') $controlEditBlocksLiContainer.addClass('disabled').find('.dropdown-menu') }) selection = new SelectionArea({ selectables: ['#drawflow .drawflow-node'], boundaries: ['#drawflow'] }) .on('beforestart', function (data) { var evt = data.event if (!evt.shiftKey) { return false } }) .on('start', function(data) { var store = data.store var evt = data.event if (!evt.ctrlKey && !evt.metaKey) { store.stored.forEach(function(el) { el.classList.remove('selected'); }) selection.clearSelection(); } }) .on('move', function(data) { var store = data.store var added = store.changed.added var removed = store.changed.removed added.forEach(function (el) { el.classList.add('selected'); }) removed.forEach(function (el) { el.classList.remove('selected'); }) }) .on('stop', function (data) { var store = data.store if (store.selected.length > 0) { editor.node_selected = store.selected[0] editor.dispatch('nodeSelected', editor.node_selected.id.slice(5)); } }) $controlDuplicateButton.click(function() { duplicateSelection() }) $controlFrameNodeButton.click(function() { createFrameNodeForSelected() }) $controlDeleteButton.click(function() { deleteSelectedNodes(false) }) $controlSaveBlocksLi.click(function(evt) { var $link = $(this).find('a') evt.preventDefault() if (!$(this).hasClass('disabled')) { saveBlueprint($link.attr('href')) } }) $controlEditBlocksLis.click(function(evt) { var $link = $(this).find('a') evt.preventDefault() if (!$(this).hasClass('disabled')) { saveBlueprint($link.attr('href')) } }) $saveBlueprintButton.click(function(evt) { evt.preventDefault() saveBlueprint($(this).attr('href')) }) $(window).bind('beforeunload', function() { if (contentChanged) { return false; } }) } function saveBlueprint(href) { var selectedNodes = selection.getSelection() var editorData = getEditorData(true) openGenericModal(href, undefined, function () { var trigger_id = (all_triggers_by_id[workflowTriggerId] || { id: 'unknown-trigger' }).id var nodes = selectedNodes.map(function (nodeHtml) { var node = editorData[nodeHtml.id.slice(5)] return node }) var $modal = $('#genericModal') var $graphData = $modal.find('form #WorkflowBlueprintData') var $graphDescription = $modal.find('form #WorkflowBlueprintDescription') $graphData.val(JSON.stringify(nodes)) if ($graphDescription.val().length == 0 ) { $graphDescription.val('[' + trigger_id + ']\n') } var $modalBody = $modal.find('.modal-body') $modalBody.append( $('

').append( $('') .attr('title', 'Copy Workflow Blueprint to clipboard') .click(function () { var $clicked = $(this) navigator.clipboard.writeText(JSON.stringify(nodes)).then(function () { $clicked.removeClass('fa-copy').addClass('fa-check').addClass('text-success') setTimeout(function () { $clicked.removeClass('fa-check').addClass('fa-copy').removeClass('text-success') }, 2000); }, function (err) { console.error('Async: Could not copy text: ', err); }); }), ) ) var $ul = $('') nodes.forEach(function (node) { var validParams = {} Object.entries(node.data.indexed_params).forEach(function (e) { var k = e[0], v = e[1] if (v) { validParams[k] = v } }) var validFilters = {} Object.entries(node.data.saved_filters).forEach(function (e) { var k = e[0], v = e[1] if (v) { validFilters[k] = v } }) $ul.append( $('
  • ').append( $('').text(node.data.name), $('').append( Object.values(validFilters).length == 0 ? null : $('
  • ').text('Has filter').attr('title', JSON.stringify(validFilters, null, 4)), Object.values(validParams).length == 0 ? null : $('
  • ').text('Has ' + Object.values(validParams).length + ' parameters').attr('title', JSON.stringify(validParams, null, 4)) ) ) ) }) $modalBody.append($ul) }) } function duplicateSelection() { var currentSelection = selection.getSelection() var newNodes = duplicateNodesFromHtml(currentSelection) selection.clearSelection() selection.select(newNodes) } function createFrameNodeForSelected() { var text = prompt('Enter text for the frame node') var selectedNodesHtml = selection.getSelection() var selectedIDs = selectedNodesHtml.map(function(nodeHtml) { return parseInt(nodeHtml.id.slice(5)) }) createFrameForNodes(selectedIDs, text) selection.clearSelection() invalidateContentCache() } function createFrameForNodes(nodesIDs, text) { const frameNode = { nodes: nodesIDs, text: text, class: "", } editor.addFrameNode(frameNode) } function buildModalForBlock(node_id, node) { var html = genNodeParamHtml(node, false) $blockModal .data('selected-block', node.data) .data('selected-node-id', node_id) $blockModal.find('.modal-body').empty().append(html) } function buildNotificationModalForBlock(node_id, data) { var html = genBlockNotificationForModalHtml(data) $blockNotificationModal .data('selected-block', data) .data('selected-node-id', node_id) $blockNotificationModal.find('.modal-body').empty().append(html) } function buildFilteringModalForNode(node_id, node) { var html = genModalFilteringHtml(node) $blockFilteringModal .data('selected-block', node.data) .data('selected-node-id', node_id) $blockFilteringModal.find('.modal-body').empty().append(html) } function showNotificationModalForBlock() { var selectedNode = getSelectedNode() buildNotificationModalForBlock(selectedNode.id, selectedNode.data) $blockNotificationModal.modal('show') } function showNotificationModalForModule(module_id, data) { buildNotificationModalForBlock(module_id, {module_data: data}) $blockNotificationModal.modal('show') } function showNotificationModalForSidebarModule(clicked) { var $block = $(clicked).closest('.sidebar-workflow-block') var blockID = $block.data('blockid') showNotificationModalForModule(blockID, all_modules_by_id[blockID]) } function showFilteringModalForNode() { var selectedNode = getSelectedNode() buildFilteringModalForNode(selectedNode.id, selectedNode) $blockFilteringModal.modal('show') } function invalidateContentCache() { changeDetectedMessage = ' Last saved change: ' contentChanged = true toggleSaveButton(true) $workflowSavedIconContainer.removeClass('text-success').addClass('text-error') $workflowSavedIconText .removeClass('text-success').addClass('text-error') .text('not saved') $workflowSavedIconTextDetails.text(changeDetectedMessage + moment(parseInt(lastModified)).fromNow()) } function revalidateContentCache() { changeDetectedMessage = ' Last saved change: ' contentChanged = false toggleSaveButton(false) $workflowSavedIconContainer.removeClass('text-error').addClass('text-success') $workflowSavedIconText .removeClass('text-error').addClass('text-success') .text('saved') $workflowSavedIconTextDetails.text(changeDetectedMessage + moment(parseInt(lastModified)).fromNow()) } function addNode(block, position, additionalData={}) { var module = all_modules_by_id[block.id] || all_triggers_by_id[block.id] if (!module) { console.error('Tried to add node for unknown module ' + block.data.id + ' (' + block.id + ')') return ''; } var node_uid = uid() // only used for UI purposes block['node_uid'] = node_uid var pos_x = position.left; var pos_y = position.top; // Credit: Drawflow example page pos_x = pos_x * (editor.precanvas.clientWidth / (editor.precanvas.clientWidth * editor.zoom)) - (editor.precanvas.getBoundingClientRect().x * (editor.precanvas.clientWidth / (editor.precanvas.clientWidth * editor.zoom))); pos_y = pos_y * (editor.precanvas.clientHeight / (editor.precanvas.clientHeight * editor.zoom)) - (editor.precanvas.getBoundingClientRect().y * (editor.precanvas.clientHeight / (editor.precanvas.clientHeight * editor.zoom))); var module_data = Object.assign({}, module) var newNode = {name: block.name, data: {}} if (additionalData.indexed_params) { newNode.data.indexed_params = additionalData.indexed_params } if (additionalData.saved_filters) { newNode.data.saved_filters = additionalData.saved_filters } newNode = mergeNodeAndModule(newNode, module_data) newNode.data['_node_param_html'] = genNodeParamHtml(newNode) newNode.data['_node_notification_html'] = genNodeNotificationHtml(newNode.data) newNode.data['_node_filter_html'] = genNodeFilteringHtml(newNode) var html = getTemplateForNode(newNode) var blockClass = newNode.data.module_data.class === undefined ? [] : newNode.data.module_data.class blockClass = !Array.isArray(blockClass) ? [blockClass] : blockClass blockClass.push('block-type-' + (newNode.data.module_data.html_template !== undefined ? newNode.data.module_data.html_template : 'default')) if (newNode.data.module_data.module_type == 'logic') { blockClass.push('block-type-logic') } if (newNode.data.module_data.expect_misp_core_format) { blockClass.push('expect-misp-core-format') } editor.addNode( newNode.name, module.inputs === undefined ? 1 : module.inputs, module.outputs === undefined ? 1 : module.outputs, pos_x, pos_y, blockClass.join(' '), newNode.data, html ) afterNodeDrawCallback() return editor.getNodeFromId(editor.nodeId-1) } function getEditorData(cleanNodes) { var data = {} // Make sure nodes are index by their internal IDs var editorExport = editor.export().drawflow.Home.data var frameNodes = editorExport._frames !== undefined ? editorExport._frames : {} delete editorExport._frames editorExport = Array.isArray(editorExport) ? editorExport : Object.values(editorExport) editorExport.forEach(function(node) { if (node !== null) { // for some reason, the editor create null nodes if (cleanNodes && node.data.params !== undefined) { node.data.params = deleteInvalidParams(node.data.params) cleanedIndexedParams = {} node.data.params.forEach(function(param) { cleanedIndexedParams[param.id] = param.value !== undefined ? param.value : param.default }) node.data.indexed_params = cleanedIndexedParams } if (cleanNodes) { delete node.html delete node.data.module_data delete node.data.params } Object.keys(node.data).forEach(function (k) { if (k.startsWith('_')) { delete node.data[k] } }) data[node.id] = node } }) data._frames = frameNodes return data } function deleteInvalidParams(params) { return params.filter(function(param) { return !param.is_invalid }) } function fetchAndLoadWorkflow() { return new Promise(function (resolve, reject) { editor.isLoading = true fetchWorkflow(workflow_id, function (workflow) { lastModified = workflow.timestamp + '000' loadWorkflow(workflow) editor.isLoading = false revalidateContentCache() resolve() }) }) } function loadWorkflow(workflow) { editor.clear() if (workflow.data.length == 0) { console.error('Workflow doesn\'t have a trigger.') showMessage('fail', 'Workflow doesn\'t have a trigger.') return } // We cannot rely on the editor's import function as it recreates the nodes with the saved HTML instead of rebuilding them // We have to manually add the nodes and their connections Object.entries(workflow.data).forEach(function (entry) { var i = entry[0] var node = entry[1] if (i == '_frames') { return } var module = all_modules_by_id[node.data.id] || all_triggers_by_id[node.data.id] if (!module) { console.error('Tried to add node for unknown module ' + node.data.id + ' (' + node.id + ')') var html = window['dotBlock_error']({ error: 'Invalid module id`' + node.data.id + '` (' + node.id + ')', data: JSON.stringify(node.data.indexed_params, null, 2) }) editor.addNode( node.name, Object.values(node.inputs).length, Object.values(node.outputs).length, node.pos_x, node.pos_y, '', node.data, html ) return } var module_data = Object.assign({}, module) var newNode = mergeNodeAndModule(node, module_data) newNode.data['_node_param_html'] = genNodeParamHtml(newNode) newNode.data['_node_notification_html'] = genNodeNotificationHtml(newNode.data) newNode.data['_node_filter_html'] = genNodeFilteringHtml(newNode) var nodeClass = newNode.data.module_data.class === undefined ? [] : newNode.data.module_data.class nodeClass = !Array.isArray(nodeClass) ? [nodeClass] : nodeClass nodeClass.push('block-type-' + (newNode.data.module_data.html_template !== undefined ? newNode.data.module_data.html_template : 'default')) if (newNode.data.module_data.module_type == 'logic') { nodeClass.push('block-type-logic') } if (newNode.data.module_data.expect_misp_core_format) { nodeClass.push('expect-misp-core-format') } if (newNode.data.module_data.disabled) { nodeClass.push('disabled') } var html = getTemplateForNode(newNode) editor.nodeId = newNode.id // force the editor to use the saved id of the node instead of generating a new one editor.addNode( newNode.data.name, Object.values(newNode.inputs).length, Object.values(newNode.outputs).length, newNode.pos_x, newNode.pos_y, nodeClass.join(' '), newNode.data, html ) }) afterNodeDrawCallback() Object.values(workflow.data).forEach(function (node) { for (var input_name in node.inputs) { node.inputs[input_name].connections.forEach(function (connection) { 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) }) } }) var frameNodes = workflow.data._frames || {} Object.values(frameNodes).forEach(function (frameNode) { editor.addFrameNode(frameNode) }) } function filterModules(clicked) { var $activeButton = $(clicked) var selectedFilter if ($activeButton.length > 0) { selectedFilter = $activeButton.data('type') } else { selectedFilter = 'enabled' } var $modulesToShow = $('.sidebar .tab-pane.active').find('.sidebar-workflow-block') $modulesToShow.show() if (selectedFilter == 'enabled') { $modulesToShow.filter(function() { return $(this).data('module')['disabled'] }).hide() } else if (selectedFilter == 'misp-module') { $modulesToShow.filter(function () { return !$(this).data('module')['is_misp_module'] || $(this).data('module')['disabled'] }).hide() } else if (selectedFilter == 'is-blocking') { $modulesToShow.filter(function () { return !$(this).data('module')['blocking'] || $(this).data('module')['disabled'] }).hide() } } function duplicateNodesFromHtml(currentSelection) { var selectedNodeIDs = currentSelection.map(function (nodeHtml) { return nodeHtml.id.slice(5) }) var newNodes = [] var oldNewIDMapping = {} currentSelection.forEach(function (nodeHtml) { nodeHtml.classList.remove('selected'); var node_id = nodeHtml.id.slice(5) var node = getEditorData()[node_id] if (node.data.module_data.module_type == 'trigger') { return } var position = { top: nodeHtml.getBoundingClientRect().top - 100 * editor.zoom, left: nodeHtml.getBoundingClientRect().left + 100 * editor.zoom, } var newNode = Object.assign({}, all_modules_by_id[node.data.module_data.id]) var additionalData = { indexed_params: node.data.indexed_params, saved_filters: node.data.saved_filters, } addNode(newNode, 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 }) selectedNodeIDs.forEach(function (node_id) { var node = getEditorData()[node_id] Object.keys(node.outputs).forEach(function (outputName) { node.outputs[outputName].connections.forEach(function (connection) { if (selectedNodeIDs.includes(connection.node)) { editor.addConnection( oldNewIDMapping[node_id], oldNewIDMapping[connection.node], outputName, connection.output, [] ) } }); }) }) return newNodes } function addNodesFromBlueprint(workflowBlueprint, cursorPosition) { var newNodes = [] if (workflowBlueprint.data.length == 0) { return counterNodeAdded } var oldNewIDMapping = {} // We position all nodes relatively based on the left most node var minX = workflowBlueprint.data[0].pos_x var matchingY = workflowBlueprint.data[0].pos_y workflowBlueprint.data.forEach(function(node) { minX = node.pos_x < minX ? node.pos_x : minX matchingY = node.pos_x < minX ? node.pos_y : matchingY }) workflowBlueprint.data.forEach(function(node) { if (node.data.module_type == 'trigger') { return } var position = { top: (node.pos_y - matchingY) * editor.zoom + cursorPosition.top, left: (node.pos_x - minX) * editor.zoom + cursorPosition.left, } if (all_modules_by_id[node.data.id] === undefined) { 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) }) editor.addNode( node.name, Object.values(node.inputs).length, Object.values(node.outputs).length, node.pos_x, node.pos_y, '', node.data, html ) } else { 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 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( oldNewIDMapping[node.id], oldNewIDMapping[connection.node], outputName, connection.output, [] ) } }); } }) }) return newNodes } function getCanvasCentroid() { var parentOffsetY = editor.precanvas.parentElement.getBoundingClientRect().top var maxX = 0, maxY = 0, minX = 9999999, minY = 9999999 var nodes = $(editor.precanvas).find('.drawflow-node') nodes.each(function () { var node_bcr = JSON.parse(JSON.stringify(this.getBoundingClientRect())) node_bcr.top = node_bcr.top - parentOffsetY // Make bcr relative maxX = (node_bcr.left + node_bcr.width) > maxX ? (node_bcr.left + node_bcr.width) : maxX maxY = (node_bcr.top + node_bcr.height) > maxY ? (node_bcr.top + node_bcr.height) : maxY minX = node_bcr.left < minX ? node_bcr.left : minX minY = node_bcr.top < minY ? node_bcr.top : minY }); var centroidX = (Math.abs(maxX) - Math.abs(minX)) / 2 var centroidY = (Math.abs(maxY) - Math.abs(minY)) / 2 return { centroidX: centroidX, centroidY: centroidY, minX: minX, minY: minY, maxX: maxX, maxY: maxY, } } function mergeNodeAndModule(node, module_data) { if (node.data === undefined) { node.data = {} } node.data.node_uid = uid() // only used for UI purposes node.data.params = node.data.params !== undefined ? node.data.params : [] node.data.indexed_params = node.data.indexed_params !== undefined ? node.data.indexed_params : {} node.data.saved_filters = node.data.saved_filters !== undefined ? node.data.saved_filters : {} node.data.module_type = module_data.module_type node.data.id = module_data.id node.data.name = node.data.name ? node.data.name : module_data.name node.data.module_data = module_data node.data.multiple_output_connection = module_data.multiple_output_connection node.data.previous_module_version = node.data.module_version ? node.data.module_version : '?' node.data.module_version = module_data.version node.data.params = mergeNodeAndModuleParams(node, module_data.params) node.data.indexed_params = getIndexedParams(node, module_data.params) node.data.saved_filters = mergeNodeAndModuleFilters(node, module_data.saved_filters) return node } function mergeNodeAndModuleParams(node, moduleParams) { var moduleParamsById = {} var nodeParamsById = {} moduleParams.forEach(function (param, i) { if (param.id === undefined) { // Param id is not set in the module definition. param.id = 'param-' + i param.no_id = true } moduleParamsById[param.id] = param }) Object.entries(node.data.indexed_params).forEach(function (e) { var param_id = e[0], val = e[1] nodeParamsById[param_id] = { id: param_id, label: param_id, type: 'input', value: val } }) var procesedParams = {} var finalParams = [] var fakeNodeFullParams = Object.values(nodeParamsById) var nodeAndModuleParams = moduleParams.concat(fakeNodeFullParams) nodeAndModuleParams.forEach(function (param) { var finalParam if (procesedParams[param.id]) { // param has already been processed return; } procesedParams[param.id] = true if (moduleParamsById[param.id] === undefined) { // Param do not exist in the module (anymore or never did) param.is_invalid = true finalParam = Object.assign({}, nodeParamsById[param.id]) } else { finalParam = Object.assign({}, moduleParamsById[param.id]) finalParam.value = node.data.indexed_params[param.id] } if (!finalParam['param_id']) { finalParam['param_id'] = getIDForNodeParameter(node, finalParam) } finalParams.push(finalParam) }) return finalParams } function getIndexedParams(node, moduleParams) { var finalParams = {} moduleParams.forEach(function (param, i) { if (param.id === undefined) { // Param id is not set in the module definition. param.id = 'param-' + i param.no_id = true } finalParams[param.id] = node.data.indexed_params[param.id] ? node.data.indexed_params[param.id] : (param.default ? param.default : '') }) return finalParams } function mergeNodeAndModuleFilters(node, moduleFilters) { node.saved_filters = node.data.saved_filters ? node.data.saved_filters : [] var finalFilters = {} moduleFilters.forEach(function(filter) { finalFilters[filter.text] = node.data.saved_filters[filter.text] ? node.data.saved_filters[filter.text] : filter.value }) return finalFilters } /* API */ function fetchWorkflow(id, callback) { var url = baseurl + '/workflows/view/' + id + '.json' $.ajax({ beforeSend: function () { toggleEditorLoading(true, 'Loading workflow') }, success: function (workflow, textStatus) { if (workflow) { workflow = workflow.Workflow showMessage('success', 'Workflow fetched'); if (callback !== undefined) { callback(workflow) } } }, error: function (jqXHR, textStatus, errorThrown) { showMessage('fail', saveFailedMessage + ': ' + errorThrown); if (callback !== undefined) { callback(false) } }, complete: function () { toggleEditorLoading(false) }, type: "post", url: url }) } function saveWorkflow(confirmSave, callback) { saveConfirmMessage = 'Confirm saving the current state of the workflow' saveFailedMessage = 'Failed to save the workflow' confirmSave = confirmSave === undefined ? true : confirmSave if (confirmSave && !confirm(saveConfirmMessage)) { return } var url = baseurl + "/workflows/edit/" + workflow_id fetchFormDataAjax(url, function (formHTML) { $('body').append($('