forked from JasonKeirstead/stix2-viz
-
Notifications
You must be signed in to change notification settings - Fork 1
/
application.js
545 lines (483 loc) · 18.7 KB
/
application.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
// Copyright (c) 2016, The MITRE Corporation. All rights reserved.
// See LICENSE.txt for complete terms.
// Config
var d3Config = {
color: d3.scale.category20(),
nodeSize: 10,
linkMultiplier: 20
}
// Init some stuff
// MATT: For optimization purposes, I might look into moving these to
// local variables
selectedContainer = document.getElementById('selection');
uploader = document.getElementById('uploader');
canvasContainer = document.getElementById('canvas-container');
canvas = document.getElementById('canvas');
styles = window.getComputedStyle(uploader);
width = 900;// parseInt(styles.width) - 350;
height = 450;// parseInt(styles.height);
canvas.style.width = width;
canvas.style.height = height;
refRegex = /_ref$/;
relationshipsKeyRegex = /(r|R)elationships?/; // Added by Matt
// Determines the "float and repel" behavior of the nodes
var force = d3.layout.force().charge(-400).linkDistance(d3Config.linkMultiplier * d3Config.nodeSize).size([width, height]);
// Determines the "float and repel" behavior of the text labels
var labelForce = d3.layout.force().gravity(0).linkDistance(25).linkStrength(8).charge(-120).size([width, height]);
var svg = d3.select('svg');
var typeGroups = {};
var typeIndex = 0;
var currentGraph = {
nodes: [],
edges: []
}
var labelGraph = {
nodes: [],
edges: []
}
var idCache = {};
/* ----------------------------------------------------- *
* ******************************************************
* This group of functions is for handling file "upload."
* They take an event as input and parse the file on the
* front end.
* ******************************************************/
function handleFileSelect(evt) {
handleFiles(evt.target.files);
}
function handleFileDrop(evt) {
evt.stopPropagation();
evt.preventDefault();
handleFiles(evt.dataTransfer.files);
}
function handleDragOver(evt) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
}
function handleFiles(files) {
// files is a FileList of File objects (in our case, just one)
for (var i = 0, f; f = files[i]; i++) {
document.getElementById('chosen-files').innerText += f.name + " ";
hideMessages();
var r = new FileReader();
r.onload = function(e) {addToGraph(JSON.parse(e.target.result))};
r.readAsText(f);
}
}
/* ---------------------------------------------------- */
/* ******************************************************
* Handles content pasted to the text area.
* ******************************************************/
function handleTextarea() {
content = document.getElementById('paste-area').value;
handlePackage(content);
}
/* ******************************************************
* Fetches STIX 2.0 data from an external URL (supplied
* user) via AJAX. Server-side Access-Control-Allow-Origin
* must allow cross-domain requests for this to work.
* ******************************************************/
function handleFetchJson() {
var url = document.getElementById("url").value;
fetchJsonAjax(url, handlePackage);
}
/* ******************************************************
* Attempts to build and display the graph from an
* arbitrary input string. If parsing the string does not
* produce valid JSON, fails gracefully and alerts the user.
*
* Takes a string as input.
* ******************************************************/
function handlePackage(package) {
try {
var parsed = JSON.parse(package); // Saving this to a variable stops the rest of the function from executing on parse failure
hideMessages();
addToGraph(parsed);
} catch (err) {
alert("Something went wrong!\n\nError:\n" + err);
}
}
function addToGraph(package) {
// Turn header into a "home" "link"
var header = document.getElementById('header');
header.classList.add('linkish');
buildNodes(package);
initGraph();
}
/* ******************************************************
* Generates the components on the chart from the JSON data
* ******************************************************/
function initGraph() {
force.nodes(currentGraph.nodes).links(currentGraph.edges).start();
labelForce.nodes(labelGraph.nodes).links(labelGraph.edges).start();
// build the arrow.
// (Only builds one arrow which is then referenced by the marker-end
// attribute of the lines)
// code shamelessly cribbed from: http://stackoverflow.com/questions/28050434/introducing-arrowdirected-in-force-directed-graph-d3
var defs = svg.append("svg:defs");
defs.selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.style("stroke-width", 0)
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// create filter with id #drop-shadow
// height=130% so that the shadow is not clipped
var filter = defs.append("filter")
.attr("id", "drop-shadow")
.attr("height", "200%")
.attr("width", "200%")
.attr("x", "-50%") // x and y have to have negative offsets to
.attr("y", "-50%"); // stop the edges from getting cut off
// translate output of Gaussian blur to the right and downwards with 2px
// store result in offsetBlur
filter.append("feOffset")
.attr("in", "SourceAlpha")
.attr("dx", 0)
.attr("dy", 0)
.attr("result", "offOut");
// SourceAlpha refers to opacity of graphic that this filter will be applied to
// convolve that with a Gaussian with standard deviation 3 and store result
// in blur
filter.append("feGaussianBlur")
.attr("in", "offOut")
.attr("stdDeviation", 7)
.attr("result", "blurOut");
filter.append("feBlend")
.attr("in", "SourceGraphic")
.attr("in2", "blurOut")
.attr("mode", "normal");
// Adds style directly because it wasn't getting picked up by the style sheet
var link = svg.selectAll('path.link').data(currentGraph.edges).enter().append('path')
.attr('class', 'link')
.style("stroke", "#aaa")
.style("stroke-width", "3px")
.attr("marker-end", "url(#end)")// Add the arrow to the end of the link
.attr('id', function(d, i) { return "link_" + i; })
.on('click', function(d, i) { handleSelected(d, this) });
// Create the text labels that will be attatched to the paths
var linktext = svg.append("svg:g").selectAll("g.linklabelholder").data(currentGraph.edges);
linktext.enter().append("g").attr("class", "linklabelholder")
.append("text")
.attr("class", "linklabel")
.style("font-size", "13px")
.attr("text-anchor", "start")
.style("fill","#000")
.append("textPath")
.attr("xlink:href",function(d,i) { return "#link_" + i;})
.attr("startOffset", "20%")
.text(function(d) {
return d.label;
});
var node = svg.selectAll("circle.node")
.data(currentGraph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", d3Config.nodeSize)
.style("fill", function(d) { return d3Config.color(d.typeGroup); })
.call(force.drag); // <-- What does the "call()" function do?
node.on('click', function(d, i) { handleSelected(d, this) }); // If they're holding shift, release
// Fix on click/drag, unfix on double click
force.drag().on('dragstart', function(d, i) { handlePin(d, this, true) });//d.fixed = true });
node.on('dblclick', function(d, i) { handlePin(d, this, false) });//d.fixed = false });
// Right click will greatly dim the node and associated edges
// >>>>>>> Does not currently work <<<<<<<
node.on('contextmenu', function(d) {
if(d.dimmed) {
d.dimmed = false; // <-- What is this? Where is this set? How does this work?
d.attr("class", "node");
} else {
d.dimmed = true;
d.attr("class", "node dimmed");
}
})
var anchorNode = svg.selectAll("g.anchorNode").data(labelForce.nodes()).enter().append("svg:g").attr("class", "anchorNode");
anchorNode.append("svg:circle").attr("r", 0).style("fill", "#FFF");
anchorNode.append("svg:text").text(function(d, i) {
return i % 2 == 0 ? "" : titleFor(d.node);
}).style("fill", "#555").style("font-family", "Arial").style("font-size", 12);
// Code in the "tick" function determines where the elements
// should be redrawn every cycle (essentially, it allows the
// elements to be animated)
force.on("tick", function() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y;
dr = 0; // Change this to make paths curved
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
anchorNode.each(function(d, i) {
labelForce.start();
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.childNodes[1].getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 5;
this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
anchorNode.call(function() {
this.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
});
}
/* ******************************************************
* Adds class "selected" to last graph element clicked
* and removes it from all other elements.
*
* Takes datum and element as input.
* ******************************************************/
function handleSelected(d, el) {
jsonString = JSON.stringify(d, replacer, 2); // get only the STIX values
purified = JSON.parse(jsonString); // make a new JSON object from the STIX values
// Remove old values from HTML
selectedContainer.innerHTML = "";
var counter = 0;
Object.keys(purified).forEach(function(key) { // Make new HTML elements and display them
var keyString = key;
if (refRegex.exec(key)) { // key is "created_by_ref"... let's pretty that up
keyString = key.replace(/_(ref)?/g, " ").trim();
}
keyString += ":";
// Create new, empty HTML elements to be filled and injected
var div = document.createElement('div');
var type = document.createElement('div');
var val = document.createElement('div');
// Assign classes for proper styling
if ((counter % 2) != 0) {
div.classList.add("odd"); // every other row will have a grey background
}
type.classList.add("type");
val.classList.add("value");
// Some of the potential values are not very readable (IDs
// and object references). Let's see if we can fix that.
var value = purified[key];
// Lots of assumptions being made about the structure of the JSON here...
if (typeof(value) === 'object') {
value = value.title;
} else if (/--/.exec(value) && !(keyString === "id:")) {
if (!(idCache[value] === null || idCache[value] === undefined)) {
value = currentGraph.nodes[idCache[value]].title; // IDs are gross, so let's display something more readable if we can (unless it's actually the node id)
}
}
// Add the text to the new inner html elements
type.innerText = keyString;
val.innerText = value;
// Add new divs to "Selected Node"
div.appendChild(type);
div.appendChild(val);
selectedContainer.appendChild(div);
// increment the class counter
counter += 1;
});
d3.select('.selected').classed('selected', false);
d3.select(el).classed('selected', true);
}
/* ******************************************************
* Handles pinning and unpinning of nodes.
*
* Takes datum, element, and boolean as input.
* ******************************************************/
function handlePin(d, el, pinBool) {
d.fixed = pinBool;
d3.select(el).classed("pinned", pinBool);
}
/* ******************************************************
* Parses the JSON input and builds the arrays used by
* initGraph().
*
* Takes a JSON object as input.
* ******************************************************/
function buildNodes(package) {
var relationships = [];
// Iterate through each key on the package. If it's an array, assume every item is a TLO.
Object.keys(package).forEach(function(key) {
if(package[key].constructor === Array) {
if (!(relationshipsKeyRegex.exec(key))) { // Relationships are NOT ordinary TLOs
parseTLOs(package[key]);
} else {
relationships = package[key];
}
}
});
addRelationships(relationships);
// Add the legend so we know what's what
var ul = document.getElementById('legend-content');
Object.keys(typeGroups).forEach(function(typeName) {
var li = document.createElement('li');
var val = document.createElement('p');
var key = document.createElement('div');
key.style.backgroundColor = d3Config.color(typeGroups[typeName]);
val.innerText = typeName;
li.appendChild(key);
li.appendChild(val);
ul.appendChild(li);
});
}
/* ******************************************************
* Adds a title to a TLO Node
* ******************************************************/
function titleFor(tlo) {
if(tlo.type === 'relationship') {
return "rel: " + (tlo.value);
} else if (tlo.title !== undefined) {
return tlo.title;
} else {
return tlo.type;
}
}
/* ******************************************************
* Parses valid TLOs from an array of potential TLO
* objects (ideally from the data object)
*
* Takes an array of objects as input.
* ******************************************************/
function parseTLOs(container) {
var cap = container.length;
for(var i = 0; i < cap; i++) {
// So, in theory, each of these should be a TLO. To be sure, we'll check to make sure it has an `id` and `type`. If not, raise an error and ignore it.
var maybeTlo = container[i];
if(maybeTlo.id === undefined || maybeTlo.type === undefined) {
console.error("Should this be a TLO???", maybeTlo)
} else {
addTlo(maybeTlo);
}
}
}
/* ******************************************************
* Adds a TLO node to the graph
*
* Takes a valid TLO object as input.
* ******************************************************/
function addTlo(tlo) {
if(idCache[tlo.id]) {
console.log("Already added, skipping!", tlo)
} else {
if(typeGroups[tlo.type] === undefined) {
typeGroups[tlo.type] = typeIndex++;
}
tlo.typeGroup = typeGroups[tlo.type];
idCache[tlo.id] = currentGraph.nodes.length; // Edges reference nodes by their array index, so cache the current length. When we add, it will be correct
currentGraph.nodes.push(tlo);
labelGraph.nodes.push({node: tlo}); // Two labels will orbit the node, we display the less crowded one and hide the more crowded one.
labelGraph.nodes.push({node: tlo});
labelGraph.edges.push({
source : (labelGraph.nodes.length - 2),
target : (labelGraph.nodes.length - 1),
weight: 1
});
}
}
/* ******************************************************
* Adds relationships to the graph based on the array of
* relationships contained in the data.
*
* Takes an array as input.
* ******************************************************/
function addRelationships(relationships) {
for(var i = 0; i < relationships.length; i++) {
var rel = relationships[i];
if(idCache[rel.source_ref] === null || idCache[rel.source_ref] === undefined) {
console.error("Couldn't find source!", rel);
} else if (idCache[rel.target_ref] === null || idCache[rel.target_ref] === undefined) {
console.error("Couldn't find target!", rel);
} else {
currentGraph.edges.push({source: idCache[rel.source_ref], target: idCache[rel.target_ref], label: rel.value});
}
}
}
/* ******************************************************
* Hides the data entry container and displays the graph
* container
* ******************************************************/
function hideMessages() {
uploader.classList.toggle("hidden");
canvasContainer.classList.toggle("hidden");
}
/* ******************************************************
* Screens out D3 chart data from the presentation.
* Called as the 2nd parameter to JSON.stringify().
* ******************************************************/
function replacer(key, value) {
var blacklist = ["typeGroup", "index", "weight", "x", "y", "px", "py", "fixed", "dimmed"];
if (blacklist.indexOf(key) >= 0) {
return undefined;
}
return value;
}
/* ******************************************************
* Returns the page to its original load state
* ******************************************************/
function resetPage() {
var header = document.getElementById('header');
if (header.classList.contains('linkish')) {
hideMessages();
resetGraphVariables();
document.getElementById('canvas').innerHTML = ""; // empty the svg
document.getElementById('files').value = ""; // reset the files input
document.getElementById('chosen-files').innerHTML = ""; // reset the subheader text
document.getElementById('legend-content').innerHTML = ""; // reset the legend in the sidebar
header.classList.remove('linkish');
}
}
/* ******************************************************
* Resets Graph variables so the graph can be rebuilt
* *****************************************************/
function resetGraphVariables() {
typeGroups = {};
typeIndex = 0;
currentGraph = {
nodes: [],
edges: []
}
labelGraph = {
nodes: [],
edges: []
}
idCache = {};
}
/* ******************************************************
* Generic AJAX 'GET' request.
*
* Takes a URL and a callback function as input.
* ******************************************************/
function fetchJsonAjax(url, cfunc) {
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
xhttp = new ActiveXObject("Microsoft.XMLHTTP"); // For IE5 and IE6 luddites
}
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
cfunc(xhttp.responseText);
}
}
xhttp.open("GET", url, true);
xhttp.send();
}
// Bind our events!
document.getElementById('files').addEventListener('change', handleFileSelect, false);
document.getElementById('paste-parser').addEventListener('click', handleTextarea, false);
document.getElementById('fetch-url').addEventListener('click', handleFetchJson, false);
document.getElementById('header').addEventListener('click', resetPage, false);
uploader.addEventListener('dragover', handleDragOver, false);
uploader.addEventListener('drop', handleFileDrop, false);