What we’re after
@CyberSiftIO we’ve been going through an exercise of adding “confidence levels” to our visualizations. In other words, how confident is the CyberSift engine that an alert really is an anomaly/outlier? The above screenshot shows one of the ways we visualize the output from this exercise. Each blue node is an internal PC/Server, while the other nodes are detected anomalies – ranging from green (low confidence) to red (high confidence). This heatmap-style visualization immediately allows an analyst to focus on those anomalies that really matter. Without the different colors, there may be too many alerts to investigate, but with the heatmap colors and analyst immediately figures out that best start looking at the deep orange alert on the top right corner. In this post we’ll outline how we built the below visualization.
The toolset
Here’s the libraries we used:
- ReactJS as our base framework (optional – in reality any JS framework could be used, we just like how easy and structured ReactJS makes everything)
- CytoscapeJS as our graph network visualization library
- D3.js for some helper functions
The coding
We’ll assume you have a basic ReactJS app up and running (if not… use create-react-app). The first order of the day is to format our data in a way that CytoscapeJS expects it. In this particular case, this means building an array of objects. Assuming our array of objects is going to be called “cystoscape_elements” we first loop through the internal nodes (light blue ones) and push a data object onto this array:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
internal_nodes = ["192.168.1.1","192.168.2.1"...] //sample data – in reality you'd probably get this from an API | |
cytoscape_elements = []; | |
internal_nodes.forEach(function(element){ | |
cytoscape_elements.push({ | |
data: { | |
id: element, | |
label: element, | |
backgroundColor: "internal" | |
} | |
}); | |
}); |
The most important thing to note in the above is that we add an “extra” object attribute named “backgroundColor” and set to “internal“, which we’ll later use for styling these nodes with the appropriate light blue color.
Next, we need to append our external nodes to the cytoscape_elements object. However unlike in our above code, these nodes need to be given a different color depending on their “confidence rating”. Let’s assume that the confidence rating can range from 0 (low outlier score) to 1 (high outlier score). We need to convert this range into a color palette. Fortunately, D3.js allows you to do exactly that, in a simple way:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as d3 from "d3"; | |
let node_color_scale = d3.scaleLinear() | |
.domain([0, 0.5, 1]) | |
.range([d3.rgb("#96f788"), d3.rgb('#f9f370'), d3.rgb('#f27676')]); |
In the above code, we defined a linear scale with a domain (possible number values) between 0 and 1. In the last line, we map the domain to a custom color range. The first RGB value is the start color which maps to the numerical value of “0”, the middle is the “pivot” value, and the last is the end color which maps to a numerical value of “1”.
Once we have this color scale, we can push external nodes to our array using a similar strategy as above:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
external_nodes = [["1.1.1.1", 0.23],["2.2.2.2", 0.85], …] //sample data – in reality you'd probably get this from an API | |
external_nodes.forEach(function(element){ | |
const label = element[0]; | |
const confidence = element[1]; | |
cytoscape_elements.push({ | |
data: { | |
id: label, | |
label: label, | |
confidence: node_color_scale(confidence) | |
} | |
}); | |
}); |
Note how as before, we define a new object attribute called “confidence”, and we subsequently populate this attribute with our previously defined color scale to convert the numerical confidence to an actual RGB color. We’ll use this attribute later to style the node.
Next, we add the “edges” in the graph connecting the nodes together in a pretty straightforward fashion:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
edges = [["192.168.1.1","1.1.1.1"],["192.168.2.1","2.2.2.2"], …] //sample data – in reality you'd probably get this from an API | |
let id_counter = 0; | |
external_nodes.forEach(function(element){ | |
const src = element[0]; | |
const dst = element[1]; | |
cytoscape_elements.push({ | |
data: { | |
id: "edge_"+id_counter, | |
source: src, | |
target: dst | |
} | |
}); | |
id_counter ++; | |
}); |
Note the use of javascript type coercion (from int to string) as our id, and note the javascript scope of allowing the inner forEach function to increment the id_counter variable.
Last, we put it all together:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var cy = cytoscape({ | |
container: document.getElementById('graphNetwork'), // container to render in | |
elements: cytoscape_elements, // list of graph elements | |
style: [ // the stylesheet for the graph | |
{ | |
selector: 'node', | |
style: { | |
'background-color': 'data(confidence)', | |
'label': 'data(label)', | |
'text-opacity': 0.5, | |
'font-size': "0.5em", | |
'font-weight': "bold" | |
} | |
}, | |
{ | |
selector: 'node[backgroundColor = "src"]', | |
style: { | |
'background-color': '#58cdfc', | |
'label': 'data(label)', | |
'text-opacity': 0.5, | |
'font-size': "1em" | |
} | |
}, | |
{ | |
selector: 'edge', | |
style: { | |
'width': 3, | |
'line-color': '#ccc', | |
'target-arrow-color': '#ccc', | |
'target-arrow-shape': 'triangle' | |
} | |
} | |
] | |
}); |
In the above code, note the lines:
- Line 12: CytoscapeJS uses a system of “selectors” which allow you to style different elements of the graph network. In this case, we’re interested in nodes.
- Line 16: We set the background-color CSS attribute to the value of our “confidence” attribute in the data object (if present) using the notation below (NB: this would be my entire takeaway from this article…)
'data(attribute)'
- Line 32: We again use selectors to style the internal nodes with a light blue. Recall that internal nodes had a data attribute of “backgroundColor” set to “src“, and we leverage this in the selector:
'node[backgroundColor = "src"]'
The above code in plain English says: select those nodes whose “backgroundColor“ is set to “src“
Wrap Up
As you can see, the most important points in this article relate to how to effectively use CytoscapeJS custom data attributes along with CytoscapeJS selectors. Together, they allow you to create very striking visualizations that really communicate a point efficiently to your target audience.
You must be logged in to post a comment.