Untitled
unknown
json
a month ago
18 kB
1
Indexable
Never
// Constants are wrapped in a function to prevent redeclarations when tab is closed and openned again function global() { return { FACT_DISPLAY_LIMIT: 15, OP_GRAPH_HEIGHT: 400, FACT_GRAPH_WIDTH: 800, FACT_GRAPH_HEIGHT: 600, LINK_LENGTHS: { agent_contact: 100, next_link: 50, has_agent: 50, relationship: 100 }, NODE_CHARGES: { c2: -200, operation: -100, agent: -200, link: -150, fact: -50, tactic: -200, technique_name: -200 }, GRAPH_IMAGE_URLS: { server: 'debrief/img/cloud.svg', operation: 'debrief/img/operation.svg', link: 'debrief/img/link.svg', fact: 'debrief/img/star.svg', darwin: 'debrief/img/darwin.svg', windows: 'debrief/img/windows.svg', linux: 'debrief/img/linux.svg', tactic: 'debrief/img/tactic.svg', technique_name: 'debrief/img/technique.svg', collection: 'debrief/img/collection.svg', 'credential-access': 'debrief/img/credaccess.svg', 'defense-evasion': 'debrief/img/defevasion.svg', discovery: 'debrief/img/discovery.svg', execution: 'debrief/img/execution.svg', exfiltration: 'debrief/img/exfil.svg', impact: 'debrief/img/impact.svg', 'lateral-movement': 'debrief/img/latmove.svg', persistence: 'debrief/img/persistence.svg', 'privilege-escalation': 'debrief/img/privesc.svg', 'initial-access': 'debrief/img/access.svg', 'command-and-control': 'debrief/img/commandcontrol.svg', unknown: 'debrief/img/unknown.svg' } } } function init() { getImages(); } function getImages() { for (let key in global().GRAPH_IMAGE_URLS) { fetch(global().GRAPH_IMAGE_URLS[key]).then((data) => { return data.text(); }).then((svg) => { let parser = new DOMParser(); let doc = parser.parseFromString(svg, 'text/html'); let child = doc.body.firstChild; child.id = key + '-img'; child.classList.add('svg-icon'); document.getElementById('images').append(child); }); } } function statusName(status) { if (status === 0) { return 'success'; } else if (status === -2) { return 'discarded'; } else if (status === 1) { return 'failure'; } else if (status === 124) { return 'timeout'; } else if (status === -3) { // && chain.collect) { return 'collected'; } else if (status === -4) { return 'untrusted'; } else if (status === -5) { return 'visibility'; } return 'queued'; } function createForceSimulation(type) { let opGraphWidth = document.getElementById('debrief-graph').offsetWidth; let width = (type === 'operation') ? opGraphWidth : global().FACT_GRAPH_WIDTH; let height = (type === 'operation') ? global().OP_GRAPH_HEIGHT : global().FACT_GRAPH_HEIGHT; return d3.forceSimulation() .force('link', d3.forceLink().id((d) => d.id )) .force('charge', d3.forceManyBody() .strength((d) => global().NODE_CHARGES[d.type] || -200) .theta(0.8) .distanceMax(100)) .force('center', d3.forceCenter((width - 200) / 2, height / 2)) .force('collision', d3.forceCollide().radius(40)); } function updateReportGraph(operations) { Array.from(document.getElementsByClassName('debrief-svg')).forEach((svg) => svg.innerHTML = ''); d3.selectAll('.debrief-svg > *').remove(); graphs = [ { id: '#debrief-steps-svg', type: 'steps', tooltip: d3.select('#op-tooltip'), simulation: createForceSimulation('operation'), svg: d3.select('#debrief-steps-svg') }, { id: '#debrief-attackpath-svg', type: 'attackpath', tooltip: d3.select('#op-tooltip'), simulation: createForceSimulation('operation'), svg: d3.select('#debrief-attackpath-svg') }, { id: '#debrief-tactic-svg', type: 'tactic', tooltip: d3.select('#op-tooltip'), simulation: createForceSimulation('operation'), svg: d3.select('#debrief-tactic-svg') }, { id: '#debrief-technique-svg', type: 'technique', tooltip: d3.select('#op-tooltip'), simulation: createForceSimulation('operation'), svg: d3.select('#debrief-technique-svg') }, { id: '#debrief-fact-svg', type: 'fact', tooltip: d3.select('#fact-tooltip'), simulation: createForceSimulation('fact'), svg: d3.select('#debrief-fact-svg') } ] graphs.forEach((graph) => { apiV2('GET', `/plugin/debrief/graph?type=${graph.type}&operations=${operations.join()}`).then((graphData) => { graph.nodes = graphData.nodes; graph.links = graphData.links; writeGraph(graph); if (graph.type === 'fact') { limitFactsDisplayed(operations); } graph.simulation.alpha(1).restart(); graph.svg.call(d3.zoom().scaleExtent([0.5, 5]).on('zoom', () => { d3.select(graph.id + ' .graphContainer') .attr('transform', 'translate(' + d3.event.transform.x + ',' + d3.event.transform.y + ')scale(' + d3.event.transform.k + ')'); })); }).catch((error) => { console.error('Fetch', error); }); }); } function writeGraph(graph) { graph.svg.append('defs').append('marker') .attr('id', `arrowhead${graph.type}`) .attr('viewBox', '-0 -5 10 10') .attr('refX', 30) .attr('refY', 0) .attr('orient', 'auto') .attr('markerWidth', 8) .attr('markerHeight', 8) .attr('xoverflow', 'visible') .append('svg:path') .attr('d', 'M 0,-5 L 10 ,0 L 0,5') .attr('fill', '#999') .style('stroke','none'); let container = graph.svg.append('g') .attr('class', 'container') .attr('width', '100%') .attr('height', '100%') let graphContainer = container.append('g') .attr('class', 'graphContainer') let arrows = graphContainer.append('g') .style('stroke', '#aaa') .style('fill', '#aaa') .selectAll('polyline') .data(graph.links) .enter().append('polyline') .attr('data-source', (d) => d.source) .attr('data-target', (d) => d.target) .attr('class', (d) => d.type) .attr('stroke-linecap', 'round'); container.selectAll('g.nodes').remove(); let nodes = graphContainer.append('g') .attr('class', 'nodes') .selectAll('g') .data(graph.nodes) .enter().append('g') .attr('data-op', (d) => d.operation) .attr('id', (d) => `node-${d.id}`) .attr('class', (d) => `node ${d.type}`) .attr('data-timestamp', (d) => d.timestamp) .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); nodes.append('circle') .attr('r', 16) .style('fill', (d) => (d.status || d.status == 0) ? statusColor(d.status) : '#efefef') .style('stroke', '#424242') .style('stroke-width', '1px') nodes.append('text') .attr('class', 'label') .attr('x', '18') .attr('y', '8') .style('font-size', '12px').style('fill', 'white') .text((d) => { if (d.type != 'link') { return d.name; } }); nodes.append('g') .attr('class', 'icons') .html((d) => { let c = updateIconAttr(cloneImgIcon(d), d.status); let l = ''; if (d.type == 'link') { l = document.getElementById('link-img').cloneNode(true); l = updateIconAttr(l, d.status); c.classList.add('hidden'); c.style.display = 'none'; } return c.outerHTML + l.outerHTML; }) createLegend(container, graph); let simulation = graph.simulation; simulation .nodes(graph.nodes) .on('tick', ticked) .force('link') .links(graph.links) .distance((d) => global().LINK_LENGTHS[d.type]); function ticked() { arrows .attr('points', (d) => getPolylineCoords(d.source.x, d.source.y, d.target.x, d.target.y)) .attr('transform', (d) => rotateArrow(d.source.x, d.source.y, d.target.x, d.target.y)); nodes .attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')') .on('mouseover', (d, i) => { if (graph.tooltip) { graph.tooltip.transition() .duration(200) .style('opacity', .9); graph.tooltip.html(generateTooltipHTML(d)) .style('left', `${d.x + 10}px`) .style('top', `${d.y + 10}px`); } }) .on('mouseout', (d) => { if (graph.tooltip) { graph.tooltip.transition() .duration(500) .style('opacity', 0); } }); } function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart() d.fx = d.x d.fy = d.y } function dragged(d) { d.fx = d3.event.x d.fy = d3.event.y } function dragended(d) { d.fx = d3.event.x d.fy = d3.event.y if (!d3.event.active) simulation.alphaTarget(0); } function generateTooltipHTML(d) { let ret = ''; switch (d['type']) { case 'operation': ret += 'name: ' + sanitize(d['name']) + '<br/>'; ret += 'op_id: ' + d['id'] + '<br/>'; ret += 'created: ' + d['timestamp'] + '<br/>'; break; case 'tactic': case 'technique_name': let p = d['attrs'][d['type']] ret += d['type'] + ': ' + p + '<br/>'; ret += 'created: ' + d['timestamp'] + '<br/>'; for (let attr in d['attrs']) { if (attr != d['type']) { ret += sanitize(attr) + ': ' + sanitize(d['attrs'][attr]) + '<br/>'; } } break; default: ret += d['timestamp'] ? 'created: ' + d['timestamp'] + '<br/>' : ''; for (let attr in d['attrs']) { if (d['attrs'][attr] != null) { ret += sanitize(attr) + ': '; ret += attr == 'status' ? statusName(d['attrs'][attr]) : sanitize(d['attrs'][attr]); ret += '<br/>'; } } } return ret; } function getPolylineCoords(x1, y1, x2, y2) { let p1 = `${x1} ${y1}`; let x = x1 - Math.hypot(x2-x1, y2-y1) + 17; let p2 = `${x} ${y1}`; let p3 = `${x + 7.5} ${y1 + 4}`; let p4 = `${x + 7.5} ${y1 - 4}`; let p5 = p2; return `${p1}, ${p2}, ${p3}, ${p4}, ${p5}`; } function rotateArrow(x1, y1, x2, y2) { let deltaX = x2 - x1; let deltaY = y2 - y1; let angleDeg = Math.atan2(deltaY, deltaX) * 180 / Math.PI + 180; return `rotate(${angleDeg}, ${x1}, ${y1})`; } } function createLegend(container, graph) { let opGraphWidth = document.getElementById('debrief-graph').offsetWidth; let width = (graph.type !== 'fact') ? opGraphWidth : global().FACT_GRAPH_WIDTH; let height = (graph.type !== 'fact') ? global().OP_GRAPH_HEIGHT : global().FACT_GRAPH_HEIGHT; let legend = container.append('g') .attr('class', 'legend') legend.append('rect') .attr('id', 'legend-rect-' + graph.type) .attr('x', width - 193) .attr('y', 10) .attr('rx', 6) .attr('width', 183) .attr('height', 50) .style('fill', 'rgba(170, 170, 170, 0.5)') legend.append('text') .attr('x', width - 130) .attr('y', 35) .style('font-weight', 'bold') .style('fill', 'white') .text('Legend') let entry = legend.selectAll('g') .data(graph.nodes.filter(isUniqueImg).concat(addLinkImg(graph.type))) .enter() .append('g') let lineHeight = 30; let upperPadding = 60; entry.append('svg') .attr('x', width - 180) .attr('y', (d, i) => { document.getElementById(`legend-rect-${graph.type}`).setAttribute('height', parseInt(document.getElementById(`legend-rect-${graph.type}`).getAttribute('height')) + lineHeight); let yVal = i * lineHeight + upperPadding; if (yVal > height - 80) { document.getElementById('debrief-graph').setAttribute('height', height + lineHeight); height = document.getElementById('debrief-graph').getAttribute('height'); } return yVal; }) .attr('width', 20) .attr('height', 20) .html(function (d) { let clone = cloneImgIcon(d); this.id = clone.id + '-legend'; this.setAttribute('viewBox', clone.getAttribute('viewBox')); this.setAttribute('preserveAspectRatio', 'xMidYMid meet'); this.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); this.setAttribute('version', '1.0'); return clone.innerHTML; }) entry.append('text') .attr('x', width - 135) .attr('y', (d, i) => i * lineHeight + upperPadding + lineHeight / 2) .style('fill', 'white') .style('font-size', 13) .style('text-transform', 'capitalize') .text((d) => d.img.indexOf(' ') == -1 ? d.img : d.type); if (graph.type === 'fact') { let legendHeight = 50 + parseInt(document.getElementById(`legend-rect-${graph.type}`).getAttribute('height')); let factCountTable = legend.append('g') .attr('class', 'fact-count') let factEntry = factCountTable.selectAll('g') .data(graph.nodes.filter(x => x.type == 'fact').filter(isUniqueFactTrait)) .enter() .append('g') factEntry.append('text') .attr('x', width - 190) .attr('y', (d, i) => legendHeight + i * 20) .style('fill', 'white') .style('font-size', 13) .text((d) => graph.nodes.filter(x => x.name == d.name).length) factEntry.append('text') .attr('x', width - 160) .attr('y', (d, i) => legendHeight + i * 20) .style('fill', 'white') .style('font-size', 13) .style('font-weight', 'normal') .text((d) => d.name) } } function moveLegend() { if (!graphs) return d3.selectAll('.legend').remove(); graphs.forEach((graph) => { createLegend(d3.select(`${graph.id} > .container`), graph); }); } function cloneImgIcon(d) { let c; try { if (d.img.indexOf(' ') === -1 && document.getElementById(`${d.img}-img`)) { c = document.getElementById(`${CSS.escape(d.img)}-img`).cloneNode(true); } else { c = document.getElementById(`${CSS.escape(d.type)}-img`).cloneNode(true); } } catch { c = document.getElementById('#unknown-img').cloneNode(true); } return c; } function updateIconAttr(svg, status) { svg.setAttribute('id', null); svg.setAttribute('width', 32); svg.setAttribute('height', 16); svg.setAttribute('x', '-16'); svg.setAttribute('y', '-8'); if (status && status == -2) { svg.children[0].setAttribute('fill', 'white'); } return svg; } function statusColor(status) { if (status === 0) { return '#44AA99'; } else if (status === -2) { return 'black'; } else if (status === 1) { return '#CC3311'; } else if (status === 124) { return 'cornflowerblue'; } else if (status === -3) { return '#FFB000'; } else if (status === -4) { return 'white'; } else if (status === -5) { return '#EE3377'; } return '#555555'; } function limitFactsDisplayed(operations) { let hasOverFactLimit = operations.some((op) => Array.from(document.querySelectorAll(`#debrief-fact-svg g.fact[data-op="${op}"]`)).slice(global().FACT_DISPLAY_LIMIT).length > 0) if (hasOverFactLimit) { document.getElementById('fact-limit-msg').innerHTML = `More than ${global().FACT_DISPLAY_LIMIT} facts found in the operation(s) selected. For readability, only the first ${global().FACT_DISPLAY_LIMIT} facts of each operation are displayed.`; document.getElementById('fact-limit').style.display = ''; operations.forEach((opId) => { let nodesToRemove = Array.from(document.querySelectorAll(`#debrief-fact-svg g.fact[data-op="${opId}"]`)).splice(global().FACT_DISPLAY_LIMIT); nodesToRemove.forEach((node) => { let nodeId = node.id.split('node-')[1]; Array.from(document.querySelectorAll(`#debrief-fact-svg polyline.relationship[data-source="${nodeId}"]`)).forEach((el) => el.remove()); Array.from(document.querySelectorAll(`#debrief-fact-svg polyline.relationship[data-target="${nodeId}"]`)).forEach((el) => el.remove()); node.remove(); }) }) } else { document.getElementById('fact-limit').style.display = 'none'; } } function isUniqueImg(value, index, self) { let arr = Array.from(self, x => x.img.indexOf(' ') === -1 ? x.img : x.type); let v = value.img.indexOf(' ') === -1 ? value.img : value.type; return arr.indexOf(v) === index; } function addLinkImg(graphType) { return graphType == 'graph' ? [{'name': 'link-image', 'img': 'link'}] : []; } function isUniqueFactTrait(value, index, self) { let arr = Array.from(self, x => x.name); return arr.indexOf(value.name) === index; } init();