//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//! Reveal trends and patterns from malware tracker data.
import KeyLines from "keylines";
import data from "./malware-data.js";
let chart;
let timebar;
const IPsLinkedHosts = {};
const maxMalwareSelection = 3;
const tooltipElements = {
tooltip: document.getElementById("tooltip"),
servers: document.getElementById("servers"),
};
const onlineOnlyButton = document.getElementById("onlineOnly");
async function doLayout() {
await chart.layout("organic", {
time: 400,
easing: "linear",
mode: "adaptive",
});
}
function getSelectionCriteria(malwares) {
const showOnlyOnline = onlineOnlyButton.value === "Show All";
return (item) => {
const status = showOnlyOnline ? item.d.status === "online" : true;
if (item.d.type === "asn") return true;
if (malwares) return item.d.malware in malwares && status;
return status;
};
}
function highlightMalware(malwares) {
timebar.selection(Object.values(malwares));
chart.foreground(
getSelectionCriteria(Object.keys(malwares).length !== 0 ? malwares : null),
{ animate: false, type: "link" }
);
}
function showTrends() {
// Find the checked checkboxes
const checkedMalwares = Array.from(
document.querySelectorAll("input[name=malware]:checked")
);
// If there are more than 3, uncheck the 4th one until there are just 3
while (checkedMalwares.length > maxMalwareSelection) {
checkedMalwares.pop().checked = false;
}
const reachedMaxSelections = checkedMalwares.length === maxMalwareSelection;
document
.querySelectorAll("input[name=malware]:not(:checked)")
.forEach((radioButton) => {
radioButton.disabled = reachedMaxSelections;
radioButton.parentElement.style.color = reachedMaxSelections
? "DarkGray"
: "black";
});
const malwareNames = {};
checkedMalwares.forEach((element, index) => {
const elementColour = element.parentElement
.getElementsByTagName("dt")
.item(0).style.background;
malwareNames[element.value] = { id: [], index, c: elementColour };
});
chart.each({ type: "link" }, (link) => {
if (link.d.malware in malwareNames) {
malwareNames[link.d.malware].id.push(link.id1);
}
});
// Update timebar selection and chart foregrounding
timebar.selection([]);
highlightMalware(malwareNames);
}
function showInfo() {
const info = document.getElementById("info");
const selection = chart.selection();
let title = "";
let status = "";
let imageDisplayStyle = "none";
if (selection.length === 1) {
const item = chart.getItem(selection[0]);
if (item.type === "node" && item.d.type === "host") {
title = item.d.host;
status = `Current status: <span class="${item.d.status}">${item.d.status}</span>`;
imageDisplayStyle = "block";
}
}
info.getElementsByTagName("h5").item(0).innerText = title;
info.getElementsByTagName("p").item(0).innerHTML = status;
info.getElementsByTagName("img").item(0).style.display = imageDisplayStyle;
}
function mergeObjects(object2, object1) {
Object.keys(object1).forEach((host) => {
object2[host] = object1[host];
});
return object2;
}
// Cache results for filtering by time. Only host nodes have timestamps so we need
// a reference for other node types to see what they're linked to
function calculateIPsNeighbours() {
// Build a quick lookup for each item using IP & ISP
chart.each({ type: "link" }, (link) => {
if (link.d.type === "Host2IP") {
IPsLinkedHosts[link.id2] = IPsLinkedHosts[link.id2] || {};
IPsLinkedHosts[link.id2][link.id1] = 1;
}
});
// Now do another pass to filter for the ISP
chart.each({ type: "link" }, (link) => {
if (link.d.type === "IP2ISP") {
IPsLinkedHosts[link.id2] = IPsLinkedHosts[link.id2] || {};
// now add all the hosts associated with the IP
IPsLinkedHosts[link.id2] = mergeObjects(
IPsLinkedHosts[link.id2],
IPsLinkedHosts[link.id1]
);
}
});
}
function checkNeighbours(obj) {
return obj && Object.keys(obj).some((id) => timebar.inRange(id));
}
function filterByTimeRange(item) {
if (item.type === "node") {
return timebar.inRange(item.id) || checkNeighbours(IPsLinkedHosts[item.id]);
}
return (
timebar.inRange(item.id1) ||
timebar.inRange(item.id2) ||
checkNeighbours(IPsLinkedHosts[item.id1]) ||
checkNeighbours(IPsLinkedHosts[item.id2])
);
}
function hideTooltip() {
tooltipElements.tooltip.classList.add("fadeout");
}
async function filterItemsInTimeRange() {
hideTooltip();
// Filter the chart to show only items in the new range
await chart.filter(filterByTimeRange, {
animate: false,
type: "all",
hideSingletons: true,
});
// And then adjust the chart's layout
doLayout();
}
function showTooltip({ type, value: numberOfServer, tooltipX, tooltipY }) {
const hoverOverData = type === "bar" || type === "selection";
const { servers, tooltip } = tooltipElements;
if (hoverOverData) {
// If the target is data, update the tooltip content
servers.innerText = numberOfServer;
// The top needs to be adjusted to accommodate the height of the tooltip
const tooltipTop = tooltipY - tooltip.offsetHeight - 15;
// Shift left by half width to centre it
const tooltipLeft = tooltipX - tooltip.offsetWidth / 2;
// Set the position of the tooltip
tooltip.style.left = `${tooltipLeft}px`;
tooltip.style.top = `${tooltipTop}px`;
// Show the tooltip
tooltip.classList.remove("fadeout");
} else {
hideTooltip();
}
}
function initialiseInteractions() {
timebar.on("change", filterItemsInTimeRange);
timebar.on("hover", showTooltip);
chart.on("selection-change", showInfo);
const malwareCheckboxes = document.getElementsByClassName("malware");
Array.from(malwareCheckboxes).forEach((checkbox) => {
checkbox.addEventListener("change", showTrends);
});
document.getElementById("clearFilter").addEventListener("click", () => {
Array.from(malwareCheckboxes).forEach((checkbox) => {
checkbox.checked = false;
});
showTrends();
doLayout();
});
onlineOnlyButton.addEventListener("click", () => {
onlineOnlyButton.value =
onlineOnlyButton.value === "Show All" ? "Only Show Online" : "Show All";
showTrends();
});
}
async function loadDataAndLayout() {
chart.load(data);
await chart.zoom("fit");
await timebar.load(data);
timebar.zoom("fit", { animate: false });
// Run the layout
doLayout();
}
async function loadKeyLines() {
const chartOptions = {
logo: "/images/Logo.png",
minZoom: 0.01,
handMode: true,
linkEnds: { avoidLabels: false },
overview: {
icon: false,
shown: false,
},
};
const timebarOptions = {
showPlay: false,
showExtend: true,
maxRange: {
value: 5,
units: "year",
},
};
const components = await KeyLines.create([
{ id: "kl", container: "klchart", options: chartOptions },
{
id: "tl",
container: "kltimebar",
type: "timebar",
options: timebarOptions,
},
]);
chart = components[0];
timebar = components[1];
await loadDataAndLayout();
calculateIPsNeighbours();
initialiseInteractions();
}
window.addEventListener("DOMContentLoaded", loadKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Find Malware Trends</title>
<link rel="stylesheet" type="text/css" href="/css/keylines.css">
<link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
<link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
<link rel="stylesheet" type="text/css" href="/css/demo.css">
<link rel="stylesheet" type="text/css" href="/malware.css">
<script src="/vendor/jquery.js" defer type="text/javascript"></script>
<script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
<script src="/malware.js" crossorigin="use-credentials" defer type="module"></script>
</head>
<body>
<div class="chart-wrapper demo-cols">
<div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
<div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
<div class="klchart klchart-timebar" id="klchart">
</div>
<div class="kltimebar" id="kltimebar">
<div class="tooltip fadeout" id="tooltip" style="position: absolute; pointer-events: none;">
<div class="arrow"></div>
<div class="inner">
<p>Servers Detected</p><span id="servers"></span>
</div>
</div>
</div>
</div>
</div>
<div class="rhs citext closed" id="demorhscontent">
<div class="title-bar">
<svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
<text x="0" y="38" style="width: 100%;">Find Malware Trends</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<fieldset>
<p>
Explore patterns of malware propagation by visualising the relation between C&C servers
and the hosts and ISPs they are linked to.
</p>
</fieldset>
<fieldset>
<legend>Malware Trends</legend>
<p>Select up to three malware families to see their distribution over time.</p>
<div class="row-fluid">
<dl class="span6" style="margin-bottom: 5px; margin-top: 0px;">
<label class="checkbox zeus">
<input class="malware" type="checkbox" value="ZeuS" name="malware">
<dt style="background: #e41a1c"> </dt>
<dd>ZeuS</dd>
</label>
<label class="checkbox iceix">
<input class="malware" type="checkbox" value="Ice IX" name="malware">
<dt style="background: #377eb8"> </dt>
<dd>IceIX</dd>
</label>
<label class="checkbox vmzeus">
<input class="malware" type="checkbox" value="VMZeuS" name="malware">
<dt style="background: #4daf4a"> </dt>
<dd>VMZeuS</dd>
</label>
<label class="checkbox citadel">
<input class="malware" type="checkbox" value="Citadel" name="malware">
<dt style="background: #984ea3"> </dt>
<dd>Citadel</dd>
</label>
</dl>
<dl class="span6" style="margin: 0px 0px 5px 0px;">
<label class="checkbox kins">
<input class="malware" type="checkbox" value="KINS" name="malware">
<dt style="background: #ff7f00"> </dt>
<dd>KINS</dd>
</label>
<label class="checkbox palevo">
<input class="malware" type="checkbox" value="Palevo" name="malware">
<dt style="background: #ffeb3b"> </dt>
<dd>Palevo</dd>
</label>
<label class="checkbox feodoc">
<input class="malware" type="checkbox" value="Feodo Version C" name="malware">
<dt style="background: #756bb1"> </dt>
<dd>Feodo Version C</dd>
</label>
<label class="checkbox feodod">
<input class="malware" type="checkbox" value="Feodo Version D" name="malware">
<dt style="background: #54278f"> </dt>
<dd>Feodo Version D</dd>
</label>
</dl>
</div>
<div class="row-fluid">
<input class="btn" type="button" value="Clear" id="clearFilter">
<input class="btn pull-right" type="button" value="Only Show Online" id="onlineOnly">
</div>
</fieldset>
<fieldset style="min-height: 110px;">
<p style="margin-top: 10px;">Select a host <img src="/images/icons/email.png" style="width: 22px; height: 22px;"> to inspect</p>
<div class="row-fluid" id="info">
<div class="imagediv"><img src="/images/icons/email.png" style="width: 64px; height: 64px; display: none;"></div>
<div class="textdiv">
<h5></h5>
<p></p>
</div>
</div>
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
dl {
margin-bottom:50px;
}
dl dt {
color:#fff;
float:left;
font-weight:bold;
margin-right:10px;
padding:3px;
width: 18px;
border-radius: 18px;
line-height: 12px;
}
dl dd {
margin:2px 0;
padding:5px 0;
line-height: 12px;
}
span.unknown, span.online, span.offline {
font-weight: bold;
}
.unknown {
color: grey;
}
.online {
color: green;
}
.offline {
color: red;
}
#loadingBarText {
text-align: center;
margin-top: -40px;
}
#info {
height: 70px;
max-width: 99%;
border: lightgray solid 1px;
}
/* Tooltip styling */
#tooltip {
opacity: 0.8;
transition: opacity 0.3s ease;
display: block;
line-height: 18px;
text-align: center;
z-index: 1001;
}
#tooltip .inner {
background-color: grey;
color: white;
padding: 4px;
}
#tooltip .arrow {
position: absolute;
border: solid 8px grey;
border-top-color: transparent;
border-left-color: transparent;
transform: translate(-5px, 60px) rotateZ(45deg);
position: absolute;
top: -10px;
left: 48px;
width: 16px;
height: 16px;
}
#tooltip.fadeout {
opacity: 0;
}
#servers {
font-size: 22px;
}
#info {
display: inline-block;
width: 100%;
}
#info .imagediv {
display: inline-block;
width: 64px;
}
#info .textdiv {
position: relative;
top: -10px;
display: inline-block;
width: calc(100% - 68px);
}
Loading source
