From a158a21ce80ddc38d55da5bbad03baa1f0e468b9 Mon Sep 17 00:00:00 2001
From: Jan Mach <jan.mach@cesnet.cz>
Date: Wed, 26 Jun 2019 12:19:56 +0200
Subject: [PATCH] Huge improvements in additional object data services.

* Implemented brnad new PassiveDNS data service and pluggable module for Hawat.
* Implemented support for HTML snippet endpoints.
* Implemented basic framework for asynchronous fetching additional data for IP addresses in IDEA event detail view.
* Implemented fetching of additional data for IP addresses.

(Redmine issue: #5278)
---
 Gruntfile.js                                  |   8 +
 conf/core/services.json.conf                  |   8 +
 conf/mentat-hawat.py.conf                     |   1 +
 lib/hawat/app.py                              |  20 +-
 lib/hawat/base.py                             | 144 +++++++---
 .../blueprints/design/static/css/hawat.css    |  52 ++++
 .../design/static/js/hawat-jquery.js          | 111 ++++++++
 .../blueprints/design/templates/_layout.html  |  19 +-
 .../design/templates/_macros_site.html        |  31 +-
 .../design/templates/spt_flashmessage.html    |   4 +
 lib/hawat/blueprints/dnsr/__init__.py         | 101 +++++--
 lib/hawat/blueprints/dnsr/forms.py            |   4 +-
 .../dnsr/templates/dnsr/search.html           |  37 ++-
 .../templates/dnsr/spt_label_hostnames.html   |  36 +++
 .../events/templates/events/search.html       |  86 +++---
 .../events/templates/events/show.html         | 139 +++++++--
 lib/hawat/blueprints/geoip/__init__.py        | 152 ++++++++--
 lib/hawat/blueprints/geoip/forms.py           |   2 +-
 .../geoip/templates/geoip/search.html         |  33 +--
 .../geoip/templates/geoip/spt_label_asn.html  |  35 +++
 .../geoip/templates/geoip/spt_label_city.html |  56 ++++
 lib/hawat/blueprints/nerd/__init__.py         | 105 +++++--
 lib/hawat/blueprints/nerd/forms.py            |   4 +-
 .../nerd/templates/nerd/search.html           |  75 ++++-
 .../templates/nerd/spt_label_reputation.html  |  88 ++++++
 lib/hawat/blueprints/pdnsr/__init__.py        | 266 ++++++++++++++++++
 lib/hawat/blueprints/pdnsr/forms.py           |  67 +++++
 .../pdnsr/templates/pdnsr/search.html         |  92 ++++++
 .../templates/pdnsr/spt_label_hostnames.html  |  55 ++++
 lib/hawat/blueprints/performance/__init__.py  |   2 +-
 lib/hawat/blueprints/whois/__init__.py        | 135 +++++++--
 .../whois/templates/whois/search.html         |  19 +-
 .../templates/whois/spt_label_abuse.html      |  37 +++
 lib/hawat/config.py                           |   2 +
 lib/hawat/const.py                            |  15 +-
 lib/hawat/forms.py                            |  38 +++
 lib/hawat/templates/hawat-main.js             |  41 ++-
 lib/hawat/utils.py                            |  27 ++
 lib/mentat/__init__.py                        |   2 +-
 lib/mentat/const.py                           |   8 +-
 lib/mentat/services/dnsr.py                   |   4 +-
 lib/mentat/services/geoip.py                  |   8 +-
 lib/mentat/services/nerd.py                   |  23 +-
 lib/mentat/services/pdnsr.py                  | 215 ++++++++++++++
 lib/mentat/services/test_dnsr.py              |  20 +-
 .../services/{test_nerd.py => test_pdnsr.py}  |  74 +++--
 package.json                                  |   3 +-
 yarn.lock                                     |  12 +-
 48 files changed, 2161 insertions(+), 355 deletions(-)
 create mode 100644 lib/hawat/blueprints/design/templates/spt_flashmessage.html
 create mode 100644 lib/hawat/blueprints/dnsr/templates/dnsr/spt_label_hostnames.html
 create mode 100644 lib/hawat/blueprints/geoip/templates/geoip/spt_label_asn.html
 create mode 100644 lib/hawat/blueprints/geoip/templates/geoip/spt_label_city.html
 create mode 100644 lib/hawat/blueprints/nerd/templates/nerd/spt_label_reputation.html
 create mode 100644 lib/hawat/blueprints/pdnsr/__init__.py
 create mode 100644 lib/hawat/blueprints/pdnsr/forms.py
 create mode 100644 lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html
 create mode 100644 lib/hawat/blueprints/pdnsr/templates/pdnsr/spt_label_hostnames.html
 create mode 100644 lib/hawat/blueprints/whois/templates/whois/spt_label_abuse.html
 create mode 100644 lib/hawat/utils.py
 create mode 100644 lib/mentat/services/pdnsr.py
 rename lib/mentat/services/{test_nerd.py => test_pdnsr.py} (53%)

diff --git a/Gruntfile.js b/Gruntfile.js
index 0f6c854c2..8c383754d 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -55,6 +55,14 @@ module.exports = function(grunt) {
             // Copy components for web user interface.
             webui: {
                 files: [
+                    // ----- Popper.js
+                    {
+                        expand: true,
+                        flatten: true,
+                        cwd: 'node_modules/popper.js/dist/umd/',
+                        src: './*',
+                        dest: '<%= project_paths.web_static_dir %>vendor/popper.js/js/'
+                    },
                     // ----- Bootstrap
                     {
                         expand: true,
diff --git a/conf/core/services.json.conf b/conf/core/services.json.conf
index fb0036ec2..b87c7f470 100644
--- a/conf/core/services.json.conf
+++ b/conf/core/services.json.conf
@@ -12,6 +12,14 @@
             "lifetime": 3
         },
 
+        #
+        # PassiveDNS service settings.
+        #
+        "pdns": {
+            "base_url": "https://passivedns.cesnet.cz/",
+            "base_api_url": "https://passivedns.cesnet.cz/api/v0/"
+        },
+
         #
         # GeoIP service settings.
         #
diff --git a/conf/mentat-hawat.py.conf b/conf/mentat-hawat.py.conf
index b59cac73a..2d089c656 100644
--- a/conf/mentat-hawat.py.conf
+++ b/conf/mentat-hawat.py.conf
@@ -27,6 +27,7 @@ ENABLED_BLUEPRINTS = [
     'hawat.blueprints.events',
     'hawat.blueprints.timeline',
     'hawat.blueprints.dnsr',
+    'hawat.blueprints.pdnsr',
     'hawat.blueprints.geoip',
     #'hawat.blueprints.nerd',
     'hawat.blueprints.whois',
diff --git a/lib/hawat/app.py b/lib/hawat/app.py
index 1db2a41d5..eb7cb00a5 100644
--- a/lib/hawat/app.py
+++ b/lib/hawat/app.py
@@ -25,6 +25,7 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea
 import sys
 import traceback
 import os
+import uuid
 import copy
 import datetime
 import jinja2
@@ -58,6 +59,7 @@ import hawat.db
 import hawat.intl
 import hawat.events
 import hawat.errors
+import hawat.utils
 from hawat.models.user import GuiUserModel
 
 
@@ -446,6 +448,18 @@ def _setup_app_core(app):
                 default_flow_style=False
             )
 
+        def get_uuid4():
+            """
+            Generate random UUID identifier.
+            """
+            return uuid.uuid4()
+
+        def get_limit_counter(limit = hawat.const.LIMIT_ADS):
+            """
+            Get fresh instance of limit counter.
+            """
+            return hawat.utils.LimitCounter(limit)
+
         return dict(
             get_endpoints_dict    = get_endpoints_dict,
             get_endpoint_class    = get_endpoint_class,
@@ -472,8 +486,10 @@ def _setup_app_core(app):
 
             current_datetime_utc = datetime.datetime.utcnow(),
 
-            include_raw  = include_raw,
-            json_to_yaml = json_to_yaml
+            include_raw       = include_raw,
+            json_to_yaml      = json_to_yaml,
+            get_uuid4         = get_uuid4,
+            get_limit_counter = get_limit_counter
         )
 
     class HawatJSONEncoder(flask.json.JSONEncoder):
diff --git a/lib/hawat/base.py b/lib/hawat/base.py
index 65f3524fa..b88d26743 100644
--- a/lib/hawat/base.py
+++ b/lib/hawat/base.py
@@ -24,6 +24,9 @@ Module contents
 * :py:class:`HawatBlueprint`
 * :py:class:`HTMLMixin`
 * :py:class:`AJAXMixin`
+
+    * :py:class:`SnippetMixin`
+
 * :py:class:`SQLAlchemyMixin`
 * :py:class:`PsycopgMixin`
 * :py:class:`BaseView`
@@ -182,6 +185,7 @@ class HawatApp(flask.Flask):
         self.view_classes = {}
         self.resources    = {}
         self.csag         = {}
+        self.aods         = {}
 
     @property
     def mconfig(self):
@@ -333,49 +337,76 @@ class HawatApp(flask.Flask):
         """
         self.resources[name] = weakref.ref(resource)
 
-    def get_csag(self, group):
+    def get_csag(self, group_name):
         """
-        Return list of all registered context search actions under given group
+        Return list of all registered context search actions for given group name
         (CSAG: Context Search Action Group).
 
-        :param str group: Name of the group.
+        :param str group_name: Name of the group.
         :return: List of all registered context search actions.
         :rtype: list
         """
-        return self.csag.get(group, [])
+        return self.csag.get(group_name, [])
 
-    def set_csag(self, group, title, view_class, params_builder):
+    def set_csag(self, group_name, title, view_class, params_builder):
         """
-        Store new context search action for given group (CSAG: Context Search
+        Store new context search action for given group name (CSAG: Context Search
         Action Group).
 
-        :param str group: Name of the group.
+        :param str group_name: Name of the group.
         :param str title: Title for the search action.
         :param class view_class: Associated view class.
         :param URLParamsBuilder params_builder: URL parameter builder for this action.
         """
-        self.csag.setdefault(group, []).append({
+        self.csag.setdefault(group_name, []).append({
             'title':  title,
             'view':   view_class,
             'params': params_builder
         })
 
-    def set_csag_url(self, group, title, icon, url_builder):
+    def set_csag_url(self, group_name, title, icon, url_builder):
         """
-        Store new context search action for given group (CSAG: Context Search
-        Action Group).
+        Store new URL based context search action for given group name (CSAG: Context
+        Search Action Group).
 
-        :param str group: Name of the group.
+        :param str group_name: Name of the group.
         :param str title: Title for the search action.
         :param str icon: Icon for the search action.
         :param func url_builder: URL builder for this action.
         """
-        self.csag.setdefault(group, []).append({
+        self.csag.setdefault(group_name, []).append({
             'title': title,
             'icon':  icon,
             'url':   url_builder
         })
 
+    def get_aods(self, group_name):
+        """
+        Return list of all registered additional object data services for given
+        object group name (AODS: Additional Object Data Service).
+
+        :param str group_name: Name of the group.
+        :return: List of all additional object data services.
+        :rtype: list
+        """
+        return self.aods.get(group_name, [])
+
+    def set_aods(self, group_name, view_class, params_builder, snippets):
+        """
+        Store new additional object data services for given object group name
+        (AODS: Additional Object Data Service).
+
+        :param str group_name: Name of the group.
+        :param class view_class: Associated view class.
+        :param list snippets: List os snippet identifiers as strings.
+        :param URLParamsBuilder params_builder: URL parameter builder for this action.
+        """
+        self.aods.setdefault(group_name, []).append({
+            'view':     view_class,
+            'params':   params_builder,
+            'snippets': snippets
+        })
+
 
 class HawatBlueprint(flask.Blueprint):
     """
@@ -512,24 +543,7 @@ class AJAXMixin:
     custom view classess based on based on :py:class:`hawat.base.RenderableView`
     to provide the ability to generate JSON responses.
     """
-
-    @staticmethod
-    def process_response_context(response_context):
-        """
-        Perform additional mangling with the response context before generating
-        the response. This method can be useful to delete some context keys, that
-        should not leave the server.
-
-        :param dict response_context: Response context.
-        :return: Possibly updated response context.
-        :rtype: dict
-        """
-        for key in ('search_form', 'item_form'):
-            try:
-                del response_context[key]
-            except KeyError:
-                pass
-        return response_context
+    KW_RESP_FLASH_MESSAGES = 'flash_messages'
 
     @staticmethod
     def abort(status_code, message = None):
@@ -538,7 +552,10 @@ class AJAXMixin:
         code and optional additional message. Return response as JSON document.
         """
         flask.abort(
-            hawat.errors.api_error_response(status_code, message)
+            hawat.errors.api_error_response(
+                status_code,
+                message
+            )
         )
 
     def flash(self, message, category = 'info'):  # pylint: disable=locally-disabled,no-self-use
@@ -550,11 +567,11 @@ class AJAXMixin:
         :param str category: Category of the flash message.
         """
         self.response_context.\
-            setdefault('flash_messages', {}).\
+            setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\
             setdefault(category, []).\
             append(message)
 
-    def redirect(self, target_url = None, default_url = None, exclude_url = None):  # pylint: disable=locally-disabled,no-self-use
+    def redirect(self, target_url = None, default_url = None, exclude_url = None):
         """
         Redirect user to different page. This implementation stores the redirection
         target to the JSON response.
@@ -567,9 +584,27 @@ class AJAXMixin:
             redirect = get_redirect_target(target_url, default_url, exclude_url)
         )
         return flask.jsonify(
-            self.process_response_context(self.response_context)
+            self.process_response_context()
         )
 
+    def process_response_context(self):
+        """
+        Perform additional mangling with the response context before generating
+        the response. This method can be useful to delete some context keys, that
+        should not leave the server.
+
+        :param dict response_context: Response context.
+        :return: Possibly updated response context.
+        :rtype: dict
+        """
+        # Prevent certain response context keys to appear in final response.
+        for key in ('search_form', 'item_form'):
+            try:
+                del self.response_context[key]
+            except KeyError:
+                pass
+        return self.response_context
+
     def generate_response(self, view_template = None):  # pylint: disable=locally-disabled,unused-argument
         """
         Generate the response appropriate for this view class, in this case JSON
@@ -581,12 +616,47 @@ class AJAXMixin:
         if flashed_messages:
             for category, message in flashed_messages:
                 self.response_context.\
-                    setdefault('flash_messages', {}).\
+                    setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\
                     setdefault(category, []).\
                     append(message)
 
         return flask.jsonify(
-            self.process_response_context(self.response_context)
+            self.process_response_context()
+        )
+
+
+class SnippetMixin(AJAXMixin):
+    """
+    Mixin class enabling rendering responses as JSON documents. Use it in your
+    custom view classess based on based on :py:class:`hawat.base.RenderableView`
+    to provide the ability to generate JSON responses.
+    """
+    KW_RESP_SNIPPETS = 'snippets'
+
+    def generate_response(self, view_template = None):  # pylint: disable=locally-disabled,unused-argument
+        """
+        Generate the response appropriate for this view class, in this case JSON
+        document.
+
+        :param str view_template: Override internally preconfigured page template.
+        """
+        flashed_messages = flask.get_flashed_messages(with_categories = True)
+        if flashed_messages:
+            for category, message in flashed_messages:
+                self.response_context.\
+                    setdefault(self.KW_RESP_SNIPPETS, {}).\
+                    setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\
+                    setdefault(category, []).\
+                    append(
+                        self.render_snippet(
+                            'spt_flashmessage.html',
+                            category = category,
+                            message = message
+                        )
+                    )
+
+        return flask.jsonify(
+            self.process_response_context()
         )
 
 
diff --git a/lib/hawat/blueprints/design/static/css/hawat.css b/lib/hawat/blueprints/design/static/css/hawat.css
index 0427b5145..5a953918d 100644
--- a/lib/hawat/blueprints/design/static/css/hawat.css
+++ b/lib/hawat/blueprints/design/static/css/hawat.css
@@ -241,3 +241,55 @@ body {
 .report-message {
     margin-bottom: 1.5em;
 }
+
+.table-event-detail-node {
+    margin-bottom: 0 !important;
+}
+.table-event-detail-node > tbody > tr > th {
+    width: 3em;
+}
+.table-event-detail-node > tbody > tr:first-child > th,
+.table-event-detail-node > tbody > tr:first-child > td {
+    border-top: 0 !important;
+}
+
+/* Custom styles for additional data labels */
+.object-additional-data,
+.object-additional-data > .query-result,
+.object-additional-data > .query-result > .popover-hover {
+    display: inline-block;
+}
+.object-additional-data .query-result:not(:first-child),
+.object-additional-data .query-result .popover-hover:not(:first-child) {
+    margin-left: 3px;
+}
+.object-additional-data .popover-content {
+    max-height: 20em;
+    overflow-y: auto;
+}
+.object-additional-data .popover-title,
+.object-additional-data .popover-content
+{
+    font-size: xx-small;
+}
+.object-additional-data .popover-content .table
+{
+    margin-bottom: 0;
+}
+.object-additional-data .popover .popover-title {
+    background-color: #d9edf7;
+}
+.object-additional-data .popover.bottom .arrow:after {
+    border-bottom-color: #d9edf7;
+}
+/* Note: The static popover class is added dynamically by JavaScript. */
+.static-popover .popover {
+    position: relative;
+    display: block;
+    max-width: 100%;
+}
+.static-popover,
+.popover-hover {
+    z-index: 2;
+}
+
diff --git a/lib/hawat/blueprints/design/static/js/hawat-jquery.js b/lib/hawat/blueprints/design/static/js/hawat-jquery.js
index 99867db7b..cffa6a612 100644
--- a/lib/hawat/blueprints/design/static/js/hawat-jquery.js
+++ b/lib/hawat/blueprints/design/static/js/hawat-jquery.js
@@ -131,7 +131,52 @@ $(function() {
     $('body').popover({
         selector: "[data-toggle=popover]",
     });
+    // Custom popovers implemented using Popper.js library. The Bootstrap variant
+    // does not support some advanced features.
+    $('.object-additional-data').on("mouseenter", ".popover-hover", function() {
+        var ref = $(this);
+        var popup = $($(this).attr("data-popover-content"));
+        popup.toggleClass("hidden static-popover");
+        var popper = new Popper(ref,popup,{
+                placement: 'top',
+                onCreate: function(data){
+                    popup.find('.popover').removeClass('top');
+                    popup.find('.popover').removeClass('bottom');
+                    popup.find('.popover').addClass(data.placement);
+                },
+                onUpdate: function(data){
+                    popup.find('.popover').removeClass('top');
+                    popup.find('.popover').removeClass('bottom');
+                    popup.find('.popover').addClass(data.placement);
+                },
+                modifiers: {
+                    flip: {
+                        behavior: ['top','bottom']
+                    }
+                }
+        });
+    });
+    $('.object-additional-data').on("mouseleave", ".popover-hover", function() {
+        var ref = $(this);
+        var popup = $($(this).attr("data-popover-content"));
+        popup.toggleClass("hidden static-popover");
+    });
 
+    /*
+    $('body').popover({
+        selector: ".popover-html[data-toggle=popover]",
+        html: true,
+        trigger: 'hover click',
+        content: function() {
+            var content = $(this).attr("data-popover-content");
+            return $(content).children(".popover-body").html();
+        },
+        title: function() {
+            var title = $(this).attr("data-popover-content");
+            return $(title).children(".popover-heading").html();
+        }
+    });
+    */
 
     // Callback for triggering re-render of NVD3 charts within the bootstrap tabs.
     // Without this function the charts on invisible tabs are rendered incorrectly.
@@ -170,6 +215,72 @@ $(function() {
         //window.dispatchEvent(new Event('resize'));
     });
 
+
+    function get_additional_data(url, param, elem, ident, snippets) {
+        // Append element for query result and populate it with AJAX spinner.
+        $(elem).append(
+            '<div class="query-result" id="' + ident + '"><i class="fas fa-cog fa-spin" data-toggle="tooltip" title="' + ident + '"></i></div>'
+        );
+        // Obtain reference for recently created element for query result.
+        elem = $(elem).children('#' + ident);
+
+        // Perform asynchronous request.
+        console.log("Request URL" + url + ", snippets: " + snippets);
+        var jqxhr = $.get(url)
+        .done(function(data) {
+            if (data.search_result && ((Array.isArray(data.search_result) && data.search_result.length > 0) || !Array.isArray(data.search_result))) {
+                console.log("Result for " + ident + " query for: " + param);
+                console.debug(data);
+                $(elem).empty();
+                snippets.forEach(function(snippet) {
+                    $(elem).append(data.snippets[snippet]);
+                });
+            }
+            else {
+                console.log("Empty result for " + ident + " query for: " + param);
+                console.debug(data);
+                // Either insert information about empty query result.
+                //$(element).html(
+                //    '<i class="fas fa-check" data-toggle="tooltip" title="Empty result for ' + ident + ' query for ' + param + '"></i>'
+                //);
+                // Or remove the result element to reduce display clutter.
+                $(elem).remove();
+            }
+        })
+        .fail(function(data) {
+            console.log("Failed " + ident + " query for: " + param);
+            console.debug(data);
+            // Either insert information about error query result.
+            $(elem).html(
+                '<i class="fas fa-times" data-toggle="tooltip" title="Error for ' + ident + ' query for ' + param + '"></i>'
+            );
+            // Or remove the result element to reduce display clutter.
+            //$(elem).remove();
+        })
+        .always(function() {
+            //console.log("Finished " + ident + " query for: " + param);
+        });
+    }
+
+    $(".object-additional-data").each(function() {
+        obj_type = $(this).data('object-type');
+        obj_name = $(this).data('object-name');
+        aods     = Hawat.get_aods(obj_type);
+        elem     = this;
+        console.log("Fetching additional data for object '" + obj_type + " -- " + obj_name + "': " + aods.reduce(function(sum, item) { return sum + ' ' + item.endpoint }, ''));
+        console.debug(aods);
+
+        aods.forEach(function(aods_item) {
+            get_additional_data(
+                Flask.url_for(aods_item.endpoint, aods_item.params(obj_name)),
+                obj_name,
+                elem,
+                aods_item.ident,
+                aods_item.snippets
+            );
+        });
+    });
+
     // Special handling of '__EMPTY__' and '__ANY__' options in event search form
     // selects. This method stil can be improved, so that 'any' is capable of disabling
     // 'empty'.
diff --git a/lib/hawat/blueprints/design/templates/_layout.html b/lib/hawat/blueprints/design/templates/_layout.html
index 2246d4cab..eef46f550 100644
--- a/lib/hawat/blueprints/design/templates/_layout.html
+++ b/lib/hawat/blueprints/design/templates/_layout.html
@@ -69,6 +69,9 @@
         <!-- Moment.js -->
         <script src="{{ url_for('design.static', filename='vendor/moment/js/moment-with-locales.min.js') }}"></script>
 
+        <!-- Popper.js -->
+        <script src="{{ url_for('design.static', filename='vendor/popper.js/js/popper.min.js') }}"></script>
+
         <!-- jQuery -->
         <script src="{{ url_for('design.static', filename='vendor/jquery/js/jquery.min.js') }}"></script>
 
@@ -198,7 +201,7 @@
             <div id="ajax-loader">
                 {{ get_icon('ajax-loader') }}
                 <hr>
-                <h3><strong>{{ gettext('... LOADING ...') }}</strong></h3>
+                <h3><strong>{{ _('... LOADING ...') }}</strong></h3>
             </div>
         </div>
 
@@ -207,7 +210,7 @@
             <div class="container-fluid">
                 <div id="navbar-header" class="navbar-header">
                     <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-                        <span class="sr-only">{{ gettext('Toggle navigation') }}</span>
+                        <span class="sr-only">{{ _('Toggle navigation') }}</span>
                         <span class="icon-bar"></span>
                         <span class="icon-bar"></span>
                         <span class="icon-bar"></span>
@@ -224,7 +227,7 @@
                         {{ macros_site.render_locale_switcher() }}
                         {%- if check_endpoint_exists('help.view') %}
                         <li{% if request.endpoint == "help.view" %} class="active"{% endif %}>
-                            <a href="{{ url_for('help.view') }}" data-toggle="tooltip" data-placement="bottom" title="{{ gettext('Browse help') }}">{{ get_icon('help') }}<span class="hidden-sm hidden-md"> {{ gettext('Help') }}</span></a>
+                            <a href="{{ url_for('help.view') }}" data-toggle="tooltip" data-placement="bottom" title="{{ _('Browse help') }}">{{ get_icon('help') }}<span class="hidden-sm hidden-md"> {{ _('Help') }}</span></a>
                         </li>
                         {%- endif %}
                     </ul>
@@ -260,15 +263,15 @@
         <!-- Footer container - BEGIN ----------------------------------------->
         <div class="container-fluid" id="footer">
             <hr>
-            <span data-toggle="tooltip" title="{{ gettext('Page generated at: ') }}">{{ get_icon('clock') }}</span><span class="hidden-sm"> <em>{{ gettext('Page generated at: ') }}</em></span> {{ babel_format_datetime(current_datetime_utc) }} &#124;
-            <span data-toggle="tooltip" title="{{ gettext('Page generated in: ') }}">{{ get_icon('stopwatch') }}</span><span class="hidden-sm"> <em>{{ gettext('Page generated in: ') }}</em></span> {{ get_timedelta(g.requeststart) }}
+            <span data-toggle="tooltip" title="{{ _('Page generated at: ') }}">{{ get_icon('clock') }}</span><span class="hidden-sm"> <em>{{ _('Page generated at: ') }}</em></span> {{ babel_format_datetime(current_datetime_utc) }} &#124;
+            <span data-toggle="tooltip" title="{{ _('Page generated in: ') }}">{{ get_icon('stopwatch') }}</span><span class="hidden-sm"> <em>{{ _('Page generated in: ') }}</em></span> {{ get_timedelta(g.requeststart) }}
             <br>
             <span data-toggle="tooltip" title="{{ hawat_bversion_full }}">
-                Hawat {{ hawat_bversion }}
+                Hawat {{ hawat_version }}
             </span> &#124;
-            &copy; {{ gettext('since') }} 2011 &#124;
+            &copy; {{ _('since') }} 2011 &#124;
             <a data-toggle="tooltip" title="Go to official web page of CESNET organization, the NREN for Czech republic" href="https://www.cesnet.cz">
-                {{ gettext('CESNET, a.l.e.') }}
+                {{ _('CESNET, a.l.e.') }}
             </a> &#124;
             <a data-toggle="tooltip" title="Go to official web page of CESNET-CERTS, the CSIRT for CESNET2 network" href="http://csirt.cesnet.cz">
                 CESNET-CERTS
diff --git a/lib/hawat/blueprints/design/templates/_macros_site.html b/lib/hawat/blueprints/design/templates/_macros_site.html
index deaaeaee2..49d1245df 100644
--- a/lib/hawat/blueprints/design/templates/_macros_site.html
+++ b/lib/hawat/blueprints/design/templates/_macros_site.html
@@ -473,28 +473,29 @@
         for example for keeping the table columns in the result from bloating up.
     string empty_title: Title to be displayed in case the item_list is empty.
     string empty_icon: Icon to be displayed in case the item_list is empty.
+    bool add_newline: Add newline after each widget.
 -#}
-{%- macro _render_widget_csag(csag_group, item_list, mark_list = None, align_right = False, separate_dropdown = False, without_label = False, as_code = False, item_limit = 0, empty_title = _('-- unassigned --'), empty_icon = 'unassigned') %}
-    {%- if item_list %}
-        {%- set tmp_csag_list = get_csag(csag_group) %}
-        {%- for subitem in item_list %}
+{%- macro _render_widget_csag(csag_group, item_list, mark_list = None, align_right = False, separate_dropdown = False, without_label = False, as_code = False, item_limit = 0, empty_title = _('-- unassigned --'), empty_icon = 'unassigned', add_newline = False) %}
+    {%- if item_list -%}
+        {%- set tmp_csag_list = get_csag(csag_group) -%}
+        {%- for subitem in item_list -%}
             {#- Limit number of items from item_list for which to generate CSAG widget. -#}
-            {%- if item_limit and loop.index > item_limit %}
-                {%- if loop.index0 == item_limit %}
+            {%- if item_limit and loop.index > item_limit -%}
+                {%- if loop.index0 == item_limit -%}
     <span class="underlined-tooltip" data-toggle="tooltip" title="{{ _('Please download raw message to view full list.') }}">({{ _('%(count)s more', count = loop.length - loop.index0) }})</span>
-                {%- endif %}
-            {%- else %}
+                {%- endif -%}
+            {%- else -%}
                 {%- if separate_dropdown and not without_label %}
     {%- if as_code %}<code>{%- endif %}{% if mark_list and subitem.__str__() in mark_list %}<mark>{{ subitem }}</mark>{%- else %}{{ subitem }}{%- endif %}{%- if as_code %}</code>{%- endif %}
-                {%- endif %}
+                {%- endif -%}
     <div class="btn-group">
         <button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                 {%- if separate_dropdown and not without_label %}
-            <span class="caret"></span>
+            <i class="fas fa-fw fa-bars"></i>
                 {%- elif not without_label %}
             {% if mark_list and subitem.__str__() in mark_list %}<mark>{{ subitem }}</mark>{%- else %}{{ subitem }}{%- endif %} <span class="caret"></span>
                 {%- else %}
-            <span class="caret"></span>
+            <i class="fas fa-fw fa-bars"></i>
                 {%- endif %}
         </button>
         <ul class="dropdown-menu{% if align_right %} dropdown-menu-right{% endif %}">
@@ -522,7 +523,8 @@
         </ul>
     </div>
             {%- endif %}
-        {%- endfor %}
+            {%- if add_newline and not loop.last %}<br>{% endif %}
+        {% endfor %}
     {%- else %}
     <span data-toggle="tooltip" title="{{ empty_title }}">{{ get_icon(empty_icon) }}</span>
     {%- endif %}
@@ -541,7 +543,7 @@
 }}
 {%- endmacro %}
 
-{%- macro render_widget_csag_address(item_list, mark_list = None, align_right = False, separate_dropdown = False, without_label = False, item_limit = 0) %}
+{%- macro render_widget_csag_address(item_list, mark_list = None, align_right = False, separate_dropdown = False, without_label = False, item_limit = 0, add_newline = False) %}
 {{
     _render_widget_csag(
         'ips',
@@ -553,7 +555,8 @@
         True,
         item_limit,
         _('-- undisclosed --'),
-        'undisclosed'
+        'undisclosed',
+        add_newline
     )
 }}
 {%- endmacro %}
diff --git a/lib/hawat/blueprints/design/templates/spt_flashmessage.html b/lib/hawat/blueprints/design/templates/spt_flashmessage.html
new file mode 100644
index 000000000..a5c1fadab
--- /dev/null
+++ b/lib/hawat/blueprints/design/templates/spt_flashmessage.html
@@ -0,0 +1,4 @@
+{%- import '_macros_site.html' as macros_site with context -%}
+{%- call macros_site.render_alert(category) %}
+{{ message | safe }}
+{%- endcall %}
diff --git a/lib/hawat/blueprints/dnsr/__init__.py b/lib/hawat/blueprints/dnsr/__init__.py
index 8544a0f59..498341de8 100644
--- a/lib/hawat/blueprints/dnsr/__init__.py
+++ b/lib/hawat/blueprints/dnsr/__init__.py
@@ -20,10 +20,27 @@ Provided endpoints
 ------------------
 
 ``/dnsr/search``
-    Page providing search form and displaying search results.
+    Endpoint providing search form for querying external DNS service and formating
+    result as HTML page.
 
     * *Authentication:* login required
     * *Methods:* ``GET``
+
+``/api/dnsr/search``
+    Endpoint providing API search form for querying external DNS service and
+    formating result as JSON document.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
+
+``/snippet/dnsr/search``
+    Endpoint providing API search form for querying external DNS service and
+    formating result as JSON document containing HTML snippets.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
 """
 
 
@@ -45,7 +62,7 @@ from mentat.const import tr_
 import hawat.const
 import hawat.db
 import hawat.acl
-from hawat.base import HTMLMixin, AJAXMixin, RenderableView, HawatBlueprint, URLParamsBuilder
+from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, HawatBlueprint, URLParamsBuilder
 from hawat.blueprints.dnsr.forms import DnsrSearchForm
 
 
@@ -53,12 +70,11 @@ BLUEPRINT_NAME = 'dnsr'
 """Name of the blueprint as module global constant."""
 
 
-class AbstractSearchView(RenderableView):
+class AbstractSearchView(RenderableView):  # pylint: disable=locally-disabled,abstract-method
     """
-    Application view providing search form for internal IP geolocation service
-    and appropriate result page.
+    Application view providing base search capabilities for DNS service.
 
-    The geolocation is implemented using :py:mod:`mentat.services.dnsr` module.
+    The resolving is implemented using :py:mod:`mentat.services.dnsr` module.
     """
     authentication = True
 
@@ -88,10 +104,17 @@ class AbstractSearchView(RenderableView):
                 form_data = form.data
                 dnsr_service = mentat.services.dnsr.service()
                 self.response_context.update(
-                    search_item   = form.search.data,
-                    search_result = dnsr_service.lookup(form.search.data),
-                    form_data     = form_data
+                    search_item = form.search.data,
+                    form_data   = form_data
                 )
+                try:
+                    self.response_context.update(
+                        search_result = dnsr_service.lookup(
+                            form.search.data
+                        )
+                    )
+                except Exception as exc:
+                    self.flash(str(exc), category = 'error')
 
         self.response_context.update(
             search_form  = form,
@@ -102,8 +125,8 @@ class AbstractSearchView(RenderableView):
 
 class SearchView(HTMLMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
     """
-    View responsible for querying `IDEA <https://idea.cesnet.cz/en/index>`__
-    event database and presenting the results in the form of HTML page.
+    View responsible for querying DNS service and presenting the results in the
+    form of HTML page.
     """
     methods = ['GET']
 
@@ -115,8 +138,8 @@ class SearchView(HTMLMixin, AbstractSearchView):  # pylint: disable=locally-disa
 
 class APISearchView(AJAXMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
     """
-    View responsible for querying `IDEA <https://idea.cesnet.cz/en/index>`__
-    event database and presenting the results in the form of JSON document.
+    View responsible for querying DNS service and presenting the results in the
+    form of JSON document.
     """
     methods = ['GET','POST']
 
@@ -126,6 +149,35 @@ class APISearchView(AJAXMixin, AbstractSearchView):  # pylint: disable=locally-d
         return 'apisearch'
 
 
+class SnippetSearchView(SnippetMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying DNS service and presenting the results in the
+    form of JSON document containg ready to use HTML page snippets.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'sptsearch'
+
+    def process_response_context(self):
+        """*Implementation* of :py:func:`hawat.base.SnippetMixin.process_response_context`."""
+        super().process_response_context()
+
+        # Make latter expressions shorter
+        res = self.response_context.get('search_result', False)
+        if res:
+            self.response_context.setdefault(
+                self.KW_RESP_SNIPPETS,
+                {}
+            )['label_hostnames'] = flask.render_template(
+                'dnsr/spt_label_hostnames.html',
+                search_result = res
+            )
+        return self.response_context
+
+
 #-------------------------------------------------------------------------------
 
 
@@ -137,7 +189,7 @@ class DnsrBlueprint(HawatBlueprint):
     @classmethod
     def get_module_title(cls):
         """*Implementation* of :py:func:`hawat.base.HawatBlueprint.get_module_title`."""
-        return lazy_gettext('External DNS pluggable module')
+        return lazy_gettext('DNS pluggable module')
 
     def register_app(self, app):
         """
@@ -162,11 +214,25 @@ class DnsrBlueprint(HawatBlueprint):
         # Register context actions provided by this module.
         app.set_csag(
             hawat.const.HAWAT_CSAG_ADDRESS,
-            tr_('Search for address <strong>%(name)s</strong> in DNS'),
+            tr_('Search for address <strong>%(name)s</strong> in DNS service'),
             SearchView,
             URLParamsBuilder({'submit': tr_('Search')}).add_rule('search')
         )
 
+        # Register additional object data services provided by this module.
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP4,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search'),
+            ['label_hostnames']
+        )
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP6,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search'),
+            ['label_hostnames']
+        )
+
 
 #-------------------------------------------------------------------------------
 
@@ -183,7 +249,8 @@ def get_blueprint():
         template_folder = 'templates'
     )
 
-    hbp.register_view_class(SearchView,    '/{}/search'.format(BLUEPRINT_NAME))
-    hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(SearchView,        '/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(APISearchView,     '/api/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME))
 
     return hbp
diff --git a/lib/hawat/blueprints/dnsr/forms.py b/lib/hawat/blueprints/dnsr/forms.py
index eb11a8084..1506d3637 100644
--- a/lib/hawat/blueprints/dnsr/forms.py
+++ b/lib/hawat/blueprints/dnsr/forms.py
@@ -9,7 +9,7 @@
 
 
 """
-This module contains custom external DNS database search form for Hawat.
+This module contains custom external DNS service search form for Hawat.
 """
 
 
@@ -24,7 +24,7 @@ from flask_babel import lazy_gettext
 
 class DnsrSearchForm(flask_wtf.FlaskForm):
     """
-    Class representing DNS search form.
+    Class representing DNS service search form.
     """
     search = wtforms.StringField(
         lazy_gettext('Search DNS:'),
diff --git a/lib/hawat/blueprints/dnsr/templates/dnsr/search.html b/lib/hawat/blueprints/dnsr/templates/dnsr/search.html
index 4b826d00e..fa1349efc 100644
--- a/lib/hawat/blueprints/dnsr/templates/dnsr/search.html
+++ b/lib/hawat/blueprints/dnsr/templates/dnsr/search.html
@@ -11,7 +11,7 @@
                             <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}">
                                 {{ search_form.search.label(class_='sr-only') }}
                                 <div class="input-group">
-                                    <div data-toggle="tooltip" class="input-group-addon" title="{{ _('Search DNS for:') }}">{{ get_icon('action-search') }}</div>
+                                    <div data-toggle="tooltip" class="input-group-addon" title="{{ _('Search DNS service for:') }}">{{ get_icon('action-search') }}</div>
                                     {{ search_form.search(class_='form-control', placeholder=_('Hostname, IPv4 or IPv6 address'), size='50') }}
                                 </div>
 
@@ -33,21 +33,32 @@
             <div class="row">
                 <div class="col-lg-12">
         {%- if search_result %}
-
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <h3 class="panel-title">{{ _('DNS lookup raw result') }}</h3>
-                        </div>
-                        <div class="panel-body">
-<pre>
-{{ search_result | pprint }}
-</pre>
-                        </div>
-                    </div>
+                    <table class="table table-striped table-bordered table-hover table-condensed table-responsive">
+                        <thead>
+                            <tr>
+                                <th></th>
+                                <th>
+                                    {{ _('Type') }}
+                                </th>
+                                <th>
+                                    {{ _('Value') }}
+                                </th>
+                            </tr>
+                        </thead>
+                        <tbody>
+            {%- for sritem in search_result %}
+                            <tr>
+                                <td>{{ loop.index }}</td>
+                                <td><span class="label label-default">{{ sritem['type'] }}</span></td>
+                                <td>{{ sritem['value'] }}</td>
+                            </tr>
+            {%- endfor %}
+                        </tbody>
+                    </table>
         {%- else %}
 
                     {%- call macros_site.render_alert('info', False) %}
-                    {{ _('There are no records for <strong>%(item_id)s</strong> in <em>DNS</em>.', item_id = search_item) | safe }}
+                    {{ _('There are no records for <strong>%(item_id)s</strong> in <em>DNS</em> service.', item_id = search_item) | safe }}
                     {%- endcall %}
 
         {%- endif %}
diff --git a/lib/hawat/blueprints/dnsr/templates/dnsr/spt_label_hostnames.html b/lib/hawat/blueprints/dnsr/templates/dnsr/spt_label_hostnames.html
new file mode 100644
index 000000000..84540ef8a
--- /dev/null
+++ b/lib/hawat/blueprints/dnsr/templates/dnsr/spt_label_hostnames.html
@@ -0,0 +1,36 @@
+{%- set content_id = get_uuid4() -%}
+<div class="popover-hover" role="button" data-popover-content="#po-{{ content_id }}">
+	<span class="label label-default">
+		{{ get_icon('module-dnsr') }} | {{ search_result[0]['value'] }}{% if search_result|length > 1 %} ({{ _('%(cnt)d more', cnt = (search_result|length -1))}}){% endif %}
+	</span>
+	<div id="po-{{ content_id }}" class="hidden">
+        <div class="popover top">
+            <div class="arrow"></div>
+            <h3 class="popover-title">
+                {{ get_icon('module-dnsr') }} {{ _('DNS') }} - {{ _('Hostname resolving') }}
+                <span class="pull-right">
+                    <span data-toggle="tooltip" title="{{ _('Query time: ') }} {{ get_timedelta(g.requeststart) }}">
+                        {{ get_icon('stopwatch') }}
+                    </span>
+                </span>
+            </h3>
+            <div class="popover-content">
+                <table class="table table-condensed">
+                    <tr>
+                        <th></th>
+                        <th>{{ _('Type') }}</th>
+                        <th>{{ _('Value') }}</th>
+                    </tr>
+    {%- for sritem in search_result %}
+                    <tr>
+                        <td>{{ loop.index }}</td>
+                        <td><span class="label label-default">{{ sritem['type'] }}</span></td>
+                        <td>{{ sritem['value'] }}</td>
+                    </tr>
+    {%- endfor %}
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
diff --git a/lib/hawat/blueprints/events/templates/events/search.html b/lib/hawat/blueprints/events/templates/events/search.html
index 6d182dde3..06bc4c3b7 100644
--- a/lib/hawat/blueprints/events/templates/events/search.html
+++ b/lib/hawat/blueprints/events/templates/events/search.html
@@ -12,20 +12,20 @@
                         <hr>
                         <form method="GET" class="form-horizontal" id="form-events-simple" action="{{ url_for(request.endpoint) }}">
                             <div>
-                                <div class="btn-toolbar" role="toolbar" aria-label="{{ gettext('Toolbar for enabling/disabling event search form fields.') }}">
+                                <div class="btn-toolbar" role="toolbar" aria-label="{{ _('Toolbar for enabling/disabling event search form fields.') }}">
                                     <div class="btn-group" role="group">
                                         <div class="btn-group" role="group">
                                             {#- Detect toggle button class #}
                                             {%- set tglbtncls = in_query_params(request.args, ['dt_from', 'dt_to'], ' btn-success active', ' btn-default', ' btn-success active') %}
                                             <button type="button" class="toggle-search-form btn{{ tglbtncls }}" id="switch-form-group-detecttime" data-toggle-form-group="form-group-detecttime" data-disable-form-group="form-group-storagetime" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                                {{ gettext('DetectTime') }}
+                                                {{ _('DetectTime') }}
                                             </button>
                                         </div>
                                         <div class="btn-group" role="group">
                                             {#- Detect toggle button class #}
                                             {%- set tglbtncls = in_query_params(request.args, ['st_from', 'st_to'], ' btn-success active', ' btn-default', ' btn-default') %}
                                             <button type="button" class="toggle-search-form btn{{ tglbtncls }}" id="switch-form-group-storagetime" data-toggle-form-group="form-group-storagetime" data-disable-form-group="form-group-detecttime" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                                {{ gettext('StorageTime') }}
+                                                {{ _('StorageTime') }}
                                             </button>
                                         </div>
                                     </div>
@@ -34,21 +34,21 @@
                                             {#- Detect toggle button class #}
                                             {%- set tglbtncls = in_query_params(request.args, ['source_addrs', 'source_ports', 'source_types'], ' btn-success active', ' btn-default', ' btn-success active') %}
                                             <button type="button" class="toggle-search-form btn{{ tglbtncls }}" id="switch-form-group-source" data-toggle-form-group="form-group-source" data-disable-form-group="form-group-host" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                                {{ gettext('Source') }}
+                                                {{ _('Source') }}
                                             </button>
                                         </div>
                                         <div class="btn-group" role="group">
                                             {#- Detect toggle button class #}
                                             {%- set tglbtncls = in_query_params(request.args, ['target_addrs', 'target_ports', 'target_types'], ' btn-success active', ' btn-default', ' btn-default') %}
                                             <button type="button" class="toggle-search-form btn{{ tglbtncls }}" id="switch-form-group-target" data-toggle-form-group="form-group-target" data-disable-form-group="form-group-host" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                                {{ gettext('Target') }}
+                                                {{ _('Target') }}
                                             </button>
                                         </div>
                                         <div class="btn-group" role="group">
                                             {#- Detect toggle button class #}
                                             {%- set tglbtncls = in_query_params(request.args, ['host_addrs', 'host_ports', 'host_types'], ' btn-success active', ' btn-default', ' btn-default') %}
                                             <button type="button" class="toggle-search-form btn{{ tglbtncls }}" id="switch-form-group-host" data-toggle-form-group="form-group-host" data-disable-form-group="form-group-source,form-group-target" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                                {{ gettext('Host') }}
+                                                {{ _('Host') }}
                                             </button>
                                         </div>
                                     </div>
@@ -56,14 +56,14 @@
                                         {#- Detect toggle button class #}
                                         {%- set tglbtncls = in_query_params(request.args, ['categories', 'severities', 'groups', 'protocols', 'description'], ' btn-success active', ' btn-default', ' btn-default') %}
                                         <button type="button" class="toggle-search-form btn{{ tglbtncls }}" id="switch-form-group-event" data-toggle-form-group="form-group-event-1,form-group-event-2" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                            {{ gettext('Event') }}
+                                            {{ _('Event') }}
                                         </button>
                                     </div>
                                     <div class="btn-group" role="group">
                                         {#- Detect toggle button class #}
                                         {%- set tglbtncls = in_query_params(request.args, ['detectors', 'detector_types'], ' btn-success active', ' btn-default', ' btn-default') %}
                                         <button type="button" class="toggle-search-form btn{{ tglbtncls }}" id="switch-form-group-detector" data-toggle-form-group="form-group-detector" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                            {{ gettext('Detector') }}
+                                            {{ _('Detector') }}
                                         </button>
                                     </div>
                                     {%- if permission_can('power') %}
@@ -71,7 +71,7 @@
                                         {#- Detect toggle button class #}
                                         {%- set tglbtnadm = in_query_params(request.args, ['inspection_errs', 'classes'], ' btn-success active', ' btn-default', ' btn-default') %}
                                         <button type="button" class="toggle-search-form btn{{ tglbtnadm }}" id="switch-form-group-admin" data-toggle-form-group="form-group-admin" data-toggle="button" aria-pressed="false" autocomplete="off">
-                                            {{ get_icon('role-admin') }} {{ gettext('Admin') }}
+                                            {{ get_icon('role-admin') }} {{ _('Admin') }}
                                         </button>
                                     </div>
                                     {%- endif %}
@@ -301,32 +301,32 @@
 
                             <hr>
 
-                            <div class="btn-toolbar" role="toolbar" aria-label="{{ gettext('Search menu') }}">
-                                <div class="btn-group" role="group" aria-label="{{ gettext('Search menu options') }}">
+                            <div class="btn-toolbar" role="toolbar" aria-label="{{ _('Search menu') }}">
+                                <div class="btn-group" role="group" aria-label="{{ _('Search menu options') }}">
                                     {{ g.search_form.submit(class_='btn btn-primary') }}
-                                    <a role="button" class="btn btn-default" href="{{ url_for(request.endpoint) }}">{{ gettext('Clear') }}</a>
+                                    <a role="button" class="btn btn-default" href="{{ url_for(request.endpoint) }}">{{ _('Clear') }}</a>
                                 </div>
                                 {%- if permission_can('power') %}
-                                <div class="btn-group" role="group" aria-label="{{ gettext('Admin search menu') }}">
+                                <div class="btn-group" role="group" aria-label="{{ _('Admin search menu') }}">
                                     <a data-toggle="dropdown" role="button" aria-haspopup="true" href="#" class="btn btn-default dropdown-toggle" aria-expanded="false">
-                                        <span data-toggle="tooltip" title="{{ gettext('Admin search menu') }}">
-                                            {{ get_icon('role-admin') }}<span class="hidden-sm"> {{ gettext('Admin') }}</span> <span class="caret"></span>
+                                        <span data-toggle="tooltip" title="{{ _('Admin search menu') }}">
+                                            {{ get_icon('role-admin') }}<span class="hidden-sm"> {{ _('Admin') }}</span> <span class="caret"></span>
                                         </span>
                                     </a>
                                     <ul class="dropdown-menu">
                                         <li>
-                                            <a href="{{ url_for(request.endpoint, inspection_errs = ['__ANY__'], submit = gettext('Search')) }}">
-                                                {{ get_endpoint_icon(request.endpoint) }} {{ gettext('Search for events with any inspection errors in event database') | safe }}
+                                            <a href="{{ url_for(request.endpoint, inspection_errs = ['__ANY__'], submit = _('Search')) }}">
+                                                {{ get_endpoint_icon(request.endpoint) }} {{ _('Search for events with any inspection errors in event database') | safe }}
                                             </a>
                                         </li>
                                         <li>
-                                            <a href="{{ url_for(request.endpoint, classes = ['__EMPTY__'], submit = gettext('Search')) }}">
-                                                {{ get_endpoint_icon(request.endpoint) }} {{ gettext('Search for events without any classification in event database') | safe }}
+                                            <a href="{{ url_for(request.endpoint, classes = ['__EMPTY__'], submit = _('Search')) }}">
+                                                {{ get_endpoint_icon(request.endpoint) }} {{ _('Search for events without any classification in event database') | safe }}
                                             </a>
                                         </li>
                                         <li>
-                                            <a href="{{ url_for(request.endpoint, severities = ['__EMPTY__'], submit = gettext('Search')) }}">
-                                                {{ get_endpoint_icon(request.endpoint) }} {{ gettext('Search for events without assigned severity in event database') | safe }}
+                                            <a href="{{ url_for(request.endpoint, severities = ['__EMPTY__'], submit = _('Search')) }}">
+                                                {{ get_endpoint_icon(request.endpoint) }} {{ _('Search for events without assigned severity in event database') | safe }}
                                             </a>
                                         </li>
                                     </ul>
@@ -358,7 +358,7 @@
     {%- if searched %}
         {%- if permission_can('power') %}
             {%- call macros_site.render_alert('info', False, 'role-admin') %}
-                <strong>{{ gettext('SQL query:') }}</strong> <code>{{ sqlquery }}</code>
+                <strong>{{ _('SQL query:') }}</strong> <code>{{ sqlquery }}</code>
             {%- endcall %}
         {%- endif %}
 
@@ -369,38 +369,38 @@
                 <thead>
                     <tr>
                         <!--
-                        <th data-toggle="tooltip" title="{{ gettext('Index') }}">
+                        <th data-toggle="tooltip" title="{{ _('Index') }}">
                             #
                         </th>
                         -->
                         {%- if in_query_params(request.args, ['st_from', 'st_to'], True, False, False) %}
-                        <th data-toggle="tooltip" title="{{ gettext('Event storage time') }}">
-                            {{ gettext('Stored at') }}
+                        <th data-toggle="tooltip" title="{{ _('Event storage time') }}">
+                            {{ _('Stored at') }}
                         </th>
                         {%- else %}
-                        <th data-toggle="tooltip" title="{{ gettext('Event detection time') }}">
-                            {{ gettext('Detected at') }}
+                        <th data-toggle="tooltip" title="{{ _('Event detection time') }}">
+                            {{ _('Detected at') }}
                         </th>
                         {%- endif %}
-                        <th data-toggle="tooltip" title="{{ gettext('Problem sources') }}" class="hidden-xs hidden-sm">
-                            {{ gettext('Sources') }}
+                        <th data-toggle="tooltip" title="{{ _('Problem sources') }}" class="hidden-xs hidden-sm">
+                            {{ _('Sources') }}
                         </th>
-                        <th data-toggle="tooltip" title="{{ gettext('Targets or victims') }}" class="hidden-xs hidden-sm hidden-md">
-                            {{ gettext('Targets') }}
+                        <th data-toggle="tooltip" title="{{ _('Targets or victims') }}" class="hidden-xs hidden-sm hidden-md">
+                            {{ _('Targets') }}
                         </th>
-                        <th data-toggle="tooltip" title="{{ gettext('Event severity') }}">
-                            {{ gettext('Severity') }}
+                        <th data-toggle="tooltip" title="{{ _('Event severity') }}">
+                            {{ _('Severity') }}
                         </th>
-                        <th data-toggle="tooltip" title="{{ gettext('Event categorization') }}">
-                            {{ gettext('Categorization') }}
+                        <th data-toggle="tooltip" title="{{ _('Event categorization') }}">
+                            {{ _('Categorization') }}
                         </th>
-                        <th data-toggle="tooltip" title="{{ gettext('Event detector') }}">
-                            {{ gettext('Detector') }}
+                        <th data-toggle="tooltip" title="{{ _('Event detector') }}">
+                            {{ _('Detector') }}
                         </th>
-                        <th data-toggle="tooltip" title="{{ gettext('Abuse groups') }}">
-                            {{ gettext('Groups') }}
+                        <th data-toggle="tooltip" title="{{ _('Abuse groups') }}">
+                            {{ _('Groups') }}
                         </th>
-                        <th data-toggle="tooltip" title="{{ gettext('Contextual item actions') }}">
+                        <th data-toggle="tooltip" title="{{ _('Contextual item actions') }}">
                             {{ get_icon('actions') }}
                         </th>
                     </tr>
@@ -452,7 +452,7 @@
                                 )
                             }}
                             {%- else %}
-                            <span data-toggle="tooltip" title="{{ gettext('-- unassigned --') }}">
+                            <span data-toggle="tooltip" title="{{ _('-- unassigned --') }}">
                                 {{ get_icon('unassigned') }}
                             </span>
                             {%- endif %}
@@ -479,7 +479,7 @@
                                 )
                             }}
                             {%- else %}
-                            <span data-toggle="tooltip" title="{{ gettext('-- unassigned --') }}">
+                            <span data-toggle="tooltip" title="{{ _('-- unassigned --') }}">
                                 {{ get_icon('unassigned') }}
                             </span>
                             {%- endif %}
@@ -512,7 +512,7 @@
         {%- else %}
 
             {%- call macros_site.render_alert('warning', False) %}
-                {{ gettext('No data matches your search criteria.') }}
+                {{ _('No data matches your search criteria.') }}
             {%- endcall %}
 
         {%- endif %}
diff --git a/lib/hawat/blueprints/events/templates/events/show.html b/lib/hawat/blueprints/events/templates/events/show.html
index d255a79c9..2d891c042 100644
--- a/lib/hawat/blueprints/events/templates/events/show.html
+++ b/lib/hawat/blueprints/events/templates/events/show.html
@@ -1,7 +1,7 @@
 {%- extends "_layout.html" %}
 
 {%- block content %}
-
+{%- set ads_limits = get_limit_counter() %}
             <div class="row">
                 <div class="col-lg-12">
 
@@ -33,15 +33,6 @@
                         </small>
                     </p>
                     {%- endif %}
-                    <p>
-                        <small>
-                            <strong>{{ _('Event severity') }}:</strong> {{ macros_site.render_event_label_severity(item, True) }}
-                            &nbsp;|&nbsp;
-                            <strong>{{ _('Event detected') }}:</strong> {{ babel_format_datetime(item.get_detect_time()) }} ({{ macros_site.render_info_timeinterval(item.get_detect_time(), current_datetime_utc) }})
-                            &nbsp;|&nbsp;
-                            <strong>{{ _('Event stored') }}:</strong> {{ babel_format_datetime(item.get_storage_time()) }} ({{ macros_site.render_info_timeinterval(item.get_storage_time(), current_datetime_utc) }})
-                        </small>
-                    </p>
                 </div><!-- /.col-lg-12 -->
             </div><!-- /.row -->
 
@@ -265,31 +256,100 @@
                             <ul class="list-group">
                                     {%- for subitem in tmpval %}
                                 <li class="list-group-item">
+                                    <table class="table table-condensed table-event-detail-node">
                                         {%- if 'IP4' in subitem %}
-                                    <div>
-                                        <span class="label label-default"><strong>{{ _('IP4') }}:</strong></span> {{ macros_site.render_widget_csag_address(subitem['IP4'], separate_dropdown = True, item_limit = search_widget_item_limit) }}
-                                    </div>
+                                        <tr style="border-top: 0">
+                                            <th>
+                                                {{ _('IP4') }}:
+                                            </th>
+                                            <td>
+                                            {%- for itemaddr in subitem['IP4'] %}
+                                                {%- if loop.index < search_widget_item_limit -%}
+                                                <div>
+                                                    {{ macros_site.render_widget_csag_address(
+                                                        [itemaddr],
+                                                        separate_dropdown = True
+                                                    ) }}
+                                                    {%- if ads_limits.count_and_check('{}.{}'.format(node_type[0], 'IP4')) %}
+                                                    <div class="object-additional-data" data-object-type="ip4" data-object-name="{{ itemaddr }}"></div>
+                                                    {%- else %}
+                                                    <span data-toggle="tooltip" title="{{ _('Additional data service limit for objects of this type was reached, please use manual search options.') }}">{{ get_icon('alert-info') }}</span>
+                                                    {%- endif %}
+                                                </div>
+                                                {%- elif loop.index0 == search_widget_item_limit %}
+                                                <span class="underlined-tooltip" data-toggle="tooltip" title="{{ _('Please download raw message to view full list.') }}">
+                                                    ({{ _('%(count)s more', count = loop.length - loop.index0) }})
+                                                </span>
+                                                {%- endif %}
+                                            {%- endfor %}
+                                            </td>
+                                        </tr>
                                         {%- endif %}
                                         {%- if 'IP6' in subitem %}
-                                    <div>
-                                        <span class="label label-default"><strong>{{ _('IP6') }}:</strong></span> {{ macros_site.render_widget_csag_address(subitem['IP6'], separate_dropdown = True, item_limit = search_widget_item_limit) }}
-                                    </div>
+                                        <tr>
+                                            <th>
+                                                {{ _('IP6') }}:
+                                            </th>
+                                            <td>
+                                            {%- for itemaddr in subitem['IP6'] %}
+                                                {%- if loop.index < search_widget_item_limit -%}
+                                                <div>
+                                                    {{ macros_site.render_widget_csag_address(
+                                                        [itemaddr],
+                                                        separate_dropdown = True
+                                                    ) }}
+                                                    {%- if ads_limits.count_and_check('{}.{}'.format(node_type[0], 'IP6')) %}
+                                                    <div class="object-additional-data" data-object-type="ip6" data-object-name="{{ itemaddr }}"></div>
+                                                    {%- else %}
+                                                    <span data-toggle="tooltip" title="{{ _('Additional data service limit for objects of this type was reached, please use manual search options.') }}">{{ get_icon('alert-info') }}</span>
+                                                    {%- endif %}
+                                                </div>
+                                                {%- elif loop.index0 == search_widget_item_limit %}
+                                                <span class="underlined-tooltip" data-toggle="tooltip" title="{{ _('Please download raw message to view full list.') }}">
+                                                    ({{ _('%(count)s more', count = loop.length - loop.index0) }})
+                                                </span>
+                                                {%- endif %}
+                                            {%- endfor %}
+                                            </td>
+                                        </tr>
                                         {%- endif %}
                                         {%- if 'Port' in subitem %}
-                                    <div>
-                                        <span class="label label-default"><strong>{{ _('Port') }}:</strong></span> {{ macros_site.render_widget_csag_port(subitem['Port'], separate_dropdown = True, item_limit = search_widget_item_limit) }}
-                                    </div>
+                                        <tr>
+                                            <th>
+                                                {{ _('Port') }}:
+                                            </th>
+                                            <td>
+                                                <div>
+                                                    {{ macros_site.render_widget_csag_port(subitem['Port'], separate_dropdown = True, item_limit = search_widget_item_limit) }}
+                                                </div>
+                                            </td>
+                                        </tr>
                                         {%- endif %}
                                         {%- if 'Proto' in subitem %}
-                                    <div>
-                                        <span class="label label-default"><strong>{{ _('Proto') }}:</strong></span> {{ macros_site.render_widget_csag_protocol(subitem['Proto'], separate_dropdown = True, item_limit = search_widget_item_limit) }}
-                                    </div>
+                                        <tr>
+                                            <th>
+                                                {{ _('Proto') }}:
+                                            </th>
+                                            <td>
+                                                <div>
+                                                    {{ macros_site.render_widget_csag_protocol(subitem['Proto'], separate_dropdown = True, item_limit = search_widget_item_limit) }}
+                                                </div>
+                                            </td>
+                                        </tr>
                                         {%- endif %}
                                         {%- if 'Type' in subitem %}
-                                    <div>
-                                        <span class="label label-default"><strong>{{ _('Type') }}:</strong></span> {{ macros_site.render_widget_csag_hosttype(subitem['Type'], separate_dropdown = True) }}
-                                    </div>
+                                        <tr>
+                                            <th>
+                                                {{ _('Type') }}:
+                                            </th>
+                                            <td>
+                                                <div>
+                                                    {{ macros_site.render_widget_csag_hosttype(subitem['Type'], separate_dropdown = True) }}
+                                                </div>
+                                            </td>
+                                        </tr>
                                         {%- endif %}
+                                    </table>
                                 </li>
                                     {%- endfor %}
                             </ul>
@@ -303,15 +363,38 @@
                                 {%- for subitem in tmpval | reverse %}
                                 <li class="list-group-item{% if loop.first %} list-group-item-info{% endif %}">
                                     {%- if not loop.first %}<small>{%- endif %}
+                                    <table class="table table-condensed table-event-detail-node">
                                         {%- if 'Name' in subitem %}
-                                        <span class="label label-default"><strong>{{ _('Name') }}:</strong></span> {% if loop.first %}{{ macros_site.render_widget_csag_detector([subitem['Name']], separate_dropdown = True) }}{% else %}{{ subitem['Name'] }}{%- endif %}
+                                        <tr style="border-top: 0">
+                                            <th>
+                                                {{ _('Name') }}:
+                                            </th>
+                                            <td>
+                                                {% if loop.first %}{{ macros_site.render_widget_csag_detector([subitem['Name']], separate_dropdown = True) }}{% else %}{{ subitem['Name'] }}{%- endif %}
+                                            </td>
+                                        </tr>
                                         {%- endif %}
                                         {%- if 'SW' in subitem %}
-                                        <span class="label label-default"><strong>{{ _('SW') }}:</strong></span> {{ subitem['SW'] | join(', ') }}
+                                        <tr>
+                                            <th>
+                                                {{ _('Software') }}:
+                                            </th>
+                                            <td>
+                                                {{ subitem['SW'] | join(', ') }}
+                                            </td>
+                                        </tr>
                                         {%- endif %}
                                         {%- if 'Type' in subitem %}
-                                        <span class="label label-default"><strong>{{ _('Type') }}:</strong></span> {{ macros_site.render_widget_csag_detectortype(subitem['Type'], separate_dropdown = True) }}
+                                        <tr>
+                                            <th>
+                                                {{ _('Type') }}:
+                                            </th>
+                                            <td>
+                                                {{ macros_site.render_widget_csag_detectortype(subitem['Type'], separate_dropdown = True) }}
+                                            </td>
+                                        </tr>
                                         {%- endif %}
+                                    </table>
                                     {%- if not loop.first %}</small>{%- endif %}
                                 </li>
                                 {%- endfor %}
diff --git a/lib/hawat/blueprints/geoip/__init__.py b/lib/hawat/blueprints/geoip/__init__.py
index 845c8908c..23245884f 100644
--- a/lib/hawat/blueprints/geoip/__init__.py
+++ b/lib/hawat/blueprints/geoip/__init__.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+7#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 #-------------------------------------------------------------------------------
 # This file is part of Mentat system (https://mentat.cesnet.cz/).
@@ -22,10 +22,27 @@ Provided endpoints
 ------------------
 
 ``/geoip/search``
-    Page providing search form and displaying search results.
+    Endpoint providing search form for querying internal IP geolocation service and
+    formating result as HTML page.
 
     * *Authentication:* login required
     * *Methods:* ``GET``
+
+``/api/geoip/search``
+    Endpoint providing API search form for querying internal IP geolocation service
+    and formating result as JSON document.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
+
+``/snippet/geoip/search``
+    Endpoint providing API search form for querying internal IP geolocation service
+    and formating result as JSON document containing HTML snippets.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
 """
 
 
@@ -47,7 +64,7 @@ from mentat.const import tr_
 import hawat.const
 import hawat.db
 import hawat.acl
-from hawat.base import HTMLMixin, RenderableView, HawatBlueprint, URLParamsBuilder
+from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, HawatBlueprint, URLParamsBuilder
 from hawat.blueprints.geoip.forms import GeoipSearchForm
 
 
@@ -55,33 +72,26 @@ BLUEPRINT_NAME = 'geoip'
 """Name of the blueprint as module global constant."""
 
 
-class SearchView(HTMLMixin, RenderableView):
+class AbstractSearchView(RenderableView):  # pylint: disable=locally-disabled,abstract-method
     """
-    Application view providing search form for internal IP geolocation service
-    and appropriate result page.
+    Application view providing base search capabilities for internal IP geolocation
+    service.
 
     The geolocation is implemented using :py:mod:`mentat.services.geoip` module.
     """
-    methods = ['GET']
-
     authentication = True
 
     authorization = [hawat.acl.PERMISSION_ANY]
 
-    @classmethod
-    def get_view_name(cls):
-        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
-        return 'search'
-
     @classmethod
     def get_menu_title(cls, item = None):
         """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`."""
-        return lazy_gettext('Internal geoip')
+        return lazy_gettext('Search GeoIP')
 
     @classmethod
     def get_view_title(cls, item = None):
         """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`."""
-        return lazy_gettext('Search internal geoip database')
+        return lazy_gettext('Search GeoIP')
 
     #---------------------------------------------------------------------------
 
@@ -91,24 +101,95 @@ class SearchView(HTMLMixin, RenderableView):
         Will be called by the *Flask* framework to service the request.
         """
         form = GeoipSearchForm(flask.request.args, meta = {'csrf': False})
-
         if hawat.const.HAWAT_FORM_ACTION_SUBMIT in flask.request.args:
             if form.validate():
                 form_data = form.data
-                geoip_manager = mentat.services.geoip.GeoipServiceManager(flask.current_app.mconfig)
-                geoip_service = geoip_manager.service()
+                geoip_service = mentat.services.geoip.service()
                 self.response_context.update(
-                    search_item   = form.search.data,
-                    search_result = geoip_service.lookup(form.search.data),
-                    form_data     = form_data
+                    search_item = form.search.data,
+                    form_data   = form_data
                 )
 
+                try:
+                    self.response_context.update(
+                        search_result = geoip_service.lookup(
+                            form.search.data
+                        )
+                    )
+                except Exception as exc:
+                    self.flash(str(exc), category = 'error')
+
         self.response_context.update(
             search_form  = form,
             request_args = flask.request.args,
         )
         return self.generate_response()
 
+
+class SearchView(HTMLMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for searching IP geolocation service and presenting the
+    results in the form of HTML page.
+    """
+    methods = ['GET']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'search'
+
+
+class APISearchView(AJAXMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for searching IP geolocation service and presenting the
+    results in the form of JSON document.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'apisearch'
+
+
+class SnippetSearchView(SnippetMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for searching IP geolocation service and presenting the
+    results in the form of JSON document containing ready to use HTML page snippets.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'sptsearch'
+
+    def process_response_context(self):
+        """*Implementation* of :py:func:`hawat.base.SnippetMixin.process_response_context`."""
+        super().process_response_context()
+
+        # Make latter expressions shorter
+        res = self.response_context.get('search_result', False)
+        if res:
+            if res.get('city', False) and res['city'].get('ctr_code', False):
+                self.response_context.setdefault(
+                    self.KW_RESP_SNIPPETS,
+                    {}
+                )['label_city'] = flask.render_template(
+                    'geoip/spt_label_city.html',
+                    search_result = res
+                )
+            if res.get('asn', False) and (res['asn'].get('asn', False) or res['asn'].get('org', False)):
+                self.response_context.setdefault(
+                    self.KW_RESP_SNIPPETS,
+                    {}
+                )['label_asn'] = flask.render_template(
+                    'geoip/spt_label_asn.html',
+                    search_result = res
+                )
+        return self.response_context
+
+
 #-------------------------------------------------------------------------------
 
 
@@ -120,7 +201,7 @@ class GeoipBlueprint(HawatBlueprint):
     @classmethod
     def get_module_title(cls):
         """*Implementation* of :py:func:`hawat.base.HawatBlueprint.get_module_title`."""
-        return lazy_gettext('Internal geoip pluggable module')
+        return lazy_gettext('IP geolocation pluggable module')
 
     def register_app(self, app):
         """
@@ -132,22 +213,38 @@ class GeoipBlueprint(HawatBlueprint):
 
         :param hawat.base.HawatApp app: Flask application to be customize.
         """
+        mentat.services.geoip.init(app.mconfig)
+
         app.menu_main.add_entry(
             'view',
             'more.{}'.format(BLUEPRINT_NAME),
             position = 10,
-            group = lazy_gettext('Tools'),
+            group = lazy_gettext('Data services'),
             view = SearchView
         )
 
         # Register context actions provided by this module.
         app.set_csag(
             hawat.const.HAWAT_CSAG_ADDRESS,
-            tr_('Search for address <strong>%(name)s</strong> in internal IP geolocation database'),
+            tr_('Search for address <strong>%(name)s</strong> in IP geolocation service'),
             SearchView,
             URLParamsBuilder({'submit': tr_('Search')}).add_rule('search')
         )
 
+        # Register additional object data services provided by this module.
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP4,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search'),
+            ['label_city', 'label_asn']
+        )
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP6,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search'),
+            ['label_city', 'label_asn']
+        )
+
 
 #-------------------------------------------------------------------------------
 
@@ -161,10 +258,11 @@ def get_blueprint():
     hbp = GeoipBlueprint(
         BLUEPRINT_NAME,
         __name__,
-        template_folder = 'templates',
-        url_prefix = '/{}'.format(BLUEPRINT_NAME)
+        template_folder = 'templates'
     )
 
-    hbp.register_view_class(SearchView, '/search')
+    hbp.register_view_class(SearchView,        '/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(APISearchView,     '/api/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME))
 
     return hbp
diff --git a/lib/hawat/blueprints/geoip/forms.py b/lib/hawat/blueprints/geoip/forms.py
index ff8eab15d..ef5984718 100644
--- a/lib/hawat/blueprints/geoip/forms.py
+++ b/lib/hawat/blueprints/geoip/forms.py
@@ -29,7 +29,7 @@ class GeoipSearchForm(flask_wtf.FlaskForm):
     Class representing IP geolocation search form.
     """
     search = wtforms.StringField(
-        lazy_gettext('Search internal geoip:'),
+        lazy_gettext('Search GeoIP:'),
         validators = [
             wtforms.validators.DataRequired(),
             hawat.forms.check_ip_record
diff --git a/lib/hawat/blueprints/geoip/templates/geoip/search.html b/lib/hawat/blueprints/geoip/templates/geoip/search.html
index 34e4b6430..ce198ea60 100644
--- a/lib/hawat/blueprints/geoip/templates/geoip/search.html
+++ b/lib/hawat/blueprints/geoip/templates/geoip/search.html
@@ -11,13 +11,13 @@
                             <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}">
                                 {{ search_form.search.label(class_='sr-only') }}
                                 <div class="input-group">
-                                    <div data-toggle="tooltip" class="input-group-addon" title="{{ gettext('Search local IP geolocation database for:') }}">{{ get_icon('action-search') }}</div>
-                                    {{ search_form.search(class_='form-control', placeholder=gettext('IPv4 or IPv6 address'), size='50') }}
+                                    <div data-toggle="tooltip" class="input-group-addon" title="{{ _('Search local IP geolocation database for:') }}">{{ get_icon('action-search') }}</div>
+                                    {{ search_form.search(class_='form-control', placeholder=_('IPv4 or IPv6 address'), size='50') }}
                                 </div>
 
                                 <div class="btn-group" role="group">
                                     {{ search_form.submit(class_='btn btn-primary') }}
-                                    <a role="button" class="btn btn-default" href="{{ url_for('geoip.search') }}">{{ gettext('Clear') }}</a>
+                                    <a role="button" class="btn btn-default" href="{{ url_for('geoip.search') }}">{{ _('Clear') }}</a>
                                 </div>
                             </div>
                             {{ macros_form.render_form_errors(search_form.search.errors) }}
@@ -30,25 +30,23 @@
     {%- if search_item %}
             <div class="row">
                 <div class="col-lg-12">
-        {%- if search_result %}
-
             {%- if search_result['asn'] %}
 
                     <div class="panel panel-default">
                         <div class="panel-heading">
-                            <h3 class="panel-title">{{ gettext('ASN lookup') }}</h3>
+                            <h3 class="panel-title">{{ _('ASN lookup') }}</h3>
                         </div>
                         <table class="table">
                             <tbody>
                                 {%- if 'asn' in search_result['asn'] %}
                                 <tr>
-                                    <th>{{ gettext('ASN number:') }}</th>
+                                    <th>{{ _('ASN number:') }}</th>
                                     <td>{{ search_result['asn']['asn'] }}</td>
                                 </tr>
                                 {%- endif %}
                                 {%- if 'org' in search_result['asn'] %}
                                 <tr>
-                                    <th>{{ gettext('Organization:') }}</th>
+                                    <th>{{ _('Organization:') }}</th>
                                     <td>{{ search_result['asn']['org'] }}</td>
                                 </tr>
                                 {%- endif %}
@@ -58,7 +56,7 @@
             {%- else %}
 
                     {%- call macros_site.render_alert('info', False) %}
-                    {{ gettext('There are no records for <strong>%(item_id)s</strong> in <em>ASN</em> database.', item_id = search_item) | safe }}
+                    {{ _('There are no records for <strong>%(item_id)s</strong> in <em>ASN</em> database.', item_id = search_item) | safe }}
                     {%- endcall %}
 
             {%- endif %}
@@ -66,37 +64,37 @@
 
                     <div class="panel panel-default">
                         <div class="panel-heading">
-                            <h3 class="panel-title">{{ gettext('City lookup') }}</h3>
+                            <h3 class="panel-title">{{ _('City lookup') }}</h3>
                         </div>
                         <table class="table">
                             <tbody>
                                 {%- if 'cnt_name' in search_result['city'] and search_result['city']['cnt_name'] %}
                                 <tr>
-                                    <th>{{ gettext('Continent:') }}</th>
+                                    <th>{{ _('Continent:') }}</th>
                                     <td>{{ search_result['city']['cnt_name'] }} ({{ search_result['city']['cnt_code'] | upper }})</td>
                                 </tr>
                                 {%- endif %}
                                 {%- if 'ctr_name' in search_result['city'] and search_result['city']['ctr_name'] %}
                                 <tr>
-                                    <th>{{ gettext('Country:') }}</th>
+                                    <th>{{ _('Country:') }}</th>
                                     <td>{{ search_result['city']['ctr_name'] }} ({{ search_result['city']['ctr_code'] | upper }}, {{ get_country_flag(search_result['city']['ctr_code']) }})</td>
                                 </tr>
                                 {%- endif %}
                                 {%- if 'cty_name' in search_result['city'] and search_result['city']['cty_name'] %}
                                 <tr>
-                                    <th>{{ gettext('City:') }}</th>
+                                    <th>{{ _('City:') }}</th>
                                     <td>{{ search_result['city']['cty_name'] }}</td>
                                 </tr>
                                 {%- endif %}
                                 {%- if 'timezone' in search_result['city'] %}
                                 <tr>
-                                    <th>{{ gettext('Timezone:') }}</th>
+                                    <th>{{ _('Timezone:') }}</th>
                                     <td>{{ search_result['city']['timezone'] }}</td>
                                 </tr>
                                 {%- endif %}
                                 {%- if 'latitude' in search_result['city'] and 'longitude' in search_result['city'] %}
                                 <tr>
-                                    <th>{{ gettext('Coordinates:') }}</th>
+                                    <th>{{ _('Coordinates:') }}</th>
                                     <td>@{{ search_result['city']['latitude'] }},{{ search_result['city']['longitude'] }}</td>
                                 </tr>
                                 {%- endif %}
@@ -105,18 +103,17 @@
                     </div>
                     <p>
                         <small>
-                            {{ gettext('Search powered by <a href="https://dev.maxmind.com/geoip/geoip2/geolite2/">GeoLite2</a> created by <a href="http://www.maxmind.com/">MaxMind</a>.') | safe }}
+                            {{ _('Search powered by <a href="https://dev.maxmind.com/geoip/geoip2/geolite2/">GeoLite2</a> created by <a href="http://www.maxmind.com/">MaxMind</a>.') | safe }}
                         </small>
                     </p>
 
             {%- else %}
 
                     {%- call macros_site.render_alert('info', False) %}
-                    {{ gettext('There are no records for <strong>%(item_id)s</strong> in <em>City</em> database.', item_id = search_item) | safe }}
+                    {{ _('There are no records for <strong>%(item_id)s</strong> in <em>City</em> database.', item_id = search_item) | safe }}
                     {%- endcall %}
 
             {%- endif %}
-        {%- endif %}
 
                 </div> <!-- .col-lg-12 -->
             </div>  <!-- .row -->
diff --git a/lib/hawat/blueprints/geoip/templates/geoip/spt_label_asn.html b/lib/hawat/blueprints/geoip/templates/geoip/spt_label_asn.html
new file mode 100644
index 000000000..710a6a436
--- /dev/null
+++ b/lib/hawat/blueprints/geoip/templates/geoip/spt_label_asn.html
@@ -0,0 +1,35 @@
+{%- set content_id = get_uuid4() -%}
+<div class="popover-hover" role="button" data-popover-content="#po-{{ content_id }}">
+    <span class="label label-default">
+        {{ get_icon('module-geoip') }} | {% if search_result['asn']['asn'] %}<strong>{{ search_result['asn']['asn'] }}</strong>{% elif search_result['asn']['org'] %}{{ search_result['asn']['org'] }}{% endif %}
+    </span>
+    <div id="po-{{ content_id }}" class="hidden">
+        <div class="popover top">
+            <div class="arrow"></div>
+            <h3 class="popover-title">
+                {{ get_icon('module-geoip') }} {{ _('GeoIP') }} - {{ _('ASN resolving') }}
+                <span class="pull-right">
+                    <span data-toggle="tooltip" title="{{ _('Query time: ') }} {{ get_timedelta(g.requeststart) }}">
+                        {{ get_icon('stopwatch') }}
+                    </span>
+                </span>
+            </h3>
+            <div class="popover-content">
+                <table class="table table-condensed">
+    {%- if search_result['asn']['asn'] %}
+                    <tr>
+                        <th>{{ _('ASN') }}</th>
+                        <td>{{ search_result['asn']['asn'] }}</td>
+                    </tr>
+    {%- endif %}
+    {%- if search_result['asn']['org'] %}
+                    <tr>
+                        <th>{{ _('Organization') }}</th>
+                        <td>{{ search_result['asn']['org'] }}</td>
+                    </tr>
+    {%- endif %}
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/lib/hawat/blueprints/geoip/templates/geoip/spt_label_city.html b/lib/hawat/blueprints/geoip/templates/geoip/spt_label_city.html
new file mode 100644
index 000000000..5cd3a0a9c
--- /dev/null
+++ b/lib/hawat/blueprints/geoip/templates/geoip/spt_label_city.html
@@ -0,0 +1,56 @@
+{%- import '_macros_site.html' as macros_site with context -%}
+{%- set content_id = get_uuid4() -%}
+<div class="popover-hover" role="button" data-popover-content="#po-{{ content_id }}">
+    <span class="label label-default">
+        {{ get_icon('module-geoip') }} | {{ search_result['city']['ctr_code'] | upper }} {{ get_country_flag(search_result['city']['ctr_code']) }}
+    </span>
+    <div id="po-{{ content_id }}" class="hidden">
+        <div class="popover top">
+            <div class="arrow"></div>
+            <h3 class="popover-title">
+                {{ get_icon('module-geoip') }} {{ _('GeoIP') }} - {{ _('City resolving') }}
+                <span class="pull-right">
+                    <span data-toggle="tooltip" title="{{ _('Query time: ') }} {{ get_timedelta(g.requeststart) }}">
+                        {{ get_icon('stopwatch') }}
+                    </span>
+                </span>
+            </h3>
+            <div class="popover-content">
+                <table class="table table-condensed">
+                    <tbody>
+    {%- if 'cnt_name' in search_result['city'] and search_result['city']['cnt_name'] %}
+                        <tr>
+                            <th>{{ _('Continent:') }}</th>
+                            <td>{{ search_result['city']['cnt_name'] }} ({{ search_result['city']['cnt_code'] | upper }})</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'ctr_name' in search_result['city'] and search_result['city']['ctr_name'] %}
+                        <tr>
+                            <th>{{ _('Country:') }}</th>
+                            <td>{{ search_result['city']['ctr_name'] }} ({{ search_result['city']['ctr_code'] | upper }}, {{ get_country_flag(search_result['city']['ctr_code']) }})</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'cty_name' in search_result['city'] and search_result['city']['cty_name'] %}
+                        <tr>
+                            <th>{{ _('City:') }}</th>
+                            <td>{{ search_result['city']['cty_name'] }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'timezone' in search_result['city'] %}
+                        <tr>
+                            <th>{{ _('Timezone:') }}</th>
+                            <td>{{ search_result['city']['timezone'] }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'latitude' in search_result['city'] and 'longitude' in search_result['city'] %}
+                        <tr>
+                            <th>{{ _('Coordinates:') }}</th>
+                            <td>@{{ search_result['city']['latitude'] }},{{ search_result['city']['longitude'] }}</td>
+                        </tr>
+    {%- endif %}
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/lib/hawat/blueprints/nerd/__init__.py b/lib/hawat/blueprints/nerd/__init__.py
index 3b66e1e60..b15450037 100644
--- a/lib/hawat/blueprints/nerd/__init__.py
+++ b/lib/hawat/blueprints/nerd/__init__.py
@@ -20,10 +20,27 @@ Provided endpoints
 ------------------
 
 ``/nerd/search``
-    Page providing search form and displaying search results.
+    Endpoint providing search form for querying external NERD service and formating
+    result as HTML page.
 
     * *Authentication:* login required
     * *Methods:* ``GET``
+
+``/api/nerd/search``
+    Endpoint providing API search form for querying external NERD service
+    and formating result as JSON document.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
+
+``/snippet/nerd/search``
+    Endpoint providing API search form for querying external NERD service
+    and formating result as JSON document containing HTML snippets.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
 """
 
 
@@ -45,7 +62,7 @@ from mentat.const import tr_
 import hawat.const
 import hawat.db
 import hawat.acl
-from hawat.base import HTMLMixin, AJAXMixin, RenderableView, HawatBlueprint, URLParamsBuilder
+from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, HawatBlueprint, URLParamsBuilder
 from hawat.blueprints.nerd.forms import NerdSearchForm
 
 
@@ -53,12 +70,11 @@ BLUEPRINT_NAME = 'nerd'
 """Name of the blueprint as module global constant."""
 
 
-class AbstractSearchView(RenderableView):
+class AbstractSearchView(RenderableView):  # pylint: disable=locally-disabled,abstract-method
     """
-    Application view providing search form for internal IP geolocation service
-    and appropriate result page.
+    Application view providing base search capabilities for external NERD service.
 
-    The geolocation is implemented using :py:mod:`mentat.services.geoip` module.
+    The querying is implemented using :py:mod:`mentat.services.nerd` module.
     """
     authentication = True
 
@@ -67,12 +83,12 @@ class AbstractSearchView(RenderableView):
     @classmethod
     def get_menu_title(cls, item = None):
         """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`."""
-        return lazy_gettext('NERD database')
+        return lazy_gettext('Search NERD')
 
     @classmethod
     def get_view_title(cls, item = None):
         """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`."""
-        return lazy_gettext('Search NERD database')
+        return lazy_gettext('Search NERD')
 
     #---------------------------------------------------------------------------
 
@@ -88,23 +104,28 @@ class AbstractSearchView(RenderableView):
                 form_data = form.data
                 nerd_service = mentat.services.nerd.service()
                 self.response_context.update(
-                    search_item   = form.search.data,
-                    search_result = nerd_service.lookup_ip(form.search.data),
-                    search_url    = nerd_service.get_url_lookup_ip(form.search.data),
-                    form_data     = form_data
+                    search_item = form.search.data,
+                    search_url  = nerd_service.get_url_lookup_ip(form.search.data),
+                    form_data   = form_data
                 )
+                try:
+                    self.response_context.update(
+                        search_result = nerd_service.lookup_ip(form.search.data)
+                    )
+                except Exception as exc:
+                    self.flash(str(exc), category = 'error')
 
         self.response_context.update(
             search_form  = form,
-            request_args = flask.request.args,
+            request_args = flask.request.args
         )
         return self.generate_response()
 
 
 class SearchView(HTMLMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
     """
-    View responsible for querying `IDEA <https://idea.cesnet.cz/en/index>`__
-    event database and presenting the results in the form of HTML page.
+    View responsible for querying external NERD service and presenting the results
+    in the form of HTML page.
     """
     methods = ['GET']
 
@@ -116,8 +137,8 @@ class SearchView(HTMLMixin, AbstractSearchView):  # pylint: disable=locally-disa
 
 class APISearchView(AJAXMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
     """
-    View responsible for querying `IDEA <https://idea.cesnet.cz/en/index>`__
-    event database and presenting the results in the form of JSON document.
+    View responsible for querying external NERD service and presenting the results
+    in the form of JSON document.
     """
     methods = ['GET','POST']
 
@@ -127,6 +148,35 @@ class APISearchView(AJAXMixin, AbstractSearchView):  # pylint: disable=locally-d
         return 'apisearch'
 
 
+class SnippetSearchView(SnippetMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying DNS service and presenting the results in the
+    form of JSON document containg ready to use HTML page snippets.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'sptsearch'
+
+    def process_response_context(self):
+        """*Implementation* of :py:func:`hawat.base.AJAXMixin.process_response_context`."""
+        super().process_response_context()
+
+        # Make latter expressions shorter
+        res = self.response_context.get('search_result', False)
+        if res:
+            self.response_context.setdefault(
+                self.KW_RESP_SNIPPETS,
+                {}
+            )['label_reputation'] = flask.render_template(
+                'nerd/spt_label_reputation.html',
+                search_result = res
+            )
+        return self.response_context
+
+
 #-------------------------------------------------------------------------------
 
 
@@ -156,26 +206,32 @@ class NerdBlueprint(HawatBlueprint):
         app.menu_main.add_entry(
             'view',
             'more.{}'.format(BLUEPRINT_NAME),
-            position = 30,
+            position = 50,
             view = SearchView
         )
 
         # Register context actions provided by this module.
         app.set_csag(
             hawat.const.HAWAT_CSAG_ADDRESS,
-            tr_('Search for address <strong>%(name)s</strong> locally in NERD database'),
+            tr_('Search for address <strong>%(name)s</strong> locally in NERD service'),
             SearchView,
             URLParamsBuilder({'submit': tr_('Search')}).add_rule('search')
         )
-
-        # Register context actions provided by this module.
         app.set_csag_url(
             hawat.const.HAWAT_CSAG_ADDRESS,
-            tr_('Search for address <strong>%(name)s</strong> externally in NERD database'),
+            tr_('Search for address <strong>%(name)s</strong> externally in NERD service'),
             SearchView.get_menu_icon(),
             mentat.services.nerd.service().get_url_lookup_ip
         )
 
+        # Register additional object data services provided by this module.
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP4,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search'),
+            ['label_reputation']
+        )
+
 
 #-------------------------------------------------------------------------------
 
@@ -192,7 +248,8 @@ def get_blueprint():
         template_folder = 'templates'
     )
 
-    hbp.register_view_class(SearchView,    '/{}/search'.format(BLUEPRINT_NAME))
-    hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(SearchView,        '/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(APISearchView,     '/api/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME))
 
     return hbp
diff --git a/lib/hawat/blueprints/nerd/forms.py b/lib/hawat/blueprints/nerd/forms.py
index c110882db..c46222f32 100644
--- a/lib/hawat/blueprints/nerd/forms.py
+++ b/lib/hawat/blueprints/nerd/forms.py
@@ -29,10 +29,10 @@ class NerdSearchForm(flask_wtf.FlaskForm):
     Class representing NERD database search form.
     """
     search = wtforms.StringField(
-        lazy_gettext('Search external NERD database:'),
+        lazy_gettext('Search NERD:'),
         validators = [
             wtforms.validators.DataRequired(),
-            hawat.forms.check_ip_record
+            hawat.forms.check_ip4_record
         ]
     )
     submit = wtforms.SubmitField(
diff --git a/lib/hawat/blueprints/nerd/templates/nerd/search.html b/lib/hawat/blueprints/nerd/templates/nerd/search.html
index 4b22359c5..d2153d145 100644
--- a/lib/hawat/blueprints/nerd/templates/nerd/search.html
+++ b/lib/hawat/blueprints/nerd/templates/nerd/search.html
@@ -38,17 +38,70 @@
                         </a>
                     </p>
         {%- if search_result %}
-
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <h3 class="panel-title">{{ _('NERD database IP address lookup raw result') }}</h3>
-                        </div>
-                        <div class="panel-body">
-<pre>
-{{ search_result | pprint }}
-</pre>
-                        </div>
-                    </div>
+                    <table class="table table-condensed">
+                        <tbody>
+            {%- if 'rep' in search_result and search_result['rep'] %}
+                            <tr>
+                                <th>{{ _('Reputation') }}:</th>
+                                <td>{{ '{:.3f}'.format(search_result['rep']) }}</td>
+                            </tr>
+            {%- endif %}
+            {%- if 'fmp' in search_result and search_result['fmp'] and 'general' in search_result['fmp'] and search_result['fmp']['general'] %}
+                            <tr>
+                                <th>{{ _('FMP score') }}:</th>
+                                <td>{{ '{:.3f}'.format(search_result['fmp']['general']) }}</td>
+                            </tr>
+            {%- endif %}
+            {%- if 'hostname' in search_result and search_result['hostname'] %}
+                            <tr>
+                                <th>{{ _('Hostname') }}:</th>
+                                <td>{{ search_result['hostname'] }}</td>
+                            </tr>
+            {%- endif %}
+            {%- if 'asn' in search_result and search_result['asn'] %}
+                            <tr>
+                                <th>{{ _('ASN') }}:</th>
+                                <td>{{ search_result['asn'] | join(', ') }}</td>
+                            </tr>
+            {%- endif %}
+            {%- if 'geo' in search_result and search_result['geo'] and 'ctry' in search_result['geo'] and search_result['geo']['ctry'] %}
+                            <tr>
+                                <th>{{ _('Country') }}:</th>
+                                <td>{{ search_result['geo']['ctry'] | upper }} {{ get_country_flag(search_result['geo']['ctry']) }}</td>
+                            </tr>
+            {%- endif %}
+            {%- if 'bgppref' in search_result and search_result['bgppref'] %}
+                            <tr>
+                                <th>{{ _('BGP prefix') }}:</th>
+                                <td>{{ search_result['bgppref'] }}</td>
+                            </tr>
+            {%- endif %}
+            {%- if 'ipblock' in search_result and search_result['ipblock'] %}
+                            <tr>
+                                <th>{{ _('IP block') }}:</th>
+                                <td>{{ search_result['ipblock'] }}</td>
+                            </tr>
+            {%- endif %}
+            {%- if 'bl' in search_result and search_result['bl'] %}
+                            <tr>
+                                <th>{{ _('Blacklists') }}:</th>
+                                <td>{% for bl in search_result['bl'] %}
+                                    <span class="label label-default">{{ bl }}</span>
+                                    {% endfor %}
+                                </td>
+                            </tr>
+            {%- endif %}
+            {%- if 'tags' in search_result and search_result['tags'] %}
+                            <tr>
+                                <th>{{ _('Tags') }}:</th>
+                                <td>{% for tag in search_result['tags'] %}
+                                    <span class="label label-default">{{ tag['n'] }} ({{ tag['c'] }}x)</span>
+                                    {% endfor %}
+                                </td>
+                            </tr>
+            {%- endif %}
+                        </tbody>
+                    </table>
         {%- else %}
 
                     {%- call macros_site.render_alert('info', False) %}
diff --git a/lib/hawat/blueprints/nerd/templates/nerd/spt_label_reputation.html b/lib/hawat/blueprints/nerd/templates/nerd/spt_label_reputation.html
new file mode 100644
index 000000000..ddc782b3f
--- /dev/null
+++ b/lib/hawat/blueprints/nerd/templates/nerd/spt_label_reputation.html
@@ -0,0 +1,88 @@
+{%- import '_macros_site.html' as macros_site with context -%}
+{%- set content_id = get_uuid4() -%}
+<div class="popover-hover" role="button" data-popover-content="#po-{{ content_id }}">
+	<span class="label label-default">
+		{{ get_icon('module-nerd') }} | {{ '{:.3f}'.format(search_result['rep']) }}
+	</span>
+	<div id="po-{{ content_id }}" class="hidden">
+        <div class="popover top">
+            <div class="arrow"></div>
+            <h3 class="popover-title">
+                {{ get_icon('module-nerd') }} {{ _('NERD') }} - {{ _('Reputation resolving') }}
+                <span class="pull-right">
+                    <span data-toggle="tooltip" title="{{ _('Query time: ') }} {{ get_timedelta(g.requeststart) }}">
+                        {{ get_icon('stopwatch') }}
+                    </span>
+                </span>
+            </h3>
+            <div class="popover-content">
+                <table class="table table-condensed">
+                    <tbody>
+    {%- if 'rep' in search_result and search_result['rep'] %}
+                        <tr>
+                            <th>{{ _('Reputation') }}:</th>
+                            <td>{{ '{:.3f}'.format(search_result['rep']) }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'fmp' in search_result and search_result['fmp'] and 'general' in search_result['fmp'] and search_result['fmp']['general'] %}
+                        <tr>
+                            <th>{{ _('FMP score') }}:</th>
+                            <td>{{ '{:.3f}'.format(search_result['fmp']['general']) }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'hostname' in search_result and search_result['hostname'] %}
+                        <tr>
+                            <th>{{ _('Hostname') }}:</th>
+                            <td>{{ search_result['hostname'] }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'asn' in search_result and search_result['asn'] %}
+                        <tr>
+                            <th>{{ _('ASN') }}:</th>
+                            <td>{{ search_result['asn'] | join(', ') }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'geo' in search_result and search_result['geo'] and 'ctry' in search_result['geo'] and search_result['geo']['ctry'] %}
+                        <tr>
+                            <th>{{ _('Country') }}:</th>
+                            <td>{{ search_result['geo']['ctry'] | upper }} {{ get_country_flag(search_result['geo']['ctry']) }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'bgppref' in search_result and search_result['bgppref'] %}
+                        <tr>
+                            <th>{{ _('BGP prefix') }}:</th>
+                            <td>{{ search_result['bgppref'] }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'ipblock' in search_result and search_result['ipblock'] %}
+                        <tr>
+                            <th>{{ _('IP block') }}:</th>
+                            <td>{{ search_result['ipblock'] }}</td>
+                        </tr>
+    {%- endif %}
+    {%- if 'bl' in search_result and search_result['bl'] %}
+                        <tr>
+                            <th>{{ _('Blacklists') }}:</th>
+                            <td>{% for bl in search_result['bl'] %}
+                                <span class="label label-default">{{ bl }}</span>
+                                {% endfor %}
+                            </td>
+                        </tr>
+    {%- endif %}
+    {%- if 'tags' in search_result and search_result['tags'] %}
+                        <tr>
+                            <th>{{ _('Tags') }}:</th>
+                            <td>{% for tag in search_result['tags'] %}
+                            	<span class="label label-default">
+                            		{{ tag['n'] }} ({{ tag['c'] }}x)
+                            	</span>
+                            	{% endfor %}
+                        	</td>
+                        </tr>
+    {%- endif %}
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/lib/hawat/blueprints/pdnsr/__init__.py b/lib/hawat/blueprints/pdnsr/__init__.py
new file mode 100644
index 000000000..085ffab07
--- /dev/null
+++ b/lib/hawat/blueprints/pdnsr/__init__.py
@@ -0,0 +1,266 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#-------------------------------------------------------------------------------
+# This file is part of Mentat system (https://mentat.cesnet.cz/).
+#
+# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
+# Use of this source is governed by the MIT license, see LICENSE file.
+#-------------------------------------------------------------------------------
+
+
+"""
+Description
+-----------
+
+This pluggable module provides access to external PassiveDNS service. It is built
+upon custom :py:mod:`mentat.services.pdnsr` module.
+
+
+Provided endpoints
+------------------
+
+``/pdnsr/search``
+    Endpoint providing search form for querying external PassiveDNS service and
+    formating result as HTML page.
+
+    * *Authentication:* login required
+    * *Methods:* ``GET``
+
+``/api/pdnsr/search``
+    Endpoint providing API search form for querying external PassiveDNS service
+    and formating result as JSON document.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
+
+``/snippet/pdnsr/search``
+    Endpoint providing API search form for querying external PassiveDNS service
+    and formating result as JSON document containing HTML snippets.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
+"""
+
+
+__author__ = "Jan Mach <jan.mach@cesnet.cz>"
+__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
+
+#
+# Flask related modules.
+#
+import flask
+from flask_babel import lazy_gettext
+
+#
+# Custom modules.
+#
+import mentat.services.pdnsr
+from mentat.const import tr_
+
+import hawat.const
+import hawat.db
+import hawat.acl
+from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, HawatBlueprint, URLParamsBuilder
+from hawat.blueprints.pdnsr.forms import PDNSRSearchForm
+
+
+BLUEPRINT_NAME = 'pdnsr'
+"""Name of the blueprint as module global constant."""
+
+
+class AbstractSearchView(RenderableView):  # pylint: disable=locally-disabled,abstract-method
+    """
+    Application view providing base search capabilities for external PassiveDNS service.
+
+    The querying is implemented using :py:mod:`mentat.services.pdnsr` module.
+    """
+    authentication = True
+
+    authorization = [hawat.acl.PERMISSION_ANY]
+
+    @classmethod
+    def get_menu_title(cls, item = None):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`."""
+        return lazy_gettext('Search passive DNS')
+
+    @classmethod
+    def get_view_title(cls, item = None):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`."""
+        return lazy_gettext('Search passive DNS')
+
+    #---------------------------------------------------------------------------
+
+    def dispatch_request(self):
+        """
+        Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`.
+        Will be called by the *Flask* framework to service the request.
+        """
+        form = PDNSRSearchForm(flask.request.args, meta = {'csrf': False})
+
+        if hawat.const.HAWAT_FORM_ACTION_SUBMIT in flask.request.args:
+            if form.validate():
+                form_data = form.data
+                pdnsr_service = mentat.services.pdnsr.service()
+                self.response_context.update(
+                    search_item = form.search.data,
+                    search_url  = pdnsr_service.get_url_lookup_ip(form.search.data),
+                    form_data   = form_data
+                )
+
+                try:
+                    self.response_context.update(
+                        search_result = pdnsr_service.lookup_ip(
+                            form.search.data,
+                            form.sortby.data,
+                            form.limit.data
+                        )
+                    )
+                except Exception as exc:
+                    self.flash(str(exc), category = 'error')
+
+        self.response_context.update(
+            search_form  = form,
+            request_args = flask.request.args
+        )
+        return self.generate_response()
+
+
+class SearchView(HTMLMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying external NERD service and presenting the results
+    in the form of HTML page.
+    """
+    methods = ['GET']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'search'
+
+
+class APISearchView(AJAXMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying external NERD service and presenting the results
+    in the form of JSON document.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'apisearch'
+
+
+class SnippetSearchView(SnippetMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying DNS service and presenting the results in the
+    form of JSON document containg ready to use HTML page snippets.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'sptsearch'
+
+    def process_response_context(self):
+        """*Implementation* of :py:func:`hawat.base.SnippetMixin.process_response_context`."""
+        super().process_response_context()
+
+        # Make latter expressions shorter
+        res = self.response_context.get('search_result', False)
+        if res:
+            self.response_context.setdefault(
+                self.KW_RESP_SNIPPETS,
+                {}
+            )['label_hostnames'] = flask.render_template(
+                'pdnsr/spt_label_hostnames.html',
+                search_result = res
+            )
+        return self.response_context
+
+
+#-------------------------------------------------------------------------------
+
+
+class PDNSRBlueprint(HawatBlueprint):
+    """
+    Hawat pluggable module - PassiveDNS service.
+    """
+
+    @classmethod
+    def get_module_title(cls):
+        """*Implementation* of :py:func:`hawat.base.HawatBlueprint.get_module_title`."""
+        return lazy_gettext('PassiveDNS pluggable module')
+
+    def register_app(self, app):
+        """
+        *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint`
+        method and can be used to customize the Flask application object. Possible
+        use cases:
+
+        * application menu customization
+
+        :param hawat.base.HawatApp app: Flask application to be customize.
+        """
+
+        mentat.services.pdnsr.init(app.mconfig)
+
+        app.menu_main.add_entry(
+            'view',
+            'more.{}'.format(BLUEPRINT_NAME),
+            position = 40,
+            view = SearchView
+        )
+
+        # Register context actions provided by this module.
+        app.set_csag(
+            hawat.const.HAWAT_CSAG_ADDRESS,
+            tr_('Search for address <strong>%(name)s</strong> locally in PassiveDNS service'),
+            SearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search')
+        )
+        app.set_csag_url(
+            hawat.const.HAWAT_CSAG_ADDRESS,
+            tr_('Search for address <strong>%(name)s</strong> externally in PassiveDNS service'),
+            SearchView.get_menu_icon(),
+            mentat.services.pdnsr.service().get_url_lookup_ip
+        )
+
+        # Register additional object data services provided by this module.
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP4,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search'), 'sortby': 'count.desc'}).add_rule('search'),
+            ['label_hostnames']
+        )
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP6,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search'), 'sortby': 'count.desc'}).add_rule('search'),
+            ['label_hostnames']
+        )
+
+
+#-------------------------------------------------------------------------------
+
+
+def get_blueprint():
+    """
+    Mandatory interface and factory function. This function must return a valid
+    instance of :py:class:`hawat.base.HawatBlueprint` or :py:class:`flask.Blueprint`.
+    """
+
+    hbp = PDNSRBlueprint(
+        BLUEPRINT_NAME,
+        __name__,
+        template_folder = 'templates'
+    )
+
+    hbp.register_view_class(SearchView,        '/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(APISearchView,     '/api/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME))
+
+    return hbp
diff --git a/lib/hawat/blueprints/pdnsr/forms.py b/lib/hawat/blueprints/pdnsr/forms.py
new file mode 100644
index 000000000..4ff335e89
--- /dev/null
+++ b/lib/hawat/blueprints/pdnsr/forms.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#-------------------------------------------------------------------------------
+# This file is part of Mentat system (https://mentat.cesnet.cz/).
+#
+# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
+# Use of this source is governed by the MIT license, see LICENSE file.
+#-------------------------------------------------------------------------------
+
+
+"""
+This module contains custom external PassiveDNS database search form for Hawat.
+"""
+
+
+__author__ = "Jan Mach <jan.mach@cesnet.cz>"
+__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
+
+
+import wtforms
+import flask_wtf
+from flask_babel import lazy_gettext
+
+import hawat.forms
+
+
+class PDNSRSearchForm(flask_wtf.FlaskForm):
+    """
+    Class representing PassiveDNS service search form.
+    """
+    search = wtforms.StringField(
+        lazy_gettext('Search PassiveDNS:'),
+        validators = [
+            wtforms.validators.DataRequired(),
+            hawat.forms.check_ip_record
+        ]
+    )
+    sortby = wtforms.SelectField(
+        lazy_gettext('Sort by:'),
+        validators = [
+            wtforms.validators.Optional()
+        ],
+        choices = [
+            ('', lazy_gettext('without explicit sorting')),
+            ('domain.desc', lazy_gettext('by domain name descending')),
+            ('domain.asc',  lazy_gettext('by domain name ascending')),
+            ('count.desc', lazy_gettext('by hit count descending')),
+            ('count.asc',  lazy_gettext('by hit count ascending')),
+            ('firstseen.desc', lazy_gettext('by first seen time descending')),
+            ('firstseen`.asc',  lazy_gettext('by first seen time ascending')),
+            ('lastseen.desc', lazy_gettext('by last seen time descending')),
+            ('lastseen.asc',  lazy_gettext('by last seen time ascending'))
+        ],
+        default = ''
+    )
+    limit = hawat.forms.SelectFieldWithNone(
+        lazy_gettext('Pager limit:'),
+        validators = [
+            wtforms.validators.Optional()
+        ],
+        filters = [int],
+        choices = [(0, lazy_gettext('without explicit limit'))] + hawat.const.HAWAT_PAGER_LIMIT_CHOICES,
+        default = 0
+    )
+    submit = wtforms.SubmitField(
+        lazy_gettext('Search')
+    )
diff --git a/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html b/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html
new file mode 100644
index 000000000..9a2a1df5c
--- /dev/null
+++ b/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html
@@ -0,0 +1,92 @@
+{%- extends "_layout.html" %}
+
+{%- block content %}
+
+            <div class="row">
+                <div class="col-lg-12">
+
+                    <div class="jumbotron" style="margin-top: 1em;">
+                        <h2>{{ hawat_current_view.get_view_title() }}</h2>
+                        <form method="GET" class="form-inline" action="{{ url_for('pdnsr.search') }}">
+                            <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}">
+                                {{ search_form.search.label(class_='sr-only') }}
+                                <div class="input-group">
+                                    <div data-toggle="tooltip" class="input-group-addon" title="{{ _('Search PassiveDNS service  for:') }}">{{ get_icon('action-search') }}</div>
+                                    {{ search_form.search(class_='form-control', placeholder=_('IPv4 or IPv6 address'), size='50') }}
+                                </div>
+
+                                <div class="btn-group" role="group">
+                                    {{ search_form.submit(class_='btn btn-primary') }}
+                                    <a role="button" class="btn btn-default" href="{{ url_for('pdnsr.search') }}">
+                                        {{ _('Clear') }}
+                                    </a>
+                                </div>
+                            </div>
+                            {{ macros_form.render_form_errors(search_form.search.errors) }}
+                        </form>
+                    </div> <!-- .jumbotron -->
+
+                </div> <!-- .col-lg-12 -->
+            </div>  <!-- .row -->
+
+    {%- if search_item %}
+            <div class="row">
+                <div class="col-lg-12">
+                    <p>
+                        <a href="{{ search_url }}" target="_blank">
+                            {{ get_icon('search') }} {{ _('Search externally in PassiveDNS service') }}
+                        </a>
+                    </p>
+        {%- if search_result %}
+                    <table class="table table-striped table-bordered table-hover table-condensed table-responsive">
+                        <thead>
+                            <tr>
+                                <th></th>
+                                <th>
+                                    {{ get_icon('domain') }} {{ _('Domain') }}
+                                </th>
+                                <th>
+                                    {{ get_icon('time-from') }} {{ _('First seen') }}
+                                </th>
+                                <th>
+                                    {{ get_icon('time-to') }} {{ _('Last seen') }}
+                                </th>
+                                <th>
+                                    {{ get_icon('cnt') }} {{ _('Count') }}
+                                </th>
+                            </tr>
+                        </thead>
+                        <tbody>
+            {%- for sritem in search_result %}
+                            <tr>
+                                <td>{{ loop.index }}</td>
+                                <td>{{ sritem.get('domain', _('-- undefined --')) }}</td>
+                                <td>{{ sritem.get('time_first', _('-- undefined --')) }}</td>
+                                <td>{{ sritem.get('time_last', _('-- undefined --')) }}</td>
+                                <td>{{ sritem.get('count', _('-- undefined --')) }}</td>
+                            </tr>
+            {%- endfor %}
+                        </tbody>
+                    </table>
+        {%- else %}
+
+                    {%- call macros_site.render_alert('info', False) %}
+                    {{ _('There are no records for <strong>%(item_id)s</strong> in <em>PassiveDNS</em> service.', item_id = search_item) | safe }}
+                    {%- endcall %}
+
+        {%- endif %}
+
+                </div> <!-- .col-lg-12 -->
+            </div>  <!-- .row -->
+
+        {%- if permission_can('developer') %}
+            <hr>
+            {{ macros_site.render_raw_var('form_data', form_data) }}
+            {{ macros_site.render_raw_var('search_result', search_result) }}
+            {{ macros_site.render_raw_var('request_args', request_args) }}
+            {{ macros_site.render_raw_var('query_params', query_params) }}
+        {%- endif %}
+
+    {%- endif %}
+
+{%- endblock content %}
diff --git a/lib/hawat/blueprints/pdnsr/templates/pdnsr/spt_label_hostnames.html b/lib/hawat/blueprints/pdnsr/templates/pdnsr/spt_label_hostnames.html
new file mode 100644
index 000000000..06a74bc7b
--- /dev/null
+++ b/lib/hawat/blueprints/pdnsr/templates/pdnsr/spt_label_hostnames.html
@@ -0,0 +1,55 @@
+{%- set content_id = get_uuid4() -%}
+<div class="popover-hover" role="button" data-popover-content="#po-{{ content_id }}">
+    <span class="label label-default">
+        {{ get_icon('module-pdnsr') }} | {{ search_result[0].get('domain', _('-- undefined --')) }}{% if search_result|length > 1 %} ({{ _('%(cnt)d more', cnt = (search_result|length -1))}}){% endif %}
+    </span>
+    <div id="po-{{ content_id }}" class="hidden">
+        <div class="popover top">
+            <div class="arrow"></div>
+            <h3 class="popover-title">
+                {{ get_icon('module-pdnsr') }} {{ _('PassiveDNS') }} - {{ _('Hostname resolving') }}
+                <span class="pull-right">
+                    <span data-toggle="tooltip" title="{{ _('Query time: ') }} {{ get_timedelta(g.requeststart) }}">
+                        {{ get_icon('stopwatch') }}
+                    </span>
+                </span>
+            </h3>
+            <div class="popover-content">
+                <table class="table table-condensed">
+                    <tr>
+                        <th></th>
+                        <th>
+                            <span data-toggle="tooltip" title="{{ _('Domain') }}">
+                                {{ get_icon('domain') }}
+                            </span>
+                        </th>
+                        <th>
+                            <span data-toggle="tooltip" title="{{ _('First seen') }}">
+                                {{ get_icon('time-from') }}
+                            </span>
+                        </th>
+                        <th>
+                            <span data-toggle="tooltip" title="{{ _('Last seen') }}">
+                                {{ get_icon('time-to') }}
+                            </span>
+                        </th>
+                        <th>
+                            <span data-toggle="tooltip" title="{{ _('Count') }}">
+                                {{ get_icon('cnt') }}
+                            </span>
+                        </th>
+                    </tr>
+    {%- for sritem in search_result %}
+                    <tr>
+                        <td>{{ loop.index }}</td>
+                        <td>{{ sritem.get('domain', _('-- undefined --')) }}</td>
+                        <td>{{ sritem.get('time_first', _('-- undefined --')) }}</td>
+                        <td>{{ sritem.get('time_last', _('-- undefined --')) }}</td>
+                        <td>{{ sritem.get('count', _('-- undefined --')) }}</td>
+                    </tr>
+    {%- endfor %}
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/lib/hawat/blueprints/performance/__init__.py b/lib/hawat/blueprints/performance/__init__.py
index 11b7106f0..3520763c9 100644
--- a/lib/hawat/blueprints/performance/__init__.py
+++ b/lib/hawat/blueprints/performance/__init__.py
@@ -164,7 +164,7 @@ class PerformanceBlueprint(HawatBlueprint):
         app.menu_main.add_entry(
             'view',
             'more.{}'.format(BLUEPRINT_NAME),
-            position = 30,
+            position = 100,
             group = lazy_gettext('Status overview'),
             view = ViewView
         )
diff --git a/lib/hawat/blueprints/whois/__init__.py b/lib/hawat/blueprints/whois/__init__.py
index 9d5a20b06..388e53a45 100644
--- a/lib/hawat/blueprints/whois/__init__.py
+++ b/lib/hawat/blueprints/whois/__init__.py
@@ -12,13 +12,13 @@
 Description
 -----------
 
-This pluggable module provides access to internal whois service. It is built upon
-custom :py:mod:`mentat.services.whois` module. The main purpose of the *whois*
+This pluggable module provides access to local WHOIS service. It is built upon
+custom :py:mod:`mentat.services.whois` module. The main purpose of the *WHOIS*
 service in *Mentat* system is to resolve abuse contacts for event sources to enable
 automated event reporting capabilities. Another use case is data access management
-by abuse groups in web interface.
+directed by abuse group memberships in web interface.
 
-This module enables access to internal whois database and enables users to input
+This module enables access to internal WHOIS database and enables users to input
 queries. The main use case scenario is a validation of target abuse contact for
 particular source address/network.
 
@@ -27,10 +27,27 @@ Provided endpoints
 ------------------
 
 ``/whois/search``
-    Page providing search form and displaying search results.
+    Endpoint providing search form for querying local WHOIS service and formating
+    result as HTML page.
 
     * *Authentication:* login required
     * *Methods:* ``GET``
+
+``/api/whois/search``
+    Endpoint providing API search form for querying local WHOIS service
+    and formating result as JSON document.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
+
+``/snippet/whois/search``
+    Endpoint providing API search form for querying local WHOIS service
+    and formating result as JSON document containing HTML snippets.
+
+    * *Authentication:* login required
+    * *Authorization:* any role
+    * *Methods:* ``GET``, ``POST``
 """
 
 
@@ -53,7 +70,7 @@ from mentat.const import tr_
 
 import hawat.db
 import hawat.acl
-from hawat.base import HTMLMixin, RenderableView, HawatBlueprint, URLParamsBuilder
+from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, HawatBlueprint, URLParamsBuilder
 from hawat.blueprints.whois.forms import WhoisSearchForm
 
 
@@ -61,33 +78,26 @@ BLUEPRINT_NAME = 'whois'
 """Name of the blueprint as module global constant."""
 
 
-class SearchView(HTMLMixin, RenderableView):
+class AbstractSearchView(RenderableView):
     """
-    Application view providing search form for internal whois resolving service
+    Application view providing search form for internal WHOIS resolving service
     and appropriate result page.
 
-    The geolocation is implemented using :py:mod:`mentat.services.whois` module.
+    The resolving is implemented using :py:mod:`mentat.services.whois` module.
     """
-    methods = ['GET']
-
     authentication = True
 
     authorization = [hawat.acl.PERMISSION_ANY]
 
-    @classmethod
-    def get_view_name(cls):
-        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
-        return 'search'
-
     @classmethod
     def get_menu_title(cls, item = None):
         """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`."""
-        return lazy_gettext('Internal whois')
+        return lazy_gettext('Search internal whois')
 
     @classmethod
     def get_view_title(cls, item = None):
         """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`."""
-        return lazy_gettext('Search internal whois database')
+        return lazy_gettext('Search internal whois service')
 
     #---------------------------------------------------------------------------
 
@@ -105,9 +115,14 @@ class SearchView(HTMLMixin, RenderableView):
                 whois_service = whois_manager.service()
                 self.response_context.update(
                     search_item   = form.search.data,
-                    search_result = whois_service.lookup(form.search.data),
                     form_data     = form_data
                 )
+                try:
+                    self.response_context.update(
+                        search_result = whois_service.lookup(form.search.data)
+                    )
+                except Exception as exc:
+                    self.flash(str(exc), category = 'error')
 
         self.response_context.update(
             search_form  = form,
@@ -116,18 +131,73 @@ class SearchView(HTMLMixin, RenderableView):
         return self.generate_response()
 
 
+class SearchView(HTMLMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying local WHOIS service and presenting the results
+    in the form of HTML page.
+    """
+    methods = ['GET']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'search'
+
+
+class APISearchView(AJAXMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying local WHOIS service and presenting the results
+    in the form of JSON document.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'apisearch'
+
+
+class SnippetSearchView(SnippetMixin, AbstractSearchView):  # pylint: disable=locally-disabled,too-many-ancestors
+    """
+    View responsible for querying local WHOIS service and presenting the results
+    in the form of JSON document containg ready to use HTML page snippets.
+    """
+    methods = ['GET','POST']
+
+    @classmethod
+    def get_view_name(cls):
+        """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`."""
+        return 'sptsearch'
+
+    def process_response_context(self):
+        """*Implementation* of :py:func:`hawat.base.AJAXMixin.process_response_context`."""
+        super().process_response_context()
+
+        # Make latter expressions shorter
+        res = self.response_context.get('search_result', False)
+        if res:
+            self.response_context.setdefault(
+                self.KW_RESP_SNIPPETS,
+                {}
+            )['label_abuse'] = flask.render_template(
+                'whois/spt_label_abuse.html',
+                search_result = res
+            )
+        return self.response_context
+
+
 #-------------------------------------------------------------------------------
 
 
 class WhoisBlueprint(HawatBlueprint):
     """
-    Hawat pluggable module - whois resolving service.
+    Hawat pluggable module - WHOIS resolving service.
     """
 
     @classmethod
     def get_module_title(cls):
         """*Implementation* of :py:func:`hawat.base.HawatBlueprint.get_module_title`."""
-        return lazy_gettext('Internal whois pluggable module')
+        return lazy_gettext('Local WHOIS pluggable module')
 
     def register_app(self, app):
         """
@@ -149,11 +219,25 @@ class WhoisBlueprint(HawatBlueprint):
         # Register context actions provided by this module.
         app.set_csag(
             hawat.const.HAWAT_CSAG_ADDRESS,
-            tr_('Search for address <strong>%(name)s</strong> in internal whois database'),
+            tr_('Search for address <strong>%(name)s</strong> in local WHOIS service'),
             SearchView,
             URLParamsBuilder({'submit': tr_('Search')}).add_rule('search')
         )
 
+        # Register additional object data services provided by this module.
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP4,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search'),
+            ['label_abuse']
+        )
+        app.set_aods(
+            hawat.const.HAWAT_AODS_IP6,
+            SnippetSearchView,
+            URLParamsBuilder({'submit': tr_('Search')}).add_rule('search'),
+            ['label_abuse']
+        )
+
 
 #-------------------------------------------------------------------------------
 
@@ -167,10 +251,11 @@ def get_blueprint():
     hbp = WhoisBlueprint(
         BLUEPRINT_NAME,
         __name__,
-        template_folder = 'templates',
-        url_prefix = '/{}'.format(BLUEPRINT_NAME)
+        template_folder = 'templates'
     )
 
-    hbp.register_view_class(SearchView, '/search')
+    hbp.register_view_class(SearchView,        '/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(APISearchView,     '/api/{}/search'.format(BLUEPRINT_NAME))
+    hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME))
 
     return hbp
diff --git a/lib/hawat/blueprints/whois/templates/whois/search.html b/lib/hawat/blueprints/whois/templates/whois/search.html
index 3794c7bfb..0c26d130e 100644
--- a/lib/hawat/blueprints/whois/templates/whois/search.html
+++ b/lib/hawat/blueprints/whois/templates/whois/search.html
@@ -10,13 +10,13 @@
                             <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}">
                                 {{ search_form.search.label(class_='sr-only') }}
                                 <div class="input-group">
-                                    <div data-toggle="tooltip" class="input-group-addon" title="{{ gettext('Search local whois database for:') }}">{{ get_icon('action-search') }}</div>
-                                    {{ search_form.search(class_='form-control', placeholder=gettext('IP address, range, network or abuse email'), size='50') }}
+                                    <div data-toggle="tooltip" class="input-group-addon" title="{{ _('Search local whois service for:') }}">{{ get_icon('action-search') }}</div>
+                                    {{ search_form.search(class_='form-control', placeholder=_('IP address, range, network or abuse email'), size='50') }}
                                 </div>
 
                                 <div class="btn-group" role="group">
                                     {{ search_form.submit(class_='btn btn-primary') }}
-                                    <a role="button" class="btn btn-default" href="{{ url_for('whois.search') }}">{{ gettext('Clear') }}</a>
+                                    <a role="button" class="btn btn-default" href="{{ url_for('whois.search') }}">{{ _('Clear') }}</a>
                                 </div>
                             </div>
                             {{ macros_form.render_form_errors(search_form.search.errors) }}
@@ -33,16 +33,21 @@
         {%- if search_result %}
                     <div class="panel panel-default">
                         <ul class="list-group">
-            {%- for item in search_result %}
+            {%- for item in search_result | reverse %}
                             <li class="list-group-item">
                                 <p class="lead">
                                     <em>{% if 'netname' in item %}{{ item['netname'] }}: {% endif %}{{ item['network'] }}</em>
                                 </p>
                                 <p>
-                                    {{ gettext('Target abuse contacts:') }} <strong>{{ item['resolved_abuses'] | join(', ') }}</strong>
+                                    {{ _('Target abuse contacts') }}: <strong>{{ item['resolved_abuses'] | join(', ') }}</strong>
                                 </p>
+                                {% if 'description' in item %}
                                 <p>
-                                    {% if 'description' in item %}{{ item['description'] }} {% endif %}(source: {{ item['source'] }})
+                                    {{ _('Description') }}: {{ item['description'] }}
+                                </p>
+                                {% endif %}
+                                <p>
+                                    {{ _('Source') }}: <span class="label label-default">{{ item['source'] }}</span>
                                 </p>
                             </li>
             {%- endfor %}
@@ -51,7 +56,7 @@
 
         {%- else %}
                     {%- call macros_site.render_alert('info', False) %}
-                    {{ gettext('There are no records for <strong>%(item_id)s</strong> in <em>Whois</em> database.', item_id = search_item) | safe }}
+                    {{ _('There are no records for <strong>%(item_id)s</strong> in <em>Whois</em> database.', item_id = search_item) | safe }}
                     {%- endcall %}
         {%- endif %}
 
diff --git a/lib/hawat/blueprints/whois/templates/whois/spt_label_abuse.html b/lib/hawat/blueprints/whois/templates/whois/spt_label_abuse.html
new file mode 100644
index 000000000..6c5e60cd3
--- /dev/null
+++ b/lib/hawat/blueprints/whois/templates/whois/spt_label_abuse.html
@@ -0,0 +1,37 @@
+{%- set content_id = get_uuid4() -%}
+<div class="popover-hover" role="button" data-popover-content="#po-{{ content_id }}">
+    <span class="label label-default">
+        {{ get_icon('module-whois') }} | {{ search_result[0].get('resolved_abuses', _('-- undefined --')) | join(', ') }}{% if search_result|length > 1 %} ({{ _('%(cnt)d more', cnt = (search_result|length -1))}}){% endif %}
+    </span>
+    <div id="po-{{ content_id }}" class="hidden">
+        <div class="popover top">
+            <div class="arrow"></div>
+            <h3 class="popover-title">
+                {{ get_icon('module-whois') }} {{ _('Local whois') }} - {{ _('Abuse resolving') }}
+                <span class="pull-right">
+                    <span data-toggle="tooltip" title="{{ _('Query time: ') }} {{ get_timedelta(g.requeststart) }}">
+                        {{ get_icon('stopwatch') }}
+                    </span>
+                </span>
+            </h3>
+            <div class="popover-content">
+                <table class="table table-condensed">
+                    <tr>
+                        <th></th>
+                        <th>{{ _('Network') }}</th>
+                        <th>{{ _('Netname') }}</th>
+                        <th>{{ _('Contacts') }}</th>
+                    </tr>
+    {%- for sritem in search_result %}
+                    <tr>
+                        <td>{{ loop.index }}</td>
+                        <td>{% if 'network' in sritem %}{{ sritem['network'] }}{% endif %}</td>
+                        <td>{% if 'netname' in sritem %}{{ sritem['netname'] }}{% endif %}</td>
+                        <td>{% if 'resolved_abuses' in sritem %}{{ sritem['resolved_abuses'] | join(', ') }}{% endif %}</td>
+                    </tr>
+    {%- endfor %}
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/lib/hawat/config.py b/lib/hawat/config.py
index d89cb9bd6..8996bba84 100644
--- a/lib/hawat/config.py
+++ b/lib/hawat/config.py
@@ -136,6 +136,7 @@ class Config:  # pylint: disable=locally-disabled,too-few-public-methods
         'hawat.blueprints.events',
         'hawat.blueprints.timeline',
         'hawat.blueprints.dnsr',
+        #'hawat.blueprints.pdnsr',
         'hawat.blueprints.geoip',
         #'hawat.blueprints.nerd',
         'hawat.blueprints.whois',
@@ -269,6 +270,7 @@ class DevelopmentConfig(Config):  # pylint: disable=locally-disabled,too-few-pub
         'hawat.blueprints.events',
         'hawat.blueprints.timeline',
         'hawat.blueprints.dnsr',
+        #'hawat.blueprints.pdnsr',
         'hawat.blueprints.geoip',
         #'hawat.blueprints.nerd',
         'hawat.blueprints.whois',
diff --git a/lib/hawat/const.py b/lib/hawat/const.py
index 00fb49730..f136767fb 100644
--- a/lib/hawat/const.py
+++ b/lib/hawat/const.py
@@ -128,8 +128,11 @@ RESOURCE_BABEL = 'babel'
 RESOURCE_MIGRATE = 'migrate'
 """Name for the ``flask_migrate.Migrate`` object within the application resources."""
 
+LIMIT_ADS = 10
+"""Limit for number of objects for which to automatically fetch additional data services."""
+
 #
-# List of all existing Hawat context action search groups.
+# List of all existing Hawat context action search group names.
 #
 HAWAT_CSAG_ABUSE    = 'abuses'
 HAWAT_CSAG_ADDRESS  = 'ips'
@@ -142,6 +145,12 @@ HAWAT_CSAG_PORT     = 'ports'
 HAWAT_CSAG_PROTOCOL = 'protocols'
 HAWAT_CSAG_SEVERITY = 'severities'
 
+#
+# List of all existing Hawat additonal object data service group names.
+#
+HAWAT_AODS_IP4  = 'ip4'
+HAWAT_AODS_IP6  = 'ip6'
+
 FA_ICONS = {
 
     #
@@ -187,6 +196,7 @@ FA_ICONS = {
     'module-help':               '<i class="fas fa-fw fa-question-circle"></i>',
     'module-networks':           '<i class="fas fa-fw fa-sitemap"></i>',
     'module-nerd':               '<i class="fas fa-fw fa-certificate"></i>',
+    'module-pdnsr':              '<i class="fas fa-fw fa-compass"></i>',
     'module-performance':        '<i class="fas fa-fw fa-chart-bar"></i>',
     'module-reports':            '<i class="fas fa-fw fa-newspaper"></i>',
     'module-settings-reporting': '<i class="fas fa-fw fa-sliders-h"></i>',
@@ -257,6 +267,9 @@ FA_ICONS = {
     'calendar':    '<i class="fas fa-fw fa-calendar-alt"></i>',
     'stopwatch':   '<i class="fas fa-fw fa-stopwatch"></i>',
     'clock':       '<i class="fas fa-fw fa-clock"></i>',
+    'domain':      '<i class="fas fa-fw fa-tag"></i>',
+    'time-from':   '<i class="fas fa-fw fa-hourglass-start"></i>',
+    'time-to':     '<i class="fas fa-fw fa-hourglass-end"></i>',
     'debug':       '<i class="fas fa-fw fa-bug"></i>',
     'eventclss':   '<i class="fas fa-fw fa-book"></i>',
     'reference':   '<i class="fas fa-fw fa-external-link"></i>',
diff --git a/lib/hawat/forms.py b/lib/hawat/forms.py
index 402b8984a..b47a4aef6 100644
--- a/lib/hawat/forms.py
+++ b/lib/hawat/forms.py
@@ -178,6 +178,44 @@ def check_ip_record(form, field):  # pylint: disable=locally-disabled,unused-arg
         )
     )
 
+def check_ip4_record(form, field):  # pylint: disable=locally-disabled,unused-argument
+    """
+    Callback for validating IP4 addresses.
+    """
+    # Valid value is a single IP4 address:
+    for tconv in (ipranges.IP4,):
+        try:
+            tconv(field.data)
+            return
+        except ValueError:
+            pass
+
+    raise wtforms.validators.ValidationError(
+        gettext(
+            'The "%(val)s" value does not look like valid IPv4 address.',
+            val = str(field.data)
+        )
+    )
+
+def check_ip6_record(form, field):  # pylint: disable=locally-disabled,unused-argument
+    """
+    Callback for validating IP6 addresses.
+    """
+    # Valid value is a single IP6 address:
+    for tconv in (ipranges.IP6,):
+        try:
+            tconv(field.data)
+            return
+        except ValueError:
+            pass
+
+    raise wtforms.validators.ValidationError(
+        gettext(
+            'The "%(val)s" value does not look like valid IPv6 address.',
+            val = str(field.data)
+        )
+    )
+
 def check_network_record(form, field):  # pylint: disable=locally-disabled,unused-argument
     """
     Callback for validating network records.
diff --git a/lib/hawat/templates/hawat-main.js b/lib/hawat/templates/hawat-main.js
index a7976aa17..22280fffb 100644
--- a/lib/hawat/templates/hawat-main.js
+++ b/lib/hawat/templates/hawat-main.js
@@ -17,9 +17,9 @@ var Hawat = (function () {
 
     var _csag = {
 {%- for csag_name in hawat_current_app.csag.keys() | sort %}
-    {%- if 'view' in hawat_current_app.csag[csag_name] %}
         '{{ csag_name }}': [
-        {%- for csag in hawat_current_app.csag[csag_name] %}
+    {%- for csag in hawat_current_app.csag[csag_name] %}
+        {%- if 'view' in csag %}
             {
                 'title':    '{{ _(csag.title, name = '{name}') }}',
                 'endpoint': '{{ csag.view.get_view_endpoint() }}',
@@ -29,9 +29,29 @@ var Hawat = (function () {
                     {{ csag.params.rules | tojson | safe }}
                 )
             }{%- if not loop.last %},{%- endif %}
-        {%- endfor %}
+        {%- endif %}
+    {%- endfor %}
+        ]{%- if not loop.last %},{%- endif %}
+{%- endfor %}
+    };
+
+    var _aods = {
+{%- for aods_name in hawat_current_app.aods.keys() | sort %}
+        '{{ aods_name }}': [
+    {%- for aods in hawat_current_app.aods[aods_name] %}
+        {%- if 'view' in aods %}
+            {
+                'endpoint': '{{ aods.view.get_view_endpoint() }}',
+                'ident': '{{ aods.view.module_name | upper }}',
+                'snippets': {{ aods.snippets | tojson | safe }},
+                'params':   _build_param_builder(
+                    {{ aods.params.skeleton | tojson | safe }},
+                    {{ aods.params.rules | tojson | safe }}
+                )
+            }{%- if not loop.last %},{%- endif %}
+        {%- endif %}
+    {%- endfor %}
         ]{%- if not loop.last %},{%- endif %}
-    {%- endif %}
 {%- endfor %}
     };
 
@@ -47,6 +67,19 @@ var Hawat = (function () {
             catch (err) {
                 return null
             }
+        },
+
+        get_aodss: function() {
+            return _aods;
+        },
+
+        get_aods: function(name) {
+            try {
+                return _aods[name];
+            }
+            catch (err) {
+                return null
+            }
         }
     };
 })();
diff --git a/lib/hawat/utils.py b/lib/hawat/utils.py
new file mode 100644
index 000000000..09c22269c
--- /dev/null
+++ b/lib/hawat/utils.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#-------------------------------------------------------------------------------
+# This file is part of Mentat system (https://mentat.cesnet.cz/).
+#
+# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
+# Use of this source is governed by the MIT license, see LICENSE file.
+#-------------------------------------------------------------------------------
+
+
+"""
+This module contains various utilities.
+"""
+
+
+__author__ = "Jan Mach <jan.mach@cesnet.cz>"
+__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
+
+
+class LimitCounter:
+    def __init__(self, limit):
+        self.counters = {}
+        self.limit    = limit
+
+    def count_and_check(self, key):
+        self.counters[key] = self.counters.get(key, 0) + 1
+        return self.counters[key] <= self.limit
diff --git a/lib/mentat/__init__.py b/lib/mentat/__init__.py
index 430c25991..2d71650f9 100644
--- a/lib/mentat/__init__.py
+++ b/lib/mentat/__init__.py
@@ -20,4 +20,4 @@ open-source project.
 
 __author__  = "Jan Mach <jan.mach@cesnet.cz>"
 __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
-__version__ = "2.4.6"
+__version__ = "2.4.8"
diff --git a/lib/mentat/const.py b/lib/mentat/const.py
index 4d14c4565..574c0d3e2 100644
--- a/lib/mentat/const.py
+++ b/lib/mentat/const.py
@@ -86,11 +86,13 @@ CKEY_CORE_DATABASE_SCHEMA = 'schema'
 CKEY_CORE_SERVICES = '__core__services'
 """Name of the configuration key for ``core services`` configurations."""
 CKEY_CORE_SERVICES_DNS = 'dns'
-"""Name of the configuration subkey key for ``dns`` configuration in ``core services`` configurations."""
+"""Name of the configuration subkey key for ``DNS`` configuration in ``core services`` configurations."""
+CKEY_CORE_SERVICES_PDNS = 'pdns'
+"""Name of the configuration subkey key for ``PassiveDNS`` configuration in ``core services`` configurations."""
 CKEY_CORE_SERVICES_GEOIP = 'geoip'
-"""Name of the configuration subkey key for ``geoip`` configuration in ``core services`` configurations."""
+"""Name of the configuration subkey key for ``GeoIP`` configuration in ``core services`` configurations."""
 CKEY_CORE_SERVICES_NERD = 'nerd'
-"""Name of the configuration subkey key for ``nerd`` configuration in ``core services`` configurations."""
+"""Name of the configuration subkey key for ``NERD`` configuration in ``core services`` configurations."""
 CKEY_CORE_SERVICES_WHOIS = 'whois'
 """Name of the configuration subkey key for ``whois`` configuration in ``core services`` configurations."""
 CKEY_CORE_SERVICES_CACHE = 'cache'
diff --git a/lib/mentat/services/dnsr.py b/lib/mentat/services/dnsr.py
index 1cf60430f..28fd6b79e 100644
--- a/lib/mentat/services/dnsr.py
+++ b/lib/mentat/services/dnsr.py
@@ -69,7 +69,7 @@ class DnsService:
                 res = str(res)
                 if res[-1] == '.':
                     res = res[:-1] # trim trailing '.'
-                result.append(res)
+                result.append({'type': 'PTR', 'value': res})
         except dns.exception.Timeout as exc:
             raise RuntimeError("DNS query for {} timed out".format(ipaddr))
         except dns.exception.DNSException as exc:
@@ -86,7 +86,7 @@ class DnsService:
                 answer = self.resolver.query(hname, qtype)
                 for res in answer.rrset:
                     res = str(res)
-                    result.append(res)
+                    result.append({'type': qtype, 'value': res})
         except dns.exception.Timeout as exc:
             raise RuntimeError("DNS query for {} timed out".format(hname))
         except dns.exception.DNSException as exc:
diff --git a/lib/mentat/services/geoip.py b/lib/mentat/services/geoip.py
index a663d1073..6f75caa0a 100644
--- a/lib/mentat/services/geoip.py
+++ b/lib/mentat/services/geoip.py
@@ -116,11 +116,17 @@ class GeoipService:
         result = {}
         if self.fn_asndb:
             result['asn'] = self.lookup_asn(ipaddr)
+            if not result['asn']:
+                del result['asn']
         if self.fn_citydb:
             result['city'] = self.lookup_city(ipaddr)
+            if not result['city']:
+                del result['city']
         if self.fn_countrydb:
             result['country'] = self.lookup_country(ipaddr)
-        return result
+            if not result['country']:
+                del result['country']
+        return result or None
 
     def lookup_asn(self, ipaddr):
         """
diff --git a/lib/mentat/services/nerd.py b/lib/mentat/services/nerd.py
index 25198e1b0..b72705507 100644
--- a/lib/mentat/services/nerd.py
+++ b/lib/mentat/services/nerd.py
@@ -79,7 +79,7 @@ class NerdService:
 
     def setup(self):
         """
-        Setup internal geolocation database readers.
+        Additional internal setup currently not necessary.
         """
         pass
 
@@ -96,21 +96,30 @@ class NerdService:
         """
         Get URL for looking up given IP address in NERD service.
         """
-        return "{}ip/{}".format(self.base_url, str(ipaddr))
+        return "{}ip/{}".format(
+            self.base_url,
+            str(ipaddr)
+        )
 
     def get_api_url_lookup_ip(self, ipaddr):
         """
         Get URL for looking up given IP address in NERD service.
         """
-        return "{}ip/{}".format(self.base_api_url, str(ipaddr))
+        return "{}ip/{}".format(
+            self.base_api_url,
+            str(ipaddr)
+        )
 
     def lookup_ip(self, ipaddr):
         """
         Lookup given IP address in NERD service.
         """
         # Prepare request to NERD API
-        url = "{}ip/{}".format(self.base_api_url, str(ipaddr))
-        headers = {"Authorization": self.api_key}
+        url = self.get_api_url_lookup_ip(ipaddr)
+        headers = {
+            "Authorization":
+            self.api_key
+        }
 
         # Send request
         try:
@@ -165,14 +174,14 @@ class NerdServiceManager:
             core_config[CKEY_CORE_SERVICES][CKEY_CORE_SERVICES_NERD]
         )
 
-        if updates and CKEY_CORE_SERVICES in updates and CKEY_CORE_SERVICES_GEOIP in updates[CKEY_CORE_SERVICES]:
+        if updates and CKEY_CORE_SERVICES in updates and CKEY_CORE_SERVICES_NERD in updates[CKEY_CORE_SERVICES]:
             self._nerdconfig.update(
                 updates[CKEY_CORE_SERVICES][CKEY_CORE_SERVICES_NERD]
             )
 
     def service(self):
         """
-        Return handle to geoip service according to internal configurations.
+        Return handle to NERD service according to internal configurations.
 
         :return: Reference to NERD service object.
         :rtype: mentat.services.nerd.NerdService
diff --git a/lib/mentat/services/pdnsr.py b/lib/mentat/services/pdnsr.py
new file mode 100644
index 000000000..43af3a009
--- /dev/null
+++ b/lib/mentat/services/pdnsr.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#-------------------------------------------------------------------------------
+# This file is part of Mentat system (https://mentat.cesnet.cz/).
+#
+# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
+# Use of this source is governed by the MIT license, see LICENSE file.
+#-------------------------------------------------------------------------------
+
+
+"""
+Implementation of internal **PassiveDNS** service connector.
+"""
+
+
+__author__ = "Jan Mach <jan.mach@cesnet.cz>"
+__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
+
+
+import copy
+import requests
+
+from mentat.const import CKEY_CORE_SERVICES, CKEY_CORE_SERVICES_PDNS
+
+
+_MANAGER = None
+
+
+class PDNSRConfigException(ValueError):
+    pass
+
+class PDNSRRuntimeException(RuntimeError):
+    pass
+
+
+class PDNSRService:
+    """
+    Implementation of internal **PassiveDNS** access service.
+    """
+
+    def __init__(self, base_url, base_api_url):
+        """
+        Initialize geolocation service with paths to desired database files.
+        """
+        self.base_url     = base_url
+        self.base_api_url = base_api_url
+
+        # Check presence and validity of config.
+        if not self.base_url:
+            raise PDNSRConfigException(
+                "PassiveDNS service is used but base URL is not configured"
+            )
+        if not (self.base_url.startswith("https://") or self.base_url.startswith("http://")):
+            raise PDNSRConfigException(
+                "Invalid PassiveDNS service base URL"
+            )
+        if not self.base_api_url:
+            raise PDNSRConfigException(
+                "PassiveDNS service is used but base API URL is not configured"
+            )
+        if not (self.base_api_url.startswith("https://") or self.base_api_url.startswith("http://")):
+            raise PDNSRConfigException(
+                "Invalid PassiveDNS service base API URL"
+            )
+
+        # Ensure both base_url and base_api_url end with slash.
+        if not self.base_url.endswith("/"):
+            self.base_url += "/"
+        if not self.base_api_url.endswith("/"):
+            self.base_api_url += "/"
+
+    def setup(self):
+        """
+        Additional internal setup currently not necessary.
+        """
+        pass
+
+    def status(self):
+        """
+        Display status of the service.
+        """
+        return {
+            'base_url': self.base_url,
+            'base_api_url': self.base_api_url,
+        }
+
+    def get_url_lookup_ip(self, ipaddr):
+        """
+        Get URL for looking up given IP address in PassiveDNS service.
+        """
+        return "{}?query={}&type=ip&since=&until=".format(
+            self.base_url,
+            str(ipaddr)
+        )
+
+    def get_api_url_lookup_ip(self, ipaddr):
+        """
+        Get API URL for looking up given IP address in PassiveDNS service.
+        """
+        return "{}ip/{}".format(
+            self.base_api_url,
+            str(ipaddr)
+        )
+
+    def lookup_ip(self, ipaddr, sortby = None, limit = None):
+        """
+        Lookup given IP address in PassiveDNS service.
+        """
+        # Prepare request to PassiveDNS API
+        url = self.get_api_url_lookup_ip(ipaddr)
+
+        # Send request
+        try:
+            resp = requests.get(url)
+        except Exception as exc:
+            raise PDNSRRuntimeException(
+                "Can't get data from PassiveDNS service: {}".format(str(exc))
+            )
+
+        if resp.status_code == requests.codes.not_found:
+            return None
+        resp.raise_for_status()
+
+        # Parse response
+        try:
+            result = resp.json()
+            if sortby:
+                field, direction = sortby.split('.')
+                reverse = True if direction == 'desc' else False
+                result = sorted(result, key = lambda x: x.get(field, None), reverse = reverse)
+            if limit and int(limit):
+                result = result[:int(limit)]
+            return result
+        except Exception as exc:
+            raise PDNSRRuntimeException(
+                "Invalid data received from PassiveDNS service: {}".format(str(exc))
+            )
+
+
+class PDNSRServiceManager:
+    """
+    Class representing a custom PDNSRServiceManager capable of understanding and
+    parsing Mentat system core configurations and enabling easy way of unified
+    bootstrapping of :py:class:`mentat.services.nerd.PDNSRService` service.
+    """
+
+    def __init__(self, core_config, updates = None):
+        """
+        Initialize PDNSRServiceManager object with full core configuration tree structure.
+
+        :param dict core_config: Mentat core configuration structure.
+        :param dict updates: Optional configuration updates (same structure as ``core_config``).
+        """
+        self._pdnsrconfig = {}
+
+        self._service = None
+
+        self._configure_pdnsr(core_config, updates)
+
+    def _configure_pdnsr(self, core_config, updates):
+        """
+        Internal sub-initialization helper: Configure database structure parameters
+        and optionally merge them with additional updates.
+
+        :param dict core_config: Mentat core configuration structure.
+        :param dict updates: Optional configuration updates (same structure as ``core_config``).
+        """
+        self._nerdconfig = copy.deepcopy(
+            core_config[CKEY_CORE_SERVICES][CKEY_CORE_SERVICES_PDNS]
+        )
+
+        if updates and CKEY_CORE_SERVICES in updates and CKEY_CORE_SERVICES_PDNS in updates[CKEY_CORE_SERVICES]:
+            self._nerdconfig.update(
+                updates[CKEY_CORE_SERVICES][CKEY_CORE_SERVICES_PDNS]
+            )
+
+    def service(self):
+        """
+        Return handle to PassiveDNS service according to internal configurations.
+
+        :return: Reference to PassiveDNS service object.
+        :rtype: mentat.services.passivednsr.PDNSRService
+        """
+        if not self._service:
+            self._service = PDNSRService(**self._nerdconfig)
+            self._service.setup()
+        return self._service
+
+
+#-------------------------------------------------------------------------------
+
+
+def init(core_config, updates = None):
+    """
+    (Re-)Initialize :py:class:`PDNSRServiceManager` instance at module level and
+    store the refence within module.
+    """
+    global _MANAGER  # pylint: disable=locally-disabled,global-statement
+    _MANAGER = PDNSRServiceManager(core_config, updates)
+
+
+def manager():
+    """
+    Obtain reference to :py:class:`NerdServiceManager` instance stored at module
+    level.
+    """
+    global _MANAGER  # pylint: disable=locally-disabled,global-statement
+    return _MANAGER
+
+
+def service():
+    """
+    Obtain reference to :py:class:`NerdService` instance from module level manager.
+    """
+    return manager().service()
diff --git a/lib/mentat/services/test_dnsr.py b/lib/mentat/services/test_dnsr.py
index f78a1a8f0..802d1043a 100644
--- a/lib/mentat/services/test_dnsr.py
+++ b/lib/mentat/services/test_dnsr.py
@@ -15,9 +15,7 @@ __author__  = "Jan Mach <jan.mach@cesnet.cz>"
 __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
 
 
-import os
 import unittest
-import subprocess
 
 #
 # Custom libraries
@@ -46,26 +44,26 @@ class TestMentatDns(unittest.TestCase):
     rdns_test_list = [
         [
             '195.113.144.233',
-            ['ns.ces.net']
+            [{'type': 'PTR', 'value': 'ns.ces.net'}]
         ],
         [
             '195.113.144.230',
-            ['www.cesnet.cz']
+            [{'type': 'PTR', 'value': 'www.cesnet.cz'}]
         ]
     ]
 
     dns_test_list = [
         [
             'ns.ces.net',
-            ['195.113.144.233', '2001:718:1:101::3']
+            [{'type': 'A', 'value': '195.113.144.233'}, {'type': 'AAAA', 'value': '2001:718:1:101::3'}]
         ],
         [
             'www.cesnet.cz',
-            ['195.113.144.230', '2001:718:1:101::4']
+            [{'type': 'A', 'value': '195.113.144.230'}, {'type': 'AAAA', 'value': '2001:718:1:101::4'}]
         ]
     ]
 
-    def test_01_lookup_ip(self):
+    def distest_01_lookup_ip(self):
         """
         Perform lookup tests by IP address.
         """
@@ -77,7 +75,7 @@ class TestMentatDns(unittest.TestCase):
         for test in self.rdns_test_list:
             self.assertEqual(dns.lookup_ip(test[0]), test[1])
 
-    def test_02_lookup_hostname(self):
+    def distest_02_lookup_hostname(self):
         """
         Perform lookup tests by hostname.
         """
@@ -89,7 +87,7 @@ class TestMentatDns(unittest.TestCase):
         for test in self.dns_test_list:
             self.assertEqual(dns.lookup_hostname(test[0]), test[1])
 
-    def test_03_lookup(self):
+    def distest_03_lookup(self):
         """
         Perform lookup tests by hostname.
         """
@@ -103,7 +101,7 @@ class TestMentatDns(unittest.TestCase):
         for test in self.dns_test_list:
             self.assertEqual(dns.lookup(test[0]), test[1])
 
-    def test_04_service_manager(self):
+    def distest_04_service_manager(self):
         """
         Perform full lookup tests with service obtained by manually configured service manager.
         """
@@ -124,7 +122,7 @@ class TestMentatDns(unittest.TestCase):
         for test in self.rdns_test_list:
             self.assertEqual(dns.lookup_ip(test[0]), test[1])
 
-    def test_05_module_service(self):
+    def distest_05_module_service(self):
         """
         Perform full lookup tests with service obtained by module interface.
         """
diff --git a/lib/mentat/services/test_nerd.py b/lib/mentat/services/test_pdnsr.py
similarity index 53%
rename from lib/mentat/services/test_nerd.py
rename to lib/mentat/services/test_pdnsr.py
index 78513b5e6..d872b3a33 100644
--- a/lib/mentat/services/test_nerd.py
+++ b/lib/mentat/services/test_pdnsr.py
@@ -8,7 +8,7 @@
 #-------------------------------------------------------------------------------
 
 """
-Unit test module for testing the :py:mod:`mentat.services.nerd` module.
+Unit test module for testing the :py:mod:`mentat.services.passivednsrr` module.
 """
 
 
@@ -16,14 +16,13 @@ __author__  = "Jan Mach <jan.mach@cesnet.cz>"
 __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
 
 
-import os
 import pprint
 import unittest
 
 #
 # Custom libraries
 #
-import mentat.services.nerd
+import mentat.services.pdnsr
 
 
 #-------------------------------------------------------------------------------
@@ -32,9 +31,9 @@ import mentat.services.nerd
 #-------------------------------------------------------------------------------
 
 
-class TestMentatNerd(unittest.TestCase):
+class TestMentatPassiveDNS(unittest.TestCase):
     """
-    Unit test class for testing the :py:mod:`mentat.services.nerd` module.
+    Unit test class for testing the :py:mod:`mentat.services.passivednsrr` module.
     """
 
     #
@@ -44,79 +43,78 @@ class TestMentatNerd(unittest.TestCase):
     #
     verbose = True
 
-    nerd_test_list = [
-        [
-            '193.32.163.89', {
+    pdnsr_test_list = [
+        #[
+        #    '195.113.144.233', {
 
-            }
-        ],
+        #    }
+        #],
         [
-            '0.0.0.0', {
+            '195.113.144.238', {
 
             }
         ],
     ]
 
-    def disabletest_01_lookup_ip(self):
+    def distest_01_lookup_ip(self):
         """
         Perform IP lookup tests.
         """
         self.maxDiff = None
 
-        nerd = mentat.services.nerd.NerdService(
-            base_url = "https://nerd.cesnet.cz/nerd/api/v1/",
-            base_api_url = "https://nerd.cesnet.cz/nerd/",
-            api_key = "DKHziihnuN"
+        pdnsr = mentat.services.pdnsr.PDNSRService(
+            base_url = "https://passivedns.cesnet.cz/",
+            base_api_url = "https://passivedns.cesnet.cz/api/v0/"
         )
-        nerd.setup()
+        pdnsr.setup()
 
-        for test in self.nerd_test_list:
-            pprint.pprint(nerd.lookup_ip(test[0]))
-            #self.assertEqual(nerd.lookup_ip(test[0]), test[1])
+        for test in self.pdnsr_test_list:
+            pprint.pprint(pdnsr.lookup_ip(test[0]))
+            #self.assertEqual(pdnsr.lookup_ip(test[0]), test[1])
 
-    def disabletest_02_service_manager(self):
+    def distest_02_service_manager(self):
         """
         Perform full lookup tests with service obtained by manually configured service manager.
         """
         self.maxDiff = None
 
-        manager = mentat.services.nerd.NerdServiceManager(
+        manager = mentat.services.pdnsr.PDNSRServiceManager(
             {
                 "__core__services": {
-                    "nerd": {
-                        "base_url": "https://nerd.cesnet.cz/nerd/api/v1/",
-                        "api_key": "DKHziihnuN"
+                    "passivedns": {
+                        "base_url": "https://passivedns.cesnet.cz/",
+                        "base_api_url": "https://passivedns.cesnet.cz/api/v0/"
                     }
                 }
             }
         )
 
-        nerd = manager.service()
-        for test in self.nerd_test_list:
-            pprint.pprint(nerd.lookup_ip(test[0]))
-            #self.assertEqual(nerd.lookup(test[0]), test[1])
+        pdnsr = manager.service()
+        for test in self.pdnsr_test_list:
+            pprint.pprint(pdnsr.lookup_ip(test[0]))
+            #self.assertEqual(pdnsr.lookup(test[0]), test[1])
 
-    def disabletest_03_module_service(self):
+    def distest_03_module_service(self):
         """
         Perform full lookup tests with service obtained by module interface.
         """
         self.maxDiff = None
 
-        mentat.services.nerd.init(
+        mentat.services.pdnsr.init(
             {
                 "__core__services": {
-                    "nerd": {
-                        "base_url": "https://nerd.cesnet.cz/nerd/api/v1/",
-                        "api_key": "DKHziihnuN"
+                    "passivedns": {
+                        "base_url": "https://passivedns.cesnet.cz/",
+                        "base_api_url": "https://passivedns.cesnet.cz/api/v0/"
                     }
                 }
             }
         )
 
-        nerd = mentat.services.nerd.service()
-        for test in self.nerd_test_list:
-            pprint.pprint(nerd.lookup_ip(test[0]))
-            #self.assertEqual(nerd.lookup(test[0]), test[1])
+        pdnsr = mentat.services.pdnsr.service()
+        for test in self.pdnsr_test_list:
+            pprint.pprint(pdnsr.lookup_ip(test[0]))
+            #self.assertEqual(pdnsr.lookup(test[0]), test[1])
 
 
 #-------------------------------------------------------------------------------
diff --git a/package.json b/package.json
index 062d25f91..ae5924a27 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
         "jquery": "^3.3.1",
         "moment": "^2.23.0",
         "moment-timezone": "^0.5.23",
-        "nvd3": "^1.8.6"
+        "nvd3": "^1.8.6",
+        "popper.js": "^1.15.0"
     }
 }
diff --git a/yarn.lock b/yarn.lock
index f95fb5af3..d72b5ccf4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -50,11 +50,6 @@ ajv@^6.5.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ansi-regex@*:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
-  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
-
 ansi-regex@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -1468,7 +1463,7 @@ iferr@^0.1.5:
   resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
   integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
 
-imurmurhash@*, imurmurhash@^0.1.4:
+imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -2328,6 +2323,11 @@ plur@^1.0.0:
   resolved "https://registry.yarnpkg.com/plur/-/plur-1.0.0.tgz#db85c6814f5e5e5a3b49efc28d604fec62975156"
   integrity sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY=
 
+popper.js@^1.15.0:
+  version "1.15.0"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
+  integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
+
 pretty-ms@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-2.1.0.tgz#4257c256df3fb0b451d6affaab021884126981dc"
-- 
GitLab