diff --git a/contrib/map/LICENSE b/contrib/map/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..235572d45630865f1ab1e82518f99509ad4cac60
--- /dev/null
+++ b/contrib/map/LICENSE
@@ -0,0 +1,27 @@
+BSD License
+
+Copyright © 2016 Cesnet z.s.p.o
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the Cesnet z.s.p.o nor the names of its
+      contributors may be used to endorse or promote products derived from this
+      software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE Cesnet z.s.p.o BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/contrib/map/README b/contrib/map/README
new file mode 100644
index 0000000000000000000000000000000000000000..b69f63eb1c420c1f5c7804e5057253657c4f7369
--- /dev/null
+++ b/contrib/map/README
@@ -0,0 +1,71 @@
++---------------------------------+
+| Warden Map Client 1.0           |
++---------------------------------+
+
+Content
+
+  A. Introduction
+  B. Configuration
+  C. Usage & Help
+
+------------------------------------------------------------------------------
+A. Introduction
+
+  Warden Map Client is very simple client for drawing a map with events from
+  database of the Warden server. It consists of a Python 2.7 backend and
+  a javascript/jquery frontend.
+
+  Backend uses Warden API for downloading of events. Events are processed and
+  enhanced with a geodata via freegeoip.net API. Finally warden-map.json file
+  with information for the frontend is created.
+
+  Frontend uses datamaps project (http://datamaps.github.io/) for visualisation
+  of events on a map. It is possible to check details of the event by moving
+  cursor on a arc. It is also possible to zoom map via scrolling and/or clicking
+  on the plus, minus and, home buttons.
+
+------------------------------------------------------------------------------
+B. Configuration
+
+  1. Copy frontend folder into desired location.
+
+  2. Copy html snippet into your web page, or use it as an iframe.
+     NOTE: If necessary, change css/js paths in a html snippet.
+
+  3. Copy backend folder into desired location.
+
+  4. Setup backend call (warden-map.py) in a crontab.
+     NOTE: Please make sure you will have stored warden-map.json file
+           in the fronted folder.
+     EXAMPLE: ./warden-map.py --events 100 \
+                              --client cz.cesnet.warden.map \
+                              --key certs/key.pem \
+                              --cert certs/cert.pem \
+                              --cacert certs/cacert.pem \
+                              --secret SeCreT \
+                              --output ../fronted/
+
+  5. Enjoy your map.
+
+------------------------------------------------------------------------------
+C. Usage & Help
+
+usage: warden-map.py [-h] [--output /path/] --events <number> --client
+                     <org.ex.cl> --key /path/key.pem --cert /path/cert.pem
+                     --cacert /path/cacert.pem --secret <SeCreT>
+
+optional arguments:
+  -h, --help                 show this help message and exit
+  --output path/            path where warden-map.json should be saved
+
+required arguments:
+  --events <number>          count of events for a map
+  --client <org.ex.cl>       client name
+  --key path/key.pem        SSL key for a client
+  --cert path/cert.pem      SSL cert for a client
+  --cacert path/cacert.pem  SSL cacert for a client
+  --secret <SeCreT>          secret key for a client
+
+
+------------------------------------------------------------------------------
+Copyright (C) 2016 Cesnet z.s.p.o
diff --git a/contrib/map/backend/warden-map.py b/contrib/map/backend/warden-map.py
new file mode 100755
index 0000000000000000000000000000000000000000..096448c562f5a9e35ff26cb4e525b79975731df0
--- /dev/null
+++ b/contrib/map/backend/warden-map.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# warden-map.py
+#
+# Copyright (C) 2016 Cesnet z.s.p.o
+# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+
+
+def getLastEvents(events, client, key, cert, cacert, secret):
+
+  try:
+    ses = Session()
+    req = Request('POST', 'https://warden-hub.cesnet.cz/warden3/getEvents?client='+ client +'&secret='+ secret +'&count=' + events)
+    pre = req.prepare()
+    res = ses.send(pre, cert = (cert, key), verify=cacert)
+  except requests.exceptions.RequestException as error:
+    print error
+    sys.exit(1)
+
+  data = res.json()
+  i = 0
+  eventsList = []
+  for p in data['events']:
+    event = {}
+    if i < events:
+      for key, value in { 'event': 'Category', 'time': 'DetectTime', 'origin': 'Source', 'destination': 'Target'}.iteritems():
+        if value in p:
+          if (key == 'origin') or (key == 'destination'):
+            event[key] = {}
+            if 'IP4' in p[value][0]:
+              event[key]['ip'] = p[value][0]['IP4']
+            else:
+              event[key] = {}
+          elif (key == 'event'):
+            event[key] = ', '.join(p[value])
+          else:
+            event[key] = p[value]
+        else:
+          if (key == 'origin') or (key == 'destination'):
+            event[key] = {}
+          else:
+            event[key] = {}
+      if 'ip' in event['origin']:
+        eventsList.append(event)
+        i += 1
+    else:
+      break
+
+  return eventsList
+
+def getGeolocation(ip):
+
+  try:
+    response = requests.get('http://freegeoip.net/json/' + str(ip[0]))
+  except requests.exceptions.RequestException as error:
+    print error
+    sys.exit(1)
+
+  try:
+    json_data = json.loads(response.text)
+  except ValueError as error:
+    print error
+    sys.exit(1)
+
+  return {'latitude': json_data['latitude'], 'longitude': json_data['longitude'], 'country_name': json_data['country_name'], 'city': json_data['city']}
+
+def main(args):
+
+  events = args.events[0]
+  client = args.client[0]
+  key    = args.key[0]
+  cert   = args.cert[0]
+  cacert = args.cacert[0]
+  secret = args.secret[0]
+
+  if args.output is not None:
+    path = args.output[0] + 'warden-map.json'
+  else:
+    path = 'warden-map.json'
+
+  wardenEvents = getLastEvents(events, client, key, cert, cacert, secret)
+
+  for p in wardenEvents:
+    for target in {'origin', 'destination'}:
+      geoData = {}
+      if 'ip' in p[target]:
+        geoData = getGeolocation(p[target]['ip'])
+        for value in {'latitude', 'longitude', 'country_name', 'city'}:
+          if value in geoData:
+            if not geoData[value]:
+              p[target][value] = "???"
+            else:
+              p[target][value] = geoData[value]
+          else:
+            p[target][value] = "???"
+
+      else:
+        p[target]['ip'] = "???"
+        p[target]['country_name'] = "Czech Republic"
+        p[target]['city'] = "???"
+        p[target]['latitude'] = 49.743
+        p[target]['longitude'] = 15.338
+
+  try:
+    with open(path, 'w') as outfile:
+      json.dump(wardenEvents, outfile)
+  except IOError:
+    print "Error: File does not appear to exist."
+    sys.exit(1)
+
+  return 0
+
+if __name__ == '__main__':
+  import sys
+  import json
+  from requests import Request, Session
+  import requests
+  import argparse
+
+  parser = argparse.ArgumentParser(description='Creates warden-map.json for warden-map.html frontend.',
+                                  formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=30))
+
+  parser.add_argument('--output', metavar='path/', type=str,
+                    nargs=1, help='path where warden-map.json should be saved')
+
+  requiredNamed = parser.add_argument_group('required arguments')
+
+  requiredNamed.add_argument('--events', metavar='<number>', type=str, required=True,
+                    nargs=1, help='count of events for a map')
+  requiredNamed.add_argument('--client', metavar='<org.ex.cl>', type=str, required=True,
+                    nargs=1, help='client name')
+  requiredNamed.add_argument('--key', metavar='path/key.pem', type=str, required=True,
+                    nargs=1, help='SSL key for a client')
+  requiredNamed.add_argument('--cert', metavar='path/cert.pem', type=str, required=True,
+                    nargs=1, help='SSL cert for a client')
+  requiredNamed.add_argument('--cacert', metavar='path/cacert.pem', type=str, required=True,
+                    nargs=1, help='SSL cacert for a client')
+  requiredNamed.add_argument('--secret', metavar='<SeCreT>', type=str, required=True,
+                    nargs=1, help='secret key for a client')
+
+  args = parser.parse_args()
+  main(args)
diff --git a/contrib/map/frontend/css/warden-map.css b/contrib/map/frontend/css/warden-map.css
new file mode 100644
index 0000000000000000000000000000000000000000..a1b53d9e3c8267d80c207214d6291a49094594f9
--- /dev/null
+++ b/contrib/map/frontend/css/warden-map.css
@@ -0,0 +1,79 @@
+/*
+ *
+ * -*- coding: utf-8 -*-
+ *
+ * warden-map.css
+ *
+ * Copyright (C) 2016 Cesnet z.s.p.o
+ * Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+ *
+*/
+
+body {
+  font-family: 'Oswald', sans-serif;
+}
+
+h2 {
+ color: #0062a2;
+}
+
+.hoverinfo {
+  font-family: 'Oswald', sans-serif;
+}
+
+#country {
+  color: #0062a2;
+  font-weight: bold;
+}
+
+
+table {
+  text-align: left;
+  margin: 0;
+  padding: 0;
+  font-size: 12px;
+}
+
+table th {
+  color: #0062a2;
+  padding: 0;
+}
+
+table td {
+  color: #4b4d4a;
+  padding: 0;
+}
+
+#container {
+  overflow: hidden;
+  border: 2px solid #0062a2;
+  border-radius: 5px;
+  background: white;
+  position: relative;
+  width: 1280px;
+  height: 720px;
+  max-width: 100%;
+  max-height: 100%
+}
+
+.zoom-button {
+  width: 40px;
+  height: 40px;
+  border-radius: 5px;
+  border: none;
+  background: #dcdcda;
+  font-size: 23px;
+  font-weight: bold;
+  color: white;
+  cursor: pointer;
+}
+
+.zoom-button:hover {
+  background-color: #0062a2;
+}
+
+#zoom-info {
+  display: inline-block;
+  padding: 10px;
+  color: #0062a2;
+}
diff --git a/contrib/map/frontend/js/warden-map.js b/contrib/map/frontend/js/warden-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..e4610f58490631b4805f292e9f5ba396cb00b9b3
--- /dev/null
+++ b/contrib/map/frontend/js/warden-map.js
@@ -0,0 +1,264 @@
+/*
+ *
+ * -*- coding: utf-8 -*-
+ *
+ * warden-map.js
+ *
+ * Copyright (C) 2016 Cesnet z.s.p.o
+ * Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
+ *
+*/
+
+// NOTE: Change path in a function d3.json() if you separate backend and frontend!
+
+// Zooming functionality is based on WunderBart's implementation
+// Please see following links:
+// https://github.com/wunderbart
+// https://jsfiddle.net/wunderbart/Lom3b0gb/
+
+  function Zoom(args) {
+  $.extend(this, {
+    $buttons:   $(".zoom-button"),
+    $info:      $("#zoom-info"),
+    scale:      { max: 50, currentShift: 0 },
+    $container: args.$container,
+    datamap:    args.datamap
+  });
+
+  this.init();
+}
+
+Zoom.prototype.init = function() {
+  var paths = this.datamap.svg.selectAll("path"),
+      subunits = this.datamap.svg.selectAll(".datamaps-subunit");
+
+  // preserve stroke thickness
+  paths.style("vector-effect", "non-scaling-stroke");
+
+  // disable click on drag end
+  subunits.call(
+    d3.behavior.drag().on("dragend", function() {
+      d3.event.sourceEvent.stopPropagation();
+    })
+  );
+
+  this.scale.set = this._getScalesArray();
+  this.d3Zoom = d3.behavior.zoom().scaleExtent([ 1, this.scale.max ]);
+
+  this._displayPercentage(1);
+  this.listen();
+};
+
+Zoom.prototype.listen = function() {
+  this.$buttons.off("click").on("click", this._handleClick.bind(this));
+
+  this.datamap.svg
+    .call(this.d3Zoom.on("zoom", this._handleScroll.bind(this)))
+    .on("dblclick.zoom", null); // disable zoom on double-click
+};
+
+Zoom.prototype.reset = function() {
+  this._shift("reset");
+};
+
+Zoom.prototype._handleScroll = function() {
+  var translate = d3.event.translate,
+      scale = d3.event.scale,
+      limited = this._bound(translate, scale);
+
+  this.scrolled = true;
+
+  this._update(limited.translate, limited.scale);
+};
+
+Zoom.prototype._handleClick = function(event) {
+  var direction = $(event.target).data("zoom");
+
+  this._shift(direction);
+};
+
+Zoom.prototype._shift = function(direction) {
+  var center = [ this.$container.width() / 2, this.$container.height() / 2 ],
+      translate = this.d3Zoom.translate(), translate0 = [], l = [],
+      view = {
+        x: translate[0],
+        y: translate[1],
+        k: this.d3Zoom.scale()
+      }, bounded;
+
+  translate0 = [
+    (center[0] - view.x) / view.k,
+    (center[1] - view.y) / view.k
+  ];
+
+  if (direction == "reset") {
+    view.k = 1;
+    this.scrolled = true;
+  } else {
+    view.k = this._getNextScale(direction);
+  }
+
+l = [ translate0[0] * view.k + view.x, translate0[1] * view.k + view.y ];
+
+  view.x += center[0] - l[0];
+  view.y += center[1] - l[1];
+
+  bounded = this._bound([ view.x, view.y ], view.k);
+
+  this._animate(bounded.translate, bounded.scale);
+};
+
+Zoom.prototype._bound = function(translate, scale) {
+  var width = this.$container.width(),
+      height = this.$container.height();
+
+  translate[0] = Math.min(
+    (width / height)  * (scale - 1),
+    Math.max( width * (1 - scale), translate[0] )
+  );
+
+  translate[1] = Math.min(0, Math.max(height * (1 - scale), translate[1]));
+
+  return { translate: translate, scale: scale };
+};
+
+Zoom.prototype._update = function(translate, scale) {
+  this.d3Zoom
+    .translate(translate)
+    .scale(scale);
+
+  this.datamap.svg.selectAll("g")
+    .attr("transform", "translate(" + translate + ")scale(" + scale + ")");
+
+  this._displayPercentage(scale);
+};
+
+Zoom.prototype._animate = function(translate, scale) {
+  var _this = this,
+      d3Zoom = this.d3Zoom;
+
+  d3.transition().duration(350).tween("zoom", function() {
+    var iTranslate = d3.interpolate(d3Zoom.translate(), translate),
+        iScale = d3.interpolate(d3Zoom.scale(), scale);
+
+    return function(t) {
+      _this._update(iTranslate(t), iScale(t));
+    };
+  });
+};
+
+Zoom.prototype._displayPercentage = function(scale) {
+  var value;
+
+  value = Math.round(Math.log(scale) / Math.log(this.scale.max) * 100);
+  this.$info.text(value + "%");
+};
+
+Zoom.prototype._getScalesArray = function() {
+  var array = [],
+      scaleMaxLog = Math.log(this.scale.max);
+
+  for (var i = 0; i <= 10; i++) {
+    array.push(Math.pow(Math.E, 0.1 * i * scaleMaxLog));
+  }
+
+  return array;
+};
+
+Zoom.prototype._getNextScale = function(direction) {
+  var scaleSet = this.scale.set,
+      currentScale = this.d3Zoom.scale(),
+      lastShift = scaleSet.length - 1,
+      shift, temp = [];
+
+  if (this.scrolled) {
+
+    for (shift = 0; shift <= lastShift; shift++) {
+      temp.push(Math.abs(scaleSet[shift] - currentScale));
+    }
+
+    shift = temp.indexOf(Math.min.apply(null, temp));
+
+    if (currentScale >= scaleSet[shift] && shift < lastShift) {
+      shift++;
+    }
+
+    if (direction == "out" && shift > 0) {
+      shift--;
+    }
+
+    this.scrolled = false;
+
+  } else {
+
+    shift = this.scale.currentShift;
+
+    if (direction == "out") {
+      shift > 0 && shift--;
+    } else {
+      shift < lastShift && shift++;
+    }
+  }
+
+  this.scale.currentShift = shift;
+
+  return scaleSet[shift];
+};
+
+// Configuration of datamap canvas
+// Futher reading can be found at https://datamaps.github.io/
+function Datamap() {
+  this.$container = $("#container");
+  this.instance = new Datamaps({
+    scope: 'world',
+    element: this.$container.get(0),
+    done: this._handleMapReady.bind(this),
+    projection: 'mercator',
+        fills: {
+          defaultFill: '#dcdcda'
+        },
+        geographyConfig: {
+          borderColor: '#fdfdfd',
+          highlightFillColor: '#4b4d4a',
+          highlightBorderColor: '#fdfdfd',
+          popupOnHover: true,
+          popupTemplate: function(geography, data) {
+          return '<div class="hoverinfo" id="country">' + geography.properties.name + '</div>';
+        },
+        },
+        arcConfig: {
+          strokeColor: '#0062a2',
+          strokeWidth: 1,
+          arcSharpness: 5,
+          animationSpeed: 4000, // Milliseconds
+          popupOnHover: true,
+          // Case with latitude and longitude
+          popupTemplate: function(geography, data) {
+            if ( ( data.origin && data.destination ) && data.origin.latitude && data.origin.longitude && data.destination.latitude && data.destination.longitude ) {
+              // Content of info table
+              str = '<div class="hoverinfo"><table id="event"><tr><th>Warden Event</th></tr><tr><td>Type</td><td>'+ JSON.stringify(data.event) +'</td></tr><tr><td>Detect Time</td><td>'+ JSON.stringify(data.time) +'</td></tr><tr><th>Event origin</th></tr><tr><td>IP</td><td>' + JSON.stringify(data.origin.ip) +  '</td></tr><tr><td>City & Country</td><td>' + JSON.stringify(data.origin.city) + ',&nbsp;' + JSON.stringify(data.origin.country_name) + '</td></tr><tr><td>GPS</td><td>' + JSON.stringify(data.origin.latitude) + ',&nbsp;' + JSON.stringify(data.origin.longitude) + '</td></tr><tr><th>Event Destination</th></tr><tr><td>IP</td><td>' + JSON.stringify(data.destination.ip) + '</td></tr><tr><td>City & Country</td><td>' + JSON.stringify(data.destination.city) + ',&nbsp;' + JSON.stringify(data.destination.country_name) + '</td></tr><tr><td>GPS</td><td>' + JSON.stringify(data.destination.latitude) + ',&nbsp;' + JSON.stringify(data.destination.longitude) + '</td></tr></table></div>';
+              return str.replace(/"/g,"");
+            }
+            // Missing information
+            else {
+              return '';
+            }
+          }
+        }
+      });
+  var instance = this.instance;
+  // Link to a json file with information for drawing.
+  // NOTE: Change path if you separate backend and fronend!
+  d3.json("./warden-map.json", function(error, data) {
+      instance.arc(data);
+  });
+};
+
+
+
+Datamap.prototype._handleMapReady = function(datamap) {
+  this.zoom = new Zoom({
+    $container: this.$container,
+    datamap: datamap
+  });
+}
diff --git a/contrib/map/frontend/warden-map.html b/contrib/map/frontend/warden-map.html
new file mode 100644
index 0000000000000000000000000000000000000000..09b9593e63eab318e98086e716b12285e0d5d8a0
--- /dev/null
+++ b/contrib/map/frontend/warden-map.html
@@ -0,0 +1,39 @@
+<!--                                                                                   -->
+<!--                                                                                   -->
+<!-- -*- coding: utf-8 -*-                                                             -->
+<!--                                                                                   -->
+<!--  warden-map.html                                                                  -->
+<!--                                                                                   -->
+<!-- Copyright (C) 2016 Cesnet z.s.p.o                                                 -->
+<!-- Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. -->
+<!--                                                                                   -->
+<!--                                                                                   -->
+
+
+<!DOCTYPE html>
+<meta name="robots" content="noindex">
+<meta charset="utf-8">
+<link href='https://fonts.googleapis.com/css?family=Oswald&amp;subset=latin,latin-ext' rel='stylesheet' type='text/css'>
+<link rel="stylesheet" type="text/css" href="./css/warden-map.css"/>
+<body>
+
+<script src="https://d3js.org/d3.v3.min.js"></script>
+<script src="https://d3js.org/topojson.v1.min.js"></script>
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
+<script src="./js/datamaps.world.hires.min.js"></script>
+<script src="./js/warden-map.js"></script>
+
+<h2>Warden Map</h2>
+<div id="tools">
+  <button class="zoom-button" data-zoom="reset">&#x2302</button>
+  <button class="zoom-button" data-zoom="out">-</button>
+  <button class="zoom-button" data-zoom="in">+</button>
+  <div id="zoom-info"></div>
+</div>
+<div id="container"></div>
+
+<!-- Draw datamap into id="container" -->
+<script>new Datamap();</script>
+
+</body>
+</html>