'use strict'; // Function called to setup custom MarkdownIt rendering and parsing rules var markdownItCustomPostInit = markdownItCustomPostInit // Hint option passed to the CodeMirror constructor var cmCustomHints = hintMISPElements // Setup function called after the CodeMirror initialization var cmCustomSetup = cmCustomSetup // Hook allowing to alter the raw text before returning the GFM version to the user to be downloaded var markdownGFMSubstitution = replaceMISPElementByTheirValue // Post rendering hook called after the markdown is displayed, allowing to register listener var markdownCustomPostRenderingListener = setupMISPElementMarkdownListeners // Post rendering hook called after the markdown is display, allowing to perform any actions on the rendered markdown var markdownCustomPostRenderingActions = attachRemoteMISPElements // CodeMirror replacement/insertion actions that can be executed on the editor's text var customReplacementActions = MISPElementReplacementActions // Called after CodeMirror initialization to insert custom top bar buttons var insertCustomToolbarButtons = insertMISPElementToolbarButtons // Key of the model used by the form when saving var modelNameForSave = 'EventReport'; // Key of the field used by the form when saving var markdownModelFieldNameForSave = 'content'; var dotTemplateAttribute = doT.template("{{=it.type}}{{=it.value}}"); var dotTemplateAttributePicture = doT.template("
\"{{=it.alt}}\"
"); var dotTemplateGalaxyMatrix = doT.template("
"); var dotTemplateTag = doT.template("{{=it.elementid}}"); var dotTemplateObject = doT.template("{{=it.type}}{{=it.value}}"); var dotTemplateObjectAttribute = doT.template("{{=it.objectname}}{{=it.type}}{{=it.value}}"); var dotTemplateInvalid = doT.template("{{=it.scope}} ({{=it.id}})"); var dotCloseButtonTemplate = doT.template(''); var dotTemplateRenderingDisabled = doT.template("{{=it.value}}"); var dotTemplateSuggestionAttribute = doT.template("{{=it.type}}{{=it.value}}"); var renderingRules = { 'attribute': true, 'attribute-picture': true, 'object': true, 'object-attribute': true, 'tag': true, 'galaxymatrix': true, 'suggestion': true, } var galaxyMatrixTimer, tagTimers = {}; var cache_matrix = {}, cache_tag = {}; var firstCustomPostRenderCall = true; var contentBeforeSuggestions var typeToCategoryMapping var entitiesFromComplexTool var $suggestionContainer var unreferencedElements = { values: null, context: null }; var suggestionIDs = [] var suggestions = {} var pickedSuggestion = { tableID: null, tr: null, entity: null, index: null, isContext: null } /** _____ _ __ __ _ / ____| | | | \/ (_) | | ___ __| | ___| \ / |_ _ __ _ __ ___ _ __ | | / _ \ / _` |/ _ \ |\/| | | '__| '__/ _ \| '__| | |___| (_) | (_| | __/ | | | | | | | | (_) | | \_____\___/ \__,_|\___|_| |_|_|_| |_| \___/|_| */ function cmCustomSetup() { $suggestionContainer = $('
').attr('id', 'suggestion-container').addClass('hidden') $suggestionContainer.insertAfter('#editor-subcontainer') } /* Replacement actions and Toolbar addition */ function MISPElementReplacementActions(action) { var start = cm.getCursor('start') var end = cm.getCursor('end') var content = cm.getRange(start, end) var replacement = content var setCursorTo = false var noMatch = false switch (action) { case 'element': replacement = '@[MISPElement]()' end = null cm.replaceRange(replacement, start) cm.setSelection({line: start.line, ch: start.ch + 2}, {line: start.line, ch: start.ch + 2 + 11}) cm.focus() return true; case 'attribute': replacement = '@[attribute]()' end = null setCursorTo = {line: start.line, ch: start.ch + replacement.length - 1} break; case 'attribute-attachment': replacement = '@![attribute]()' end = null setCursorTo = {line: start.line, ch: start.ch + replacement.length - 1} break; case 'object': replacement = '@[object]()' end = null setCursorTo = {line: start.line, ch: start.ch + replacement.length - 1} break; case 'tag': replacement = '@[tag]()' end = null setCursorTo = {line: start.line, ch: start.ch + replacement.length - 1} break; case 'galaxy-matrix': replacement = '@[galaxymatrix]()' end = null setCursorTo = {line: start.line, ch: start.ch + replacement.length - 1} break; default: noMatch = true; break; } if (noMatch) { return false } cm.replaceRange(replacement, start, end) if (setCursorTo !== false) { cm.setCursor(setCursorTo.line, setCursorTo.ch) } cm.focus() return true } function insertMISPElementToolbarButtons() { insertTopToolbarSection() insertTopToolbarButton('cube', 'attribute', 'Attribute') insertTopToolbarButton('cubes', 'object', 'Object') insertTopToolbarButton('image', 'attribute-attachment', 'Attribute picture') insertTopToolbarButton('tag', 'tag', 'Tag') insertTopToolbarButton('atlas', 'galaxy-matrix', 'Galaxy matrix') } /* Hints */ var MISPElementHints = {} function buildMISPElementHints() { MISPElementHints['attribute'] = [] Object.keys(proxyMISPElements['attribute']).forEach(function(uuid) { var attribute = proxyMISPElements['attribute'][uuid] MISPElementHints['attribute'].push( [attribute.value, uuid], [attribute.type, uuid], [attribute.id, uuid], [attribute.uuid, uuid], ) }) MISPElementHints['object'] = [] Object.keys(proxyMISPElements['object']).forEach(function(uuid) { var object = proxyMISPElements['object'][uuid] var topPriorityValue = getTopPriorityValue(object) MISPElementHints['object'].push( [object.name, uuid], [object.id, uuid], [object.uuid, uuid], [topPriorityValue, uuid], ) }) MISPElementHints['galaxymatrix'] = [] Object.keys(proxyMISPElements['galaxymatrix']).forEach(function(uuid) { var galaxy = proxyMISPElements['galaxymatrix'][uuid] MISPElementHints['galaxymatrix'].push( [galaxy.id, uuid], [galaxy.uuid, uuid], [galaxy.name, uuid], [galaxy.namespace, uuid], [galaxy.type, uuid], ) }) MISPElementHints['tag'] = [] Object.keys(proxyMISPElements['tagname']).forEach(function(tagName) { var tag = proxyMISPElements['tagname'][tagName] MISPElementHints['tag'].push([tagName, tagName]) }) } function hintMISPElements(cm, options) { var authorizedMISPElements = ['attribute', 'object', 'galaxymatrix', 'tag'] var availableScopes = ['attribute', 'object', 'galaxymatrix', 'tag'] var reMISPElement = RegExp('@\\[(?' + authorizedMISPElements.join('|') + ')\\]\\((?[^\\)]+)?\\)'); var reMISPScope = RegExp('@\\[(?\\S+)\\]\\(\\)'); var reExtendedWord = /\S/ var hintList = [] var scope, elementID, element var cursor = cm.getCursor() var line = cm.getLine(cursor.line) var start = cursor.ch var end = cursor.ch while (start && reExtendedWord.test(line.charAt(start - 1))) --start while (end < line.length && reExtendedWord.test(line.charAt(end))) ++end var word = line.slice(start, end).toLowerCase() if (word === '@[]()') { availableScopes.forEach(function(scope) { hintList.push({ text: '@[' + scope + ']()' }) }); return { list: hintList, from: CodeMirror.Pos(cursor.line, start), to: CodeMirror.Pos(cursor.line, end) } } var resScope = reMISPScope.exec(word) if (resScope !== null) { var partialScope = resScope.groups.scope availableScopes.forEach(function(scope) { if (scope.startsWith(partialScope) && scope !== partialScope) { hintList.push({ text: '@[' + scope + ']()' }) } }); if (hintList.length > 0) { return { list: hintList, from: CodeMirror.Pos(cursor.line, start), to: CodeMirror.Pos(cursor.line, end) } } } var res = reMISPElement.exec(word) if (res !== null) { scope = res.groups.scope elementID = res.groups.elementid !== undefined ? res.groups.elementid : '' if (scope === 'tag') { element = proxyMISPElements['tagname'][elementID] } else { element = proxyMISPElements[scope][elementID] } if (element !== undefined) { hintList.push( { text: '@[' + scope + '](' + element.uuid + ')', render: function(elem, self, data) { var hintElement = renderHintElement(scope, element) $(elem).append(hintElement) }, className: 'hint-container', } ) } else { // search in hint arrays var addedItems = {} var maxHints = 20 + 10*(elementID.length - 3 >= 0 ? elementID.length - 3 : 0); // adapt hint numbers if typed value is large enough if (MISPElementHints[scope] !== undefined) { for (var i = 0; i < MISPElementHints[scope].length; i++) { var hintArray = MISPElementHints[scope][i]; var hintValue = hintArray[0] var hintUUID = hintArray[1] if (hintList.length >= maxHints) { break } if (hintValue.includes(elementID)) { if (addedItems[hintUUID] === undefined) { if (scope === 'tag') { element = proxyMISPElements['tagname'][hintUUID] element.uuid = hintUUID } else { element = proxyMISPElements[scope][hintUUID] } if (element !== undefined) { hintList.push({ text: '@[' + scope + '](' + element.uuid + ')', element: element, render: function(elem, self, data) { var hintElement = renderHintElement(scope, data.element) $(elem).append(hintElement) }, className: 'hint-container', }) } addedItems[hintUUID] = true } } } } } return { list: hintList, from: CodeMirror.Pos(cursor.line, start), to: CodeMirror.Pos(cursor.line, end) } } return null } function renderHintElement(scope, element) { var $node; if (scope == 'attribute') { $node = $('').addClass('hint-attribute') if (isValidObjectAttribute(element)) { $node.append($('').addClass('fas fa-cubes').css('margin-right', '3px')) } $node.append($('').addClass('').text('[' + element.category + '] ')) .append($('').addClass('bold').text(element.type + ' ')) .append( $('').addClass('bold blue ellipsis-overflow') .css({ 'max-width': '500px', 'display': 'table-cell' }) .text(element.value) ) } else if (scope == 'object') { var topPriorityValue = getTopPriorityValue(element) $node = $('').addClass('hint-object') $node.append($('').addClass('').text('[' + element['meta-category'] + '] ')) .append($('').addClass('bold').text(element.name + ' ')) .append( $('').addClass('bold blue ellipsis-overflow') .css({ 'max-width': '500px', 'display': 'table-cell' }) .text(topPriorityValue) ) } else if (scope == 'galaxymatrix') { $node = $('').addClass('hint-galaxymatrix') $node.append($('').addClass('').text('[' + element.namespace + '] ')) .append($('').addClass('bold').text(element.type + ' ')) .append($('').addClass('bold blue').text(element.name)) } else if (scope == 'tag') { $node = $('').addClass('hint-tag') $node.append(constructTagHtml(element.name, element.colour, {'box-shadow': 'none'})) } else { $node = $('No match') // should not happen } return $node } /** __ __ _ _ _____ _ | \/ | | | | | |_ _| | | \ / | __ _ _ __| | ____| | _____ ___ __ | | | |_ | |\/| |/ _` | '__| |/ / _` |/ _ \ \ /\ / / '_ \ | | | __| | | | | (_| | | | < (_| | (_) \ V V /| | | |_| |_| |_ |_| |_|\__,_|_| |_|\_\__,_|\___/ \_/\_/ |_| |_|_____|\__| */ function markdownItCustomPostInit() { markdownItSetupRules() fetchProxyMISPElements(function() { doRender() }) } function markdownItSetupRules() { md.renderer.rules.MISPElement = MISPElementRenderer; md.renderer.rules.MISPPictureElement = MISPPictureElementRenderer; md.inline.ruler.push('MISP_element_rule', MISPElementRule); md.core.ruler.push('MISP_element_suggestion_rule', MISPElementSuggestionRule); } function MISPElementSuggestionRule(state) { var blockTokens = state.tokens var tokens, blockToken, currentToken var indexOfAllLines, lineOffset, absoluteLine, relativeIndex var tokenMap var i, j, l for (i = 0, l = blockTokens.length; i < l; i++) { blockToken = blockTokens[i] if (blockToken.type !== 'inline') { continue } tokens = blockToken.children; for (j = 0; j < tokens.length; j++) { currentToken = tokens[j]; if (currentToken.type !== 'MISPElement' && !currentToken.isSuggestion) { continue } if (blockToken.indexOfAllLines === undefined) { indexOfAllLines = new md.block.State(blockToken.content, md, {}, []) blockToken.indexOfAllLines = indexOfAllLines } lineOffset = getLineNumInArrayList(currentToken.content.indexes.start, blockToken.indexOfAllLines.bMarks) tokenMap = findBackClosestStartLine(blockTokens, i) var absoluteLine = tokenMap[0] + lineOffset var relativeIndex = currentToken.content.indexes.start - blockToken.indexOfAllLines.bMarks[lineOffset] state.tokens[i].children[j].content.indexes.lineStart = absoluteLine state.tokens[i].children[j].content.indexes.start = relativeIndex } } } /* Parsing Rules */ function MISPElementRule(state, startLine, endLine, silent) { var pos, start, labelStart, labelEnd, res, elementID, code, content, token, tokens, attrs, scope var oldPos = state.pos, max = state.posMax if (state.src.charCodeAt(state.pos) !== 0x40/* @ */) { return false; } if (state.src.charCodeAt(state.pos + 1) === 0x21/* ! */) { if (state.src.charCodeAt(state.pos + 2) !== 0x5B/* [ */) { return false;} } else { if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false; } } var isPicture = state.src.charCodeAt(state.pos + 1) === 0x21/* ! */ if (isPicture) { labelStart = state.pos + 3; labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 2, false); } else { labelStart = state.pos + 2; labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false); } // parser failed to find ']', so it's not a valid link if (labelEnd < 0) { return false; } scope = state.src.slice(labelStart, labelEnd) pos = labelEnd + 1; if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { start = pos; if (scope == 'tag' || scope == 'suggestion') { // tags may contain spaces res = parseDestinationValue(state.src, pos, state.posMax); } else { res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax); } if (res.ok) { // parseLinkDestination does not support trailing characters such as `.` after the link // so we have to find the matching `)` var destinationEnd = res.str.length - 1 var traillingCharNumber = 0 for (var i = res.str.length-1; i > 1; i--) { var code = res.str.charCodeAt(i) if (code === 0x29 /* ) */) { destinationEnd = i break } traillingCharNumber++ } elementID = res.str.substring(1, destinationEnd); pos = res.pos - 1 - traillingCharNumber; } } if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { state.pos = oldPos; return false; } pos++; if (scope == 'tag' || scope == 'suggestion') { var reTagName = /^[^\n)]+$/ if (!reTagName.test(elementID)) { return false; } } else { var reUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ if (!reUUID.test(elementID)) { return false; } } // We found the end of the link, and know for a fact it's a valid link; // so all that's left to do is to call tokenizer. content = { scope: scope, elementID: elementID, indexes: { start: oldPos, } } if (isPicture) { token = state.push('MISPPictureElement', 'div', 0); } else { token = state.push('MISPElement', 'div', 0); if (scope == 'suggestion') { token.isSuggestion = true content.indexes.suggestionID = consumeSuggestionID() } else { token.isSuggestion = false } } token.children = tokens; token.content = content; state.pos = pos; state.posMax = max; return true; } /* Rendering rules */ function MISPElementRenderer(tokens, idx, options, env, slf) { var allowedScope = ['attribute', 'object', 'galaxymatrix', 'tag', 'suggestion'] var token = tokens[idx]; var scope = token.content.scope var elementID = token.content.elementID var indexes = token.content.indexes if (allowedScope.indexOf(scope) == -1) { return renderInvalidMISPElement(scope, elementID); } return renderMISPElement(scope, elementID, indexes) } function MISPPictureElementRenderer(tokens, idx, options, env, slf) { var allowedScope = ['attribute'] var token = tokens[idx]; var scope = token.content.scope var elementID = token.content.elementID if (allowedScope.indexOf(scope) == -1) { return renderInvalidMISPElement(scope, elementID); } return renderMISPPictureElement(scope, elementID) } function renderMISPElement(scope, elementID, indexes) { var templateVariables if (scope == 'suggestion') { var suggestionKey = 'suggestion-' + String(indexes.suggestionID) if (suggestions[elementID] !== undefined) { var suggestion = suggestions[elementID][suggestionKey] if (suggestion !== undefined) { templateVariables = sanitizeObject({ scope: 'suggestion', elementid: elementID, eventid: eventid, type: suggestion.complexTypeToolResult.picked_type, origValue: elementID, value: suggestion.complexTypeToolResult.value, indexStart: indexes.start, suggestionkey: suggestionKey, checked: suggestion.checked }) return renderTemplateBasedOnRenderingOptions(scope, dotTemplateSuggestionAttribute, templateVariables); } } } if (proxyMISPElements !== null) { if (scope == 'attribute') { var attribute = proxyMISPElements[scope][elementID] if (attribute !== undefined) { var templateToRender = dotTemplateAttribute var attributeData = { scope: 'attribute', elementid: elementID, type: attribute.type, value: attribute.value } if (isValidObjectAttribute(attribute)) { var mispObject = getObjectFromAttribute(attribute) if (mispObject !== undefined) { attributeData.type = attribute.object_relation attributeData.objectname = mispObject.name templateToRender = dotTemplateObjectAttribute } } templateVariables = sanitizeObject(attributeData) return renderTemplateBasedOnRenderingOptions(scope, templateToRender, templateVariables); } } else if (scope == 'object') { var mispObject = proxyMISPElements[scope][elementID] if (mispObject !== undefined) { var associatedTemplate = mispObject.template_uuid + '.' + mispObject.template_version var objectTemplate = proxyMISPElements['objectTemplates'][associatedTemplate] var topPriorityValue = mispObject.Attribute.length if (objectTemplate !== undefined) { var temp = getPriorityValue(mispObject, objectTemplate) topPriorityValue = temp !== false ? temp : topPriorityValue } templateVariables = sanitizeObject({ scope: 'object', elementid: elementID, type: mispObject.name, value: topPriorityValue }) return renderTemplateBasedOnRenderingOptions(scope, dotTemplateObject, templateVariables); } } else if (scope == 'tag') { templateVariables = sanitizeObject({scope: 'tag', elementid: elementID, eventid: eventid, value: elementID}) return renderTemplateBasedOnRenderingOptions(scope, dotTemplateTag, templateVariables); } else if (scope == 'galaxymatrix') { templateVariables = sanitizeObject({scope: 'galaxymatrix', elementid: elementID, eventid: eventid, value: elementID}) return renderTemplateBasedOnRenderingOptions(scope, dotTemplateGalaxyMatrix, templateVariables); } } return renderInvalidMISPElement(scope, elementID) } function renderMISPPictureElement(scope, elementID) { if (proxyMISPElements !== null) { var attribute = proxyMISPElements[scope][elementID] if (attribute !== undefined) { var templateVariables = sanitizeObject({ scope: 'attribute', elementid: elementID, type: attribute.type, value: attribute.value, alt: scope + ' ' + elementID, src: baseurl + '/attributes/viewPicture/' + attribute.id, title: attribute.type + ' ' + attribute.value, }) return renderTemplateBasedOnRenderingOptions('attribute-picture', dotTemplateAttributePicture, templateVariables); } } return renderInvalidMISPElement(scope, elementID) } function renderInvalidMISPElement(scope, elementID) { var templateVariables = sanitizeObject({ scope: invalidMessage, id: elementID }) return dotTemplateInvalid(templateVariables); } function renderTemplateBasedOnRenderingOptions(scope, templateToRender, templateVariables) { if (renderingRules[scope]) { return templateToRender(templateVariables) } else { return dotTemplateRenderingDisabled(templateVariables) } } function setupMISPElementMarkdownListeners() { var $elements = $('.misp-element-wrapper.attribute:not(".suggestion"), .misp-element-wrapper.object, .misp-picture-wrapper > img, .embeddedTag'); $elements.popover({ trigger: 'click', html: true, container: isInsideModal() ? 'body' : '#viewer-container', placement: 'top', title: getTitleFromMISPElementDOM, content: getContentFromMISPElementDOM }) setupSuggestionMarkdownListeners() } function setupSuggestionMarkdownListeners() { $('.misp-element-wrapper').filter('.suggestion').click(function(e) { var $checkbox = $(this).find('input[type="checkbox"]') $checkbox.prop('checked', !$checkbox.prop('checked')) updateSuggestionCheckedState($(this), $checkbox) }).find('input[type="checkbox"]') .click(function(e) { e.stopPropagation() }) .change(function() { updateSuggestionCheckedState($(this).closest('.suggestion'), $(this)) }) } function updateSuggestionCheckedState($wrapper, $checkbox) { var elementID = $wrapper.data('elementid') var suggestionKey = $wrapper.data('suggestionkey') suggestions[elementID][suggestionKey].checked = $checkbox.prop('checked') } function attachRemoteMISPElements() { $('.embeddedGalaxyMatrix[data-scope="galaxymatrix"]').each(function() { var $div = $(this) clearTimeout(galaxyMatrixTimer); $div.append($('
').css('font-size', '24px').append(loadingSpanAnimation)) var eventID = $div.data('eventid') var elementID = $div.data('elementid') var cacheKey = eventid + '-' + elementID if (cache_matrix[cacheKey] === undefined) { galaxyMatrixTimer = setTimeout(function() { attachGalaxyMatrix($div, eventID, elementID) }, firstCustomPostRenderCall ? 0 : slowDebounceDelay); } else { $div.html(cache_matrix[cacheKey]) } }) var tagNamesToLoad = []; var tagsLoading = []; $('.embeddedTag[data-scope="tag"]').each(function() { var $div = $(this); var elementID = $div.data('elementid'); if (!(elementID in cache_tag)) { $div.append($('').append(loadingSpanAnimation)); tagNamesToLoad.push(elementID); tagsLoading.push($div); } else { $div.html(cache_tag[elementID]); } }).promise().done(function() { if (tagNamesToLoad.length === 0) { return; } fetchTagInfo(tagNamesToLoad, function() { $.each(tagsLoading, function() { var $div = $(this); var elementID = $div.data('elementid'); if (elementID in cache_tag) { $div.html(cache_tag[elementID]); } }); }); }); if (firstCustomPostRenderCall) { // Wait, because .each calls are asynchronous setTimeout(function() { firstCustomPostRenderCall = false; }, 1000) } } function attachGalaxyMatrix($elem, eventid, elementID) { var galaxy = proxyMISPElements['galaxymatrix'][elementID] if (galaxy === undefined) { console.log('Something wrong happened. Could not fetch galaxy from proxy') return } var galaxyType = galaxy.type $.ajax({ data: { "returnFormat": "attack", "eventid": eventid, "attackGalaxy": galaxyType }, success:function(data, textStatus) { $elem.empty().append($(data)) $elem.find('#attackmatrix_div').css({ 'max-width': 'unset', 'min-width': 'unset', 'width': 'calc(100% - 5px)' }) $elem.find('#checkbox_attackMatrix_showAll').click() $elem.find('#attackmatrix_div .heatCell').each(function() { if ($(this).css('background-color').length > 0 && $(this).css('background-color') != 'rgba(0, 0, 0, 0)') { $(this).attr('style', 'background-color:' + $(this).css('background-color') + ' !important; color:' + $(this).css('color') + ' !important;'); } }) var cacheKey = eventid + '-' + elementID cache_matrix[cacheKey] = $elem.find('#attackmatrix_div')[0].outerHTML; }, error: function(jqXHR, textStatus, errorThrown) { var templateVariables = sanitizeObject({ scope: 'Error while fetching matrix', id: graphID }) var placeholder = dotTemplateInvalid(templateVariables) $elem.empty() .css({'text-align': 'center'}) .append($(placeholder)) }, type:"post", url: baseurl + "/events/restSearch" }) } function fetchTagInfo(tagNames, callback) { $.ajax({ data: { "tag": tagNames, }, success: function (data) { var $tag, tagName; data = $.parseJSON(data) for (var i = 0; i < data.length; i++) { var tag = data[i]; tagName = tag.Tag.name; proxyMISPElements['tag'][tagName] = tag; $tag = getTagReprensentation(tag); cache_tag[tagName] = $tag[0].outerHTML; } // If tag name doesn't exists, construct empty placeholder for (i = 0; i < tagNames.length; i++) { tagName = tagNames[i]; if (!(tagName in cache_tag)) { $tag = constructTagHtml(tagName, '#ffffff', {'border': '1px solid #000'}); cache_tag[tagName] = $tag[0].outerHTML; } } }, error: function (jqXHR, textStatus, errorThrown) { // Query failed, fill cache with placeholder var tagName, templateVariables; for (var i = 0; i < tagNames.length; i++) { tagName = tagNames[i]; if (!(tagName in cache_tag)) { templateVariables = sanitizeObject({ scope: 'Error while fetching tag', id: tagName }); cache_tag[tagName] = dotTemplateInvalid(templateVariables); } } }, complete: function () { if (callback !== undefined) { callback() } }, type: "post", url: baseurl + "/tags/search/0/1/0" }) } /** _____ _ / ____| (_) | (___ __ ___ ___ _ __ __ _ \___ \ / _` \ \ / / | '_ \ / _` | ____) | (_| |\ V /| | | | | (_| | |_____/ \__,_| \_/ |_|_| |_|\__, | __/ | |___/ */ function replaceMISPElementByTheirValue(raw) { var match, replacement, element var final = '' var authorizedMISPElements = ['attribute', 'object'] var reMISPElement = RegExp('@\\[(?' + authorizedMISPElements.join('|') + ')\\]\\((?[\\d]+)\\)', 'g'); var offset = 0 while ((match = reMISPElement.exec(raw)) !== null) { element = proxyMISPElements[match.groups.scope][match.groups.elementid] if (element !== undefined) { replacement = match.groups.scope + '-' + element.uuid } else { replacement = match.groups.scope + '-' + match.groups.elementid } final += raw.substring(offset, match.index) + replacement offset = reMISPElement.lastIndex } final += raw.substring(offset) return final } /** __ __ | \/ | | \ / | ___ _ __ _ _ | |\/| |/ _ \ '_ \| | | | | | | | __/ | | | |_| | |_| |_|\___|_| |_|\__,_| */ function injectCustomRulesMenu() { var $MISPElementMenuItem = createRulesMenuItem('MISP Elements', $(''), 'parser', 'MISP_element_rule') $markdownDropdownRulesMenu.append($MISPElementMenuItem) createSubMenu({ name: 'Markdown rendering rules', icon: 'fab fa-markdown', items: [ { name: 'Attribute', icon: 'fas fa-cube', ruleScope: 'render', ruleName: 'attribute', isToggleableRule: true }, { name: 'Attribute picture', icon: 'fas fa-image', ruleScope: 'render', ruleName: 'attribute-picture', isToggleableRule: true }, { name: 'Object', icon: 'fas fa-cubes', ruleScope: 'render', ruleName: 'object', isToggleableRule: true }, { name: 'Object Attribute', icon: 'fas fa-cube', ruleScope: 'render', ruleName: 'object-attribute', isToggleableRule: true }, { name: 'Tag', icon: 'fas fa-tag', ruleScope: 'render', ruleName: 'tag', isToggleableRule: true }, { name: 'Galaxy matrix', icon: 'fas fa-atlas', ruleScope: 'render', ruleName: 'galaxymatrix', isToggleableRule: true }, { name: 'Suggestion', icon: 'fas fa-magic', ruleScope: 'render', ruleName: 'suggestion', isToggleableRule: true }, ] }) createSubMenu({ name: 'Extract entities', icon: 'fas fa-cogs', items: [ { name: 'Manual extraction', icon: 'fas fa-highlighter', clickHandler: manualEntitiesExtraction}, { name: 'Automatic extraction', icon: 'fas fa-magic', clickHandler: automaticEntitiesExtraction}, ] }) reloadRenderingRuleEnabledUI() } function markdownItToggleCustomRule(rulename, event) { var enabled if (rulename == 'MISP_element_rule') { var rule = getRuleStatus('inline', 'ruler', 'MISP_element_rule') if (rule !== false) { enabled = rule.enabled } } return { found: enabled !== undefined, enabled: enabled } } function markdownItToggleRenderingRule(rulename, event) { if (event !== undefined) { event.stopPropagation() } if (renderingRules[rulename] === undefined) { console.log('Rule does not exist') return } renderingRules[rulename] = !renderingRules[rulename] doRender() reloadRenderingRuleEnabledUI() } function reloadRenderingRuleEnabledUI() { Object.keys(renderingRules).forEach(function(rulename) { var enabled = renderingRules[rulename] if (enabled) { $('#markdownrendering-' + rulename + '-rendering-enabled').show() $('#markdownrendering-' + rulename + '-rendering-disabled').hide() } else { $('#markdownrendering-' + rulename + '-rendering-enabled').hide() $('#markdownrendering-' + rulename + '-rendering-disabled').show() } }) } /** _____ _ _ / ____| | | (_) | (___ _ _ __ _ __ _ ___ ___| |_ _ ___ _ __ \___ \| | | |/ _` |/ _` |/ _ \/ __| __| |/ _ \| '_ \ ____) | |_| | (_| | (_| | __/\__ \ |_| | (_) | | | | |_____/ \__,_|\__, |\__, |\___||___/\__|_|\___/|_| |_| __/ | __/ | |___/ |___/ */ function automaticEntitiesExtraction() { var url = baseurl + '/eventReports/extractAllFromReport/' + reportid openGenericModal(url) } function manualEntitiesExtraction() { contentBeforeSuggestions = getEditorData() pickedSuggestion = { tableID: null, tr: null, entity: null, index: null, isContext: null } extractFromReport(function(data) { typeToCategoryMapping = data.typeToCategoryMapping prepareSuggestionInterface(data.complexTypeToolResult, data.replacementValues, data.replacementContext) toggleSuggestionInterface(true) }) } function prepareSuggestionInterface(complexTypeToolResult, replacementValues, replacementContext) { toggleMarkdownEditorLoading(true, 'Processing document') entitiesFromComplexTool = complexTypeToolResult searchForUnreferencedValues(replacementValues) searchForUnreferencedContext(replacementContext) entitiesFromComplexTool = injectNumberOfOccurrencesInReport(entitiesFromComplexTool) setupSuggestionMarkdownListeners() constructSuggestionTables(entitiesFromComplexTool) toggleMarkdownEditorLoading(false) } function highlightPickedSuggestionInReport() { setEditorData(contentBeforeSuggestions) resetSuggestionIDs() for (var i = 0; i < entitiesFromComplexTool.length; i++) { var entity = entitiesFromComplexTool[i]; if (pickedSuggestion.entity.value == entity.value) { var converted = convertEntityIntoSuggestion(contentBeforeSuggestions, entity) setEditorData(converted) var indicesInCM = getAllSuggestionIndicesOf(converted, entity.value, false) constructSuggestionMapping(entity, indicesInCM) break } } } function highlightPickedReplacementInReport() { var entity = pickedSuggestion.entity setEditorData(contentBeforeSuggestions) var content = contentBeforeSuggestions resetSuggestionIDs() var converted = convertEntityIntoSuggestion(content, entity) setEditorData(converted) var indicesInCM = getAllSuggestionIndicesOf(converted, entity.value, false) constructSuggestionMapping(entity, indicesInCM) } function convertEntityIntoSuggestion(content, entity) { var converted = '' var entityValue; if (entity.importRegexMatch) { entityValue = entity.importRegexMatch; } else if (entity.original_value) { entityValue = entity.original_value; } else { entityValue = entity.value; } var splittedContent = content.split(entityValue) splittedContent.forEach(function(text, i) { converted += text if (i < splittedContent.length-1) { if (isDoubleExtraction(converted)) { converted += entity.value } else { converted += '@[suggestion](' + entity.value + ')' } } }) return converted } function extractFromReport(callback) { $.ajax({ dataType: "json", beforeSend: function() { toggleMarkdownEditorLoading(true, 'Extracting entities') }, success:function(data, textStatus) { callback(data); }, error: function(jqXHR, textStatus, errorThrown) { showMessage('fail', 'Could not extract entities from report. ' + textStatus) }, complete: function() { toggleMarkdownEditorLoading(false) }, type:'get', url: baseurl + '/eventReports/extractFromReport/' + reportid }) } function constructSuggestionMapping(entity, indicesInCM) { var suggestionBaseKey = 'suggestion-', suggestionKey suggestions[entity.value] = {} indicesInCM.forEach(function(index) { suggestionKey = suggestionBaseKey + getNewSuggestionID() suggestions[entity.value][suggestionKey] = { startIndex: index, endIndex: {index: index.index + entity.value.length}, complexTypeToolResult: entity, checked: true } }); setTimeout(function() { var notRenderedCount = suggestionIDs.length if (notRenderedCount > 0) { pickedSuggestion.tr.find('.occurrence-issues') .attr('title', 'Could not render ' + notRenderedCount + ' elements. Manual investigation required') .text('⚠ ' + notRenderedCount) } }, 300); } function injectNumberOfOccurrencesInReport(entities) { var content = getEditorData() entities.forEach(function(entity, i) { entities[i].occurrences = getAllIndicesOf(content, entity.original_value, false, false).length }) return entities } function getAllSuggestionIndicesOf(content, entity, caseSensitive) { var toSearch = '@[suggestion](' + entity + ')' return getAllIndicesOf(content, toSearch, caseSensitive, true) } function toggleSuggestionInterface(enabled) { if (enabled) { setCMReadOnly(true) setMode('splitscreen') $('#editor-subcontainer').hide() $suggestionContainer.show() } else { setCMReadOnly(false) setEditorData(originalRaw) $('#editor-subcontainer').show() $suggestionContainer.hide() $mardownViewerToolbar.find('.btn-group:first button').css('visibility', 'visible') $('#suggestionCloseButton').remove() cm.refresh() } } function searchForUnreferencedValues(replacementValues) { unreferencedElements.values = {} var content = getEditorData() Object.keys(replacementValues).forEach(function(attributeValue) { var replacementValue = replacementValues[attributeValue] var indices = getAllIndicesOf(content, replacementValue.valueInReport, true, true) if (indices.length > 0) { var attributes = []; Object.keys(proxyMISPElements['attribute']).forEach(function(uuid) { if (replacementValue.attributeUUIDs.indexOf(uuid) > -1) { attributes.push(proxyMISPElements['attribute'][uuid]) } }); unreferencedElements.values[replacementValue.valueInReport] = { attributes: attributes, indices: indices } if (attributeValue != replacementValue.valueInReport) { unreferencedElements.values[replacementValue.valueInReport].importRegexMatch = attributeValue } } }) } function searchForUnreferencedContext(replacementContext) { unreferencedElements.context = {} var content = getEditorData() Object.keys(replacementContext).forEach(function(rawText) { var indices = getAllIndicesOf(content, rawText, true, true) if (indices.length > 0) { replacementContext[rawText].indices = indices } }) unreferencedElements.context = replacementContext; } function pickSuggestionColumn(index, tableID, force) { tableID = tableID === undefined ? 'replacementTable' : tableID force = force === undefined ? false : force; if (pickedSuggestion.tableID != tableID || pickedSuggestion.index != index || force) { if (pickedSuggestion.tr) { pickedSuggestion.tr.find('.occurrence-issues').attr('title', '').text('') } var $trs = $('#' + tableID + ' tr') $trs.removeClass('info').find('button').prop('disabled', true) $trs.find('select').prop('disabled', true) var $tr = $('#' + tableID + ' tr[data-entityindex="' + index + '"]') if ($tr.length > 0) { $tr.addClass('info').find('button').prop('disabled', false) $tr.find('select').prop('disabled', false) pickedSuggestion = { tableID: tableID, tr: $tr, index: index } if (tableID === 'replacementTable') { var uuid = $tr.find('select.attribute-replacement').val() pickedSuggestion['entity'] = { value: $tr.data('attributeValue'), picked_type: proxyMISPElements['attribute'][uuid].type, replacement: uuid } if (proxyMISPElements['attribute'][uuid].importRegexValue) { pickedSuggestion['entity']['importRegexMatch'] = proxyMISPElements['attribute'][uuid].importRegexValue } highlightPickedReplacementInReport() } else if (tableID === 'contextReplacementTable') { pickedSuggestion['entity'] = { value: $tr.data('contextValue'), picked_type: 'tag', replacement: $tr.find('select.context-replacement').val() } pickedSuggestion['isContext'] = true highlightPickedReplacementInReport() } else { pickedSuggestion['entity'] = $tr.data('entity') pickedSuggestion['entity']['picked_type'] = $tr.find('select.type').val() highlightPickedSuggestionInReport() } } } } function getContentWithCheckedElements(isReplacement) { var value = pickedSuggestion.entity.value var content = getEditorData() var contentWithPickedSuggestions = '' var nextIndex = 0 var suggestionLength = '@[suggestion]()'.length + pickedSuggestion.entity.value.length Object.keys(suggestions[value]).forEach(function(suggestionKey, i) { var suggestion = suggestions[value][suggestionKey] contentWithPickedSuggestions += content.substr(nextIndex, suggestion.startIndex.index - nextIndex) nextIndex = suggestion.startIndex.index var renderedInMardown = $('.misp-element-wrapper.suggestion[data-suggestionkey="' + suggestionKey + '"]').length > 0; if (suggestion.checked && renderedInMardown) { // If the suggestion is not rendered, ignore it (could happen if parent block is escaped) if (isReplacement) { if (pickedSuggestion.isContext === true) { contentWithPickedSuggestions += '@[tag](' + suggestion.complexTypeToolResult.replacement + ')' } else { contentWithPickedSuggestions += '@[attribute](' + suggestion.complexTypeToolResult.replacement + ')' } } else { contentWithPickedSuggestions += content.substr(nextIndex, suggestionLength) } } else { contentWithPickedSuggestions += value } nextIndex += suggestionLength }) contentWithPickedSuggestions += content.substr(nextIndex) return contentWithPickedSuggestions } function getSuggestionMapping() { var getSuggestionMapping = {} var $select = pickedSuggestion.tr.find('select') var entity = pickedSuggestion.entity getSuggestionMapping[entity.value] = { 'type': $select.filter('.type').val(), 'category': $select.filter('.category').val(), 'to_ids': entity.to_ids } return getSuggestionMapping } function submitReplacement() { var contentWithPickedReplacements = getContentWithCheckedElements(true) setEditorData(contentWithPickedReplacements); saveMarkdown(false, function() { setEditorData(originalRaw) manualEntitiesExtraction() }) } function submitExtractionSuggestion() { var url = baseurl + '/eventReports/replaceSuggestionInReport/' + reportid var contentWithPickedSuggestions = getContentWithCheckedElements(false) var suggestionsMapping = getSuggestionMapping() fetchFormDataAjax(url, function(formHTML) { $('body').append($('