| /* eslint-disable */ |
| /* HELPFUL functions to call */ |
| |
| function restRequest(requestType, data, callback = (r) => { |
| console.log('Fetch Success', r); |
| }, endpoint = '/api/rest') { |
| const requestData = requestType === 'GET' ? |
| {method: requestType, headers: {'Content-Type': 'application/json'}} : |
| {method: requestType, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)} |
| |
| fetch(endpoint, requestData) |
| .then((response) => { |
| if (!response.ok) { |
| throw (response.statusText); |
| } |
| return response.text(); |
| }) |
| .then((text) => { |
| try { |
| callback(JSON.parse(text)); |
| } catch { |
| callback(text); |
| } |
| }) |
| .catch((error) => console.error(error)); |
| } |
| |
| function apiV2(requestType, endpoint, body = null, jsonRequest = true) { |
| let requestBody = { method: requestType }; |
| if (jsonRequest) { |
| requestBody.headers = { 'Content-Type': 'application/json' }; |
| if (body) { |
| requestBody.body = JSON.stringify(body); |
| } |
| } else { |
| if (body) { |
| requestBody.body = body; |
| } |
| } |
| |
| return new Promise((resolve, reject) => { |
| fetch(endpoint, requestBody) |
| .then((response) => { |
| if (!response.ok) { |
| reject(response.statusText); |
| } |
| return response.text(); |
| }) |
| .then((text) => { |
| try { |
| resolve(JSON.parse(text)); |
| } catch { |
| resolve(text); |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * Given list of abilities, returns list filtered by the following |
| * @param abilities {object[]} - List of abilities to be filtered |
| * @param searchTerm {string} - Search query keyword |
| * @param tactic {string} - Tactic name |
| * @param technique {object} - Technique ID and name |
| * @param platforms {string[]} - List of ability platforms |
| * @param plugins {string} = Ability plugin |
| * @param executors {string[]} - List of ability executors |
| * @returns {object[]} - List of filtered abilities |
| */ |
| function getFilteredAbilities(abilities, searchTerm, tactic, technique, platforms = null, plugins = '', executors = null) { |
| return abilities.filter((ability, index) => { |
| let matchesQuery = ( |
| ability.name.toLowerCase().includes(searchTerm) || |
| ability.description.toLowerCase().includes(searchTerm) || |
| ability.tactic.toLowerCase().includes(searchTerm) || |
| ability.technique_id.toLowerCase().includes(searchTerm) || |
| ability.technique_name.toLowerCase().includes(searchTerm) |
| ); |
| let matchesTactic = (!tactic || ability.tactic === tactic); |
| let matchesTechnique = (!technique || `${ability.technique_id} | ${ability.technique_name}` === technique); |
| let abilityPlatforms = ability.executors.map((exec) => exec.platform); |
| let matchesPlatform = (!platforms) || abilityPlatforms.some((platform) => platforms.includes(platform)); |
| if (executors) matchesPlatform = matchesPlatform || ability.executors.some((exec) => executors.includes(exec.name)); |
| let matchesPlugin = (!plugins || !plugins.includes(ability.plugin)); |
| |
| return matchesQuery && matchesTactic && matchesTechnique && matchesPlatform && matchesPlugin; |
| }); |
| } |
| |
| /** |
| * Parse timestamp into human-friendly date format |
| * Modified from original code: https://stackoverflow.com/questions/7641791/javascript-library-for-human-friendly-relative-date-formatting |
| * @param date {string} - i.e. '2021-08-03 19:37:08' |
| * @returns {string} (i.e.) '5 hrs ago'; |
| * (i.e. if older than 1 day): 'yesterday 10:03:23'; |
| * (i.e. if older than 2 days): 'Aug 25 10:03:23' |
| */ |
| function getHumanFriendlyTime(date) { |
| if (!date) return ''; |
| let split = date.split('-'); |
| |
| let hMonth = Number(split[1]) - 1; |
| let hDate = Number(split[2].split(' ')[0]); |
| let hTime = split[2].split(' ')[1].split(':'); |
| |
| const givenDate = Date.UTC(split[0], hMonth, hDate, hTime[0], hTime[1], hTime[2]); |
| // Make a fuzzy time |
| let delta = Math.round((Date.now() - givenDate) / 1000); |
| |
| let minute = 60, |
| hour = minute * 60, |
| day = hour * 24; |
| |
| let fuzzy; |
| |
| if (delta < 30) { |
| fuzzy = 'just now'; |
| } else if (delta < minute) { |
| fuzzy = delta + ' seconds ago'; |
| } else if (delta < 2 * minute) { |
| fuzzy = 'a minute ago' |
| } else if (delta < hour) { |
| fuzzy = Math.floor(delta / minute) + ' min ago'; |
| } else if (Math.floor(delta / hour) === 1) { |
| fuzzy = '1 hr ago' |
| } else if (delta < day) { |
| fuzzy = Math.floor(delta / hour) + ' hrs ago'; |
| } else if (delta < day * 2) { |
| fuzzy = 'yesterday ' + (hTime.join(':')); |
| } else { |
| switch (hMonth) { |
| case 0: |
| hMonth = 'Jan'; |
| break; |
| case 1: |
| hMonth = 'Feb'; |
| break; |
| case 2: |
| hMonth = 'Mar'; |
| break; |
| case 3: |
| hMonth = 'Apr'; |
| break; |
| case 4: |
| hMonth = 'May'; |
| break; |
| case 5: |
| hMonth = 'Jun'; |
| break; |
| case 6: |
| hMonth = 'Jul'; |
| break; |
| case 7: |
| hMonth = 'Aug'; |
| break; |
| case 8: |
| hMonth = 'Sep'; |
| break; |
| case 9: |
| hMonth = 'Oct'; |
| break; |
| case 10: |
| hMonth = 'Nov'; |
| break; |
| case 11: |
| hMonth = 'Dec'; |
| break; |
| default: |
| hMonth = ''; |
| break; |
| } |
| fuzzy = hMonth + ' ' + hDate + ' ' + hTime.join(':'); |
| } |
| return fuzzy; |
| } |
| |
| /** |
| * Parse timestamp into human-friendly date ISO8601-safe format |
| * @param dateTime {string} - Expected ISO8601 input in UTC time: (i.e.) 2021-08-25T10:03:23Z |
| * @returns {string} - (i.e.) '5 hrs ago' |
| */ |
| function getHumanFriendlyTimeISO8601(dateTime) { |
| return getHumanFriendlyTime(dateTime.replace('T', ' ').replace('Z', '')); |
| } |
| |
| function sortAlphabetically(list) { |
| return list.sort((a, b) => { |
| let x = a.toLowerCase(), y = b.toLowerCase(); |
| if (x < y) return -1; |
| else if (x > y) return 1; |
| else return 0; |
| }) |
| } |
| |
| function sanitize(unsafeMsg) { |
| const parser = new DOMParser(); |
| let doc = parser.parseFromString(unsafeMsg, 'text/html'); |
| return doc.body.innerText; |
| } |
| |
| function toast(message, success) { |
| bulmaToast.toast({ |
| message: `<span class="icon"><i class="fas fa-${success ? 'check' : 'exclamation'}"></i></span> ${sanitize(message)}`, |
| type: `toast ${success ? 'is-success' : 'is-danger'}`, |
| position: 'bottom-right', |
| duration: '3000', |
| pauseOnHover: true |
| }); |
| } |
| |
| function validateInputs(obj, requiredFields) { |
| let fieldErrors = []; |
| requiredFields.forEach((field) => { |
| if (obj[field].length === 0) { |
| fieldErrors.push(field); |
| } |
| }); |
| |
| return fieldErrors; |
| } |
| |
| function downloadJson(filename, data) { |
| let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2)); |
| let downloadAnchorNode = document.createElement('a'); |
| downloadAnchorNode.setAttribute("href", dataStr); |
| downloadAnchorNode.setAttribute("download", filename + ".json"); |
| document.body.appendChild(downloadAnchorNode); |
| downloadAnchorNode.click(); |
| downloadAnchorNode.remove(); |
| } |
| |
| function downloadReport(endpoint, filename, data = {}, jsonifyData = false) { |
| function downloadObjectAsJson(data) { |
| stream('Downloading report: ' + filename); |
| const parsedData = jsonifyData ? JSON.stringify(data, null, 2) : data; |
| let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(parsedData); |
| let downloadAnchorNode = document.createElement('a'); |
| downloadAnchorNode.setAttribute("href", dataStr); |
| downloadAnchorNode.setAttribute("download", filename + ".json"); |
| document.body.appendChild(downloadAnchorNode); |
| downloadAnchorNode.click(); |
| downloadAnchorNode.remove(); |
| } |
| |
| restRequest('POST', data, downloadObjectAsJson, endpoint); |
| } |
| |
| function sleep(ms) { |
| return new Promise((resolve) => setTimeout(resolve, ms)); |
| } |
| |
| function uuidv4() { |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { |
| let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); |
| return v.toString(16); |
| }); |
| } |
| |
| /* SECTIONS */ |
| |
| // Alternative to JQuery parseHTML(keepScripts=true) |
| function setInnerHTML(elem, html) { |
| elem.innerHTML = html; |
| const scripts = Array.from(elem.querySelectorAll("script")); |
| if (scripts) { |
| scripts.forEach(oldScript => { |
| const newScript = document.createElement("script"); |
| Array.from(oldScript.attributes) |
| .forEach( attr => newScript.setAttribute(attr.name, attr.value) ); |
| newScript.appendChild(document.createTextNode(oldScript.innerHTML)); |
| oldScript.parentNode.replaceChild(newScript, oldScript); |
| }); |
| } |
| } |
| |
| |
| // TODO: remove this from all individual plugins in future, as close (x) will be in the tab rather than inside the plugins itself |
| function removeSection(identifier) { |
| $('#' + identifier).remove(); |
| } |
| |
| function b64DecodeUnicode(str) { //https://stackoverflow.com/a/30106551 |
| if (str != null) { |
| // An error check is needed in case the wrong codec (i.e. not UTF-8) was used at source |
| try { |
| return decodeURIComponent(atob(str).split('').map(function (c) { |
| return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); |
| }).join('')); |
| } catch { |
| return atob(str); |
| } |
| } else return ""; |
| } |
| |