mirror of https://github.com/CIRCL/lookyloo
chg: Step1 - use join pattern to build tree
parent
2e60c3a95f
commit
296432a096
|
@ -2,13 +2,14 @@
|
|||
|
||||
// Set the dimensions and margins of the diagram
|
||||
var margin = {top: 20, right: 200, bottom: 30, left: 90},
|
||||
width = 9600 - margin.left - margin.right,
|
||||
height = 10000 - margin.top - margin.bottom;
|
||||
width = 960 - margin.left - margin.right,
|
||||
height = 1000 - margin.top - margin.bottom;
|
||||
|
||||
var node_width = 0;
|
||||
var max_overlay_width = 1500;
|
||||
var default_max_overlay_height = 500;
|
||||
var node_height = 45;
|
||||
var node_height = 55;
|
||||
var t = d3.transition().duration(750);
|
||||
|
||||
var main_svg = d3.select("body").append("svg")
|
||||
.attr("width", width + margin.right + margin.left)
|
||||
|
@ -40,36 +41,23 @@ pattern.append('rect')
|
|||
.attr('height', height)
|
||||
.attr("fill", "#EEEEEE");
|
||||
|
||||
var background = main_svg.append('rect')
|
||||
.attr('y', 0)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.style('fill', "url(#backstripes)");
|
||||
|
||||
// append the svg object to the body of the page
|
||||
// appends a 'group' element to 'svg'
|
||||
// moves the 'group' element to the top left margin
|
||||
var node_container = main_svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
var i = 0,
|
||||
duration = 750,
|
||||
root;
|
||||
|
||||
// declares a tree layout and assigns the size
|
||||
var treemap = d3.tree().size([height, width]);
|
||||
duration = 750;
|
||||
|
||||
// Assigns parent, children, height, depth
|
||||
root = d3.hierarchy(treeData, function(d) { return d.children; });
|
||||
root.x0 = height / 2;
|
||||
var root = d3.hierarchy(treeData);
|
||||
root.x0 = height / 2; // middle of the page
|
||||
root.y0 = 0;
|
||||
|
||||
|
||||
// Fancy expand all the nodes one after the other
|
||||
// Collapse after the second level
|
||||
//root.children.forEach(collapse);
|
||||
|
||||
// declares a tree layout
|
||||
var tree = d3.tree();
|
||||
update(root);
|
||||
|
||||
// Collapse the node and all it's children
|
||||
|
@ -81,14 +69,6 @@ function collapse(d) {
|
|||
}
|
||||
};
|
||||
|
||||
// Gets the size of the box and increase it
|
||||
function getBB(selection) {
|
||||
selection.each(function(d) {
|
||||
d.data.total_width = d.data.total_width ? d.data.total_width : 0;
|
||||
d.data.total_width += this.getBBox().width;
|
||||
})
|
||||
};
|
||||
|
||||
function urlnode_click(d) {
|
||||
var url = "/tree/url/" + d.data.uuid;
|
||||
d3.blob(url, {credentials: 'same-origin'}).then(function(data) {
|
||||
|
@ -102,6 +82,15 @@ d3.selection.prototype.moveToFront = function() {
|
|||
});
|
||||
};
|
||||
|
||||
d3.selection.prototype.moveToBack = function() {
|
||||
return this.each(function() {
|
||||
var firstChild = this.parentNode.firstChild;
|
||||
if (firstChild) {
|
||||
this.parentNode.insertBefore(this, firstChild);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// What happen when clicking on a domain (load a modal display)
|
||||
function hostnode_click(d) {
|
||||
// Move the node to the front (end of the list)
|
||||
|
@ -248,27 +237,29 @@ function hostnode_click(d) {
|
|||
});
|
||||
};
|
||||
|
||||
function icon(icons, key, icon_path){
|
||||
var content = icons.append("g");
|
||||
function icon(key, icon_path, d){
|
||||
var iconContent = d3.create("svg") // WARNING: svg is required there, "g" doesn't have getBBox
|
||||
.attr('class', 'icon');
|
||||
var icon_size = 16;
|
||||
var has_icon = false;
|
||||
|
||||
content.filter(function(d){
|
||||
iconContent.datum(d);
|
||||
iconContent.filter(d => {
|
||||
if (typeof d.data[key] === 'boolean') {
|
||||
return d.data[key];
|
||||
has_icon = d.data[key];
|
||||
} else if (typeof d.data[key] === 'number') {
|
||||
return d.data[key] > 0;
|
||||
has_icon = d.data[key] > 0;
|
||||
} else if (d.data[key] instanceof Array) {
|
||||
return d.data[key].length > 0;
|
||||
has_icon = d.data[key].length > 0;
|
||||
}
|
||||
return false;
|
||||
return has_icon;
|
||||
}).append('image')
|
||||
.attr("width", icon_size)
|
||||
.attr("height", icon_size)
|
||||
.attr('x', function(d) { return d.data.total_width ? d.data.total_width + 1 : 0 })
|
||||
.attr("xlink:href", icon_path).call(getBB);
|
||||
.attr("xlink:href", icon_path);
|
||||
|
||||
|
||||
content.filter(function(d){
|
||||
iconContent.filter(d => {
|
||||
if (typeof d.data[key] === 'boolean') {
|
||||
return false;
|
||||
// return d.data[key];
|
||||
|
@ -283,34 +274,50 @@ function icon(icons, key, icon_path){
|
|||
}).append('text')
|
||||
.attr("dy", 8)
|
||||
.style("font-size", "10px")
|
||||
.attr('x', function(d) { return d.data.total_width ? d.data.total_width + 1 : 0 })
|
||||
.attr('width', function(d) { return d.to_print.toString().length + 'em'; })
|
||||
.text(function(d) { return d.to_print; }).call(getBB);
|
||||
|
||||
.attr('x', icon_size + 1)
|
||||
.attr('width', d => d.to_print.toString().length + 'em')
|
||||
.text(d => d.to_print);
|
||||
if (has_icon) {
|
||||
return iconContent.node();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function icon_list(parent_svg, relative_x_pos, relative_y_pos) {
|
||||
function icon_list(relative_x_pos, relative_y_pos, d) {
|
||||
// Put all the icone in one sub svg document
|
||||
var icons = parent_svg
|
||||
.append('svg')
|
||||
var icons = d3.create("svg") // WARNING: svg is required there, "g" doesn't have getBBox
|
||||
.attr('x', relative_x_pos)
|
||||
.attr('y', relative_y_pos);
|
||||
.attr('y', relative_y_pos)
|
||||
.datum(d);
|
||||
icon_options = [
|
||||
['js', "/static/javascript.png"],
|
||||
['exe', "/static/exe.png"],
|
||||
['css', "/static/css.png"],
|
||||
['font', "/static/font.png"],
|
||||
['html', "/static/html.png"],
|
||||
['json', "/static/json.png"],
|
||||
['iframe', "/static/ifr.png"],
|
||||
['image', "/static/img.png"],
|
||||
['unknown_mimetype', "/static/wtf.png"],
|
||||
['video', "/static/video.png"],
|
||||
['request_cookie', "/static/cookie_read.png"],
|
||||
['response_cookie', "/static/cookie_received.png"],
|
||||
['redirect', "/static/redirect.png"],
|
||||
['redirect_to_nothing', "/static/cookie_in_url.png"]
|
||||
];
|
||||
|
||||
icon(icons, 'js', "/static/javascript.png");
|
||||
icon(icons, 'exe', "/static/exe.png");
|
||||
icon(icons, 'css', "/static/css.png");
|
||||
icon(icons, 'font', "/static/font.png");
|
||||
icon(icons, 'html', "/static/html.png");
|
||||
icon(icons, 'json', "/static/json.png");
|
||||
icon(icons, 'iframe', "/static/ifr.png");
|
||||
icon(icons, 'image', "/static/img.png");
|
||||
icon(icons, 'unknown_mimetype', "/static/wtf.png");
|
||||
icon(icons, 'video', "/static/video.png");
|
||||
icon(icons, 'request_cookie', "/static/cookie_read.png");
|
||||
icon(icons, 'response_cookie', "/static/cookie_received.png");
|
||||
icon(icons, 'redirect', "/static/redirect.png");
|
||||
icon(icons, 'redirect_to_nothing', "/static/cookie_in_url.png");
|
||||
nb_icons = 0
|
||||
icon_options.forEach(entry => {
|
||||
bloc = icon(entry[0], entry[1], d);
|
||||
if (bloc){
|
||||
icons
|
||||
.append(() => bloc)
|
||||
.attr('x', 25 * nb_icons); // FIXME: make that distance a variable
|
||||
nb_icons += 1;
|
||||
};
|
||||
})
|
||||
|
||||
// FIXME: that need to move somewhere else, doesn't make sense here.
|
||||
icons.filter(function(d){
|
||||
if (d.data.sane_js_details) {
|
||||
d.libinfo = d.data.sane_js_details[0];
|
||||
|
@ -318,19 +325,21 @@ function icon_list(parent_svg, relative_x_pos, relative_y_pos) {
|
|||
}
|
||||
return false;
|
||||
}).append('text')
|
||||
.attr('x', function(d) { return d.data.total_width ? d.data.total_width + 5 : 0 })
|
||||
.attr('x', 5)
|
||||
.attr('y', 15)
|
||||
.style("font-size", "15px")
|
||||
.text(function(d) { return 'Library inforamtion: ' + d.libinfo }).call(getBB);
|
||||
.text(function(d) { return 'Library information: ' + d.libinfo });
|
||||
|
||||
return icons.node();
|
||||
}
|
||||
|
||||
function text_entry(parent_svg, relative_x_pos, relative_y_pos, onclick_callback) {
|
||||
function text_entry(relative_x_pos, relative_y_pos, onclick_callback, d) {
|
||||
// Avoid hiding the content after the circle
|
||||
var nodeContent = parent_svg
|
||||
.append('svg')
|
||||
var nodeContent = d3.create("svg") // WARNING: svg is required there, "g" doesn't have getBBox
|
||||
.attr('height', node_height)
|
||||
.attr('x', relative_x_pos)
|
||||
.attr('y', relative_y_pos);
|
||||
.attr('y', relative_y_pos)
|
||||
.datum(d);
|
||||
|
||||
// Add labels for the nodes
|
||||
var text_nodes = nodeContent.append("text")
|
||||
|
@ -341,174 +350,165 @@ function text_entry(parent_svg, relative_x_pos, relative_y_pos, onclick_callback
|
|||
.style("opacity", .9)
|
||||
.attr('cursor', 'pointer')
|
||||
.attr("clip-path", "url(#textOverlay)")
|
||||
.text(function(d) {
|
||||
d.data.total_width = 0; // reset total_width
|
||||
to_display = d.data.name
|
||||
if (d.data.urls_count) {
|
||||
// Only on Hostname node.
|
||||
to_display += ' (' + d.data.urls_count + ')';
|
||||
};
|
||||
return to_display;
|
||||
})
|
||||
.on('click', onclick_callback);
|
||||
|
||||
// This value has to be set once for all for the whole tree and cannot be updated
|
||||
// on click as clicking only updates a part of the tree
|
||||
if (node_width === 0) {
|
||||
text_nodes.each(function(d) {
|
||||
node_width = node_width > this.getBBox().width ? node_width : this.getBBox().width;
|
||||
})
|
||||
node_width += 20;
|
||||
};
|
||||
return text_nodes;
|
||||
.text(d.data.name + ' (' + d.data.urls_count + ')')
|
||||
.on('click',onclick_callback);
|
||||
return nodeContent.node();
|
||||
}
|
||||
|
||||
// Recursiveluy generate the tree
|
||||
function update(source) {
|
||||
// Recursively generate the tree
|
||||
function update(root, computed_node_width=0) {
|
||||
|
||||
// reinitialize max_depth
|
||||
// Current height of the tree (cannot use height because it isn't recomputed when we rename children -> _children)
|
||||
var max_depth = 1
|
||||
root.each(d => {
|
||||
if (d.children){
|
||||
max_depth = d.depth > max_depth ? d.depth : max_depth;
|
||||
}
|
||||
});
|
||||
|
||||
// Update height
|
||||
// 50 is the height of a node, 500 is the minimum so the root node isn't behind the icon
|
||||
var newHeight = Math.max(treemap(root).descendants().reverse().length * node_height, 10 * node_height);
|
||||
treemap = d3.tree().size([newHeight, width]);
|
||||
if (computed_node_width != 0) {
|
||||
// Re-compute SVG size depending on the generated tree
|
||||
var newWidth = Math.max((max_depth + 1) * computed_node_width, node_width);
|
||||
// Update height
|
||||
// node_height is the height of a node, node_height * 10 is the minimum so the root node isn't behind the lookyloo icon
|
||||
var newHeight = Math.max(root.descendants().reverse().length * node_height, 10 * node_height);
|
||||
tree.size([newHeight, newWidth])
|
||||
|
||||
// Set background based on the computed width and height
|
||||
var background = main_svg.insert('rect', ':first-child')
|
||||
.attr('y', 0)
|
||||
// FIXME: + 200 doesn't make much sense...
|
||||
.attr('width', newWidth + margin.right + margin.left + 200)
|
||||
.attr('height', newHeight + margin.top + margin.bottom)
|
||||
.style('fill', "url(#backstripes)");
|
||||
|
||||
// Update size
|
||||
d3.select("body svg")
|
||||
// FIXME: + 200 doesn't make much sense...
|
||||
.attr("width", newWidth + margin.right + margin.left + 200)
|
||||
.attr("height", newHeight + margin.top + margin.bottom)
|
||||
|
||||
// Update pattern
|
||||
main_svg.selectAll('pattern')
|
||||
.attr('width', computed_node_width * 2)
|
||||
pattern.selectAll('rect')
|
||||
.attr('width', computed_node_width)
|
||||
|
||||
}
|
||||
|
||||
// Assigns the x and y position for the nodes
|
||||
var treeData = treemap(root);
|
||||
|
||||
// Compute the new tree layout.
|
||||
var nodes = treeData.descendants(),
|
||||
links = treeData.descendants().slice(1);
|
||||
var treemap = tree(root);
|
||||
|
||||
// Compute the new tree layout. => Note: Need d.x & d.y
|
||||
var nodes = treemap.descendants(),
|
||||
links = treemap.descendants().slice(1);
|
||||
|
||||
// ****************** Nodes section ***************************
|
||||
|
||||
// Update the nodes...
|
||||
// TODO: set that ID to the ete3 node ID
|
||||
var node = node_container.selectAll('g.node')
|
||||
.data(nodes, function(d) {return d.id || (d.id = ++i); });
|
||||
const tree_nodes = node_container.selectAll('g.node')
|
||||
.data(nodes, node => node.data.uuid);
|
||||
|
||||
// Enter any new modes at the parent's previous position.
|
||||
var nodeEnter = node.enter().append('g')
|
||||
.attr('class', 'node')
|
||||
.attr("id", function(d) {
|
||||
return 'node_' + d.data.uuid;
|
||||
})
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + source.y0 + "," + source.x0 + ")";
|
||||
tree_nodes.join(
|
||||
// Enter any new modes at the parent's previous position.
|
||||
enter => {
|
||||
var node_group = enter.append('g');
|
||||
node_group
|
||||
.attr('class', 'node')
|
||||
.attr("id", d => 'node_' + d.data.uuid)
|
||||
.attr("transform", "translate(" + root.y0 + "," + root.x0 + ")")
|
||||
// Add Circle for the nodes
|
||||
.append('circle')
|
||||
.attr('class', 'node')
|
||||
.attr('r', 1e-6)
|
||||
.style("fill", d => d._children ? "lightsteelblue" : "#fff")
|
||||
.on('click', click);
|
||||
// Rectangle around the domain name & icons
|
||||
//.append('rect')
|
||||
//.attr("rx", 6)
|
||||
//.attr("ry", 6)
|
||||
//.attr('x', 13)
|
||||
//.attr('y', -23)
|
||||
//.style("opacity", "0.5")
|
||||
//.attr("stroke", "black")
|
||||
//.attr('stroke-opacity', "0.8")
|
||||
//.attr("stroke-width", "1.5")
|
||||
//.attr("stroke-linecap", "round")
|
||||
//.attr("fill", "white")
|
||||
// Set Hostname text
|
||||
node_group
|
||||
.append(d => text_entry(15, -20, hostnode_click, d));
|
||||
// Set list of icons
|
||||
node_group
|
||||
.append(d => icon_list(17, 10, d));
|
||||
|
||||
return node_group;
|
||||
},
|
||||
update => update,
|
||||
exit => exit
|
||||
// Remove any exiting nodes
|
||||
.call(exit => exit
|
||||
.transition(t)
|
||||
.attr("transform", "translate(" + root.y0 + "," + root.x0 + ")")
|
||||
.remove()
|
||||
)
|
||||
// On exit reduce the node circles size to 0
|
||||
.attr('r', 1e-6)
|
||||
// On exit reduce the opacity of text labels
|
||||
.style('fill-opacity', 1e-6)
|
||||
).call(node => {
|
||||
// Transition to the proper position for the node
|
||||
node.attr("transform", node => "translate(" + node.y + "," + node.x + ")");
|
||||
// Update the node attributes and style
|
||||
node.select('circle.node')
|
||||
.attr('r', 10)
|
||||
.style("fill", node => node._children ? "lightsteelblue" : "#fff")
|
||||
.attr('cursor', 'pointer');
|
||||
node.selectAll('text').nodes().forEach(n => {
|
||||
// Set the width for all the nodes
|
||||
node_width = node_width > n.getBBox().width ? node_width : n.getBBox().width;
|
||||
});
|
||||
// FIXME: should get the bbox of the whole node group
|
||||
node_width += 30;
|
||||
});
|
||||
|
||||
// Add Circle for the nodes
|
||||
nodeEnter.append('circle')
|
||||
.attr('class', 'node')
|
||||
.attr('r', 1e-6)
|
||||
.style("fill", function(d) {
|
||||
return d._children ? "lightsteelblue" : "#fff";
|
||||
})
|
||||
.on('click', click);
|
||||
|
||||
// Set Hostname text
|
||||
text_entry(nodeEnter, 10, -20, hostnode_click);
|
||||
// Set list of icons
|
||||
icon_list(nodeEnter, 12, 10);
|
||||
|
||||
// Normalize for fixed-depth.
|
||||
nodes.forEach(function(d){ d.y = d.depth * node_width});
|
||||
// Update pattern
|
||||
main_svg.selectAll('pattern')
|
||||
.attr('width', node_width * 2)
|
||||
pattern.selectAll('rect')
|
||||
.attr('width', node_width)
|
||||
|
||||
// Update svg width
|
||||
nodes.forEach(function(d){
|
||||
if (d.children){
|
||||
max_depth = d.depth > max_depth ? d.depth : max_depth;
|
||||
}
|
||||
nodes.forEach(d => {
|
||||
// Store the old positions for transition.
|
||||
d.x0 = d.x;
|
||||
d.y0 = d.y;
|
||||
});
|
||||
|
||||
// Re-compute SVG size depending on the generated tree
|
||||
var newWidth = Math.max((max_depth + 2) * node_width, node_width);
|
||||
background.attr('height', newHeight + margin.top + margin.bottom)
|
||||
background.attr('width', newWidth + margin.right + margin.left)
|
||||
treemap.size([newHeight, newWidth])
|
||||
d3.select("body svg")
|
||||
.attr("width", newWidth + margin.right + margin.left)
|
||||
.attr("height", newHeight + margin.top + margin.bottom)
|
||||
|
||||
|
||||
// UPDATE
|
||||
var nodeUpdate = nodeEnter.merge(node);
|
||||
|
||||
// Transition to the proper position for the node
|
||||
nodeUpdate.transition()
|
||||
.duration(duration)
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + d.y + "," + d.x + ")";
|
||||
});
|
||||
|
||||
// Update the node attributes and style
|
||||
nodeUpdate.select('circle.node')
|
||||
.attr('r', 10)
|
||||
.style("fill", function(d) {
|
||||
return d._children ? "lightsteelblue" : "#fff";
|
||||
})
|
||||
.attr('cursor', 'pointer');
|
||||
|
||||
|
||||
// Remove any exiting nodes
|
||||
var nodeExit = node.exit().transition()
|
||||
.duration(duration)
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + source.y + "," + source.x + ")";
|
||||
})
|
||||
.remove();
|
||||
|
||||
// On exit reduce the node circles size to 0
|
||||
nodeExit.select('circle')
|
||||
.attr('r', 1e-6);
|
||||
|
||||
// On exit reduce the opacity of text labels
|
||||
nodeExit.select('text')
|
||||
.style('fill-opacity', 1e-6);
|
||||
|
||||
// ****************** links section ***************************
|
||||
|
||||
// Update the links...
|
||||
var link = node_container.selectAll('path.link')
|
||||
.data(links, function(d) { return d.id; });
|
||||
const link = node_container.selectAll('path.link')
|
||||
.data(links, d => d.id);
|
||||
|
||||
// Enter any new links at the parent's previous position.
|
||||
var linkEnter = link.enter().insert('path', "g")
|
||||
.attr("class", "link")
|
||||
.attr('d', function(d){
|
||||
var o = {x: source.x0, y: source.y0}
|
||||
return diagonal(o, o)
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
var linkUpdate = linkEnter.merge(link);
|
||||
|
||||
// Transition back to the parent element position
|
||||
linkUpdate.transition()
|
||||
.duration(duration)
|
||||
.attr('d', function(d){ return diagonal(d, d.parent) });
|
||||
|
||||
// Remove any exiting links
|
||||
var linkExit = link.exit().transition()
|
||||
.duration(duration)
|
||||
.attr('d', function(d) {
|
||||
var o = {x: source.x, y: source.y}
|
||||
return diagonal(o, o)
|
||||
})
|
||||
.remove();
|
||||
|
||||
// Store the old positions for transition.
|
||||
nodes.forEach(function(d){
|
||||
d.x0 = d.x;
|
||||
d.y0 = d.y;
|
||||
});
|
||||
link.join(
|
||||
enter => enter
|
||||
// Enter any new links at the parent's previous position.
|
||||
.insert('path', "g")
|
||||
.attr("class", "link")
|
||||
.attr('d', d => {
|
||||
var o = {x: root.x0, y: root.y0}
|
||||
return diagonal(o, o)
|
||||
}),
|
||||
update => update,
|
||||
exit => exit
|
||||
.call(exit => exit
|
||||
.transition(t)
|
||||
.attr('d', d => {
|
||||
var o = {x: root.x0, y: root.y0}
|
||||
return diagonal(o, o)
|
||||
})
|
||||
.remove()
|
||||
)
|
||||
).call(link => link
|
||||
//.transition(t)
|
||||
.attr('d', d => diagonal(d, d.parent))
|
||||
);
|
||||
|
||||
// Creates a curved (diagonal) path from parent to the child nodes
|
||||
function diagonal(s, d) {
|
||||
|
@ -526,10 +526,16 @@ function update(source) {
|
|||
if (d.children) {
|
||||
d._children = d.children;
|
||||
d.children = null;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
d.children = d._children;
|
||||
d._children = null;
|
||||
}
|
||||
update(d);
|
||||
}
|
||||
// Call update on the whole Tree
|
||||
update(d.ancestors().reverse()[0]);
|
||||
}
|
||||
|
||||
if (computed_node_width === 0) {
|
||||
update(root, node_width)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue