From 04c7473e5d106f92e13db67526aba3ac7b789e22 Mon Sep 17 00:00:00 2001
From: Jan Mach <jan.mach@cesnet.cz>
Date: Fri, 5 Jan 2018 11:49:57 +0100
Subject: [PATCH] Implemented working prototype of report access module for
 Hawat.

This implementation still needs lots of polishing and there are following known major issues:

* invalid encoding of report message text coming from migrated MongoDB records
* some parts of the interface are not working yet (report remailing, report deletion)
* unauthorized access to reports is not working
* report data browser is not yet implemented
* report search result statistics are not yet implemented

These issues will be resolved in separate commits. (Redmine issue: #3734)
---
 conf/core/reporting.json.conf                 |   8 +
 lib/hawat/base.py                             |  34 +++
 .../design/templates/_macros_site.html        |  42 +++-
 lib/hawat/blueprints/reports/__init__.py      | 135 ++++++++++--
 lib/hawat/blueprints/reports/forms.py         |  51 +++++
 .../reports/templates/reports/search.html     |  94 ++++++--
 .../reports/templates/reports/show.html       | 202 ++++++++++++++++++
 lib/hawat/const.py                            |  11 +-
 lib/mentat/const.py                           |  16 ++
 9 files changed, 552 insertions(+), 41 deletions(-)
 create mode 100644 conf/core/reporting.json.conf
 create mode 100644 lib/hawat/blueprints/reports/forms.py
 create mode 100644 lib/hawat/blueprints/reports/templates/reports/show.html

diff --git a/conf/core/reporting.json.conf b/conf/core/reporting.json.conf
new file mode 100644
index 000000000..a12b75663
--- /dev/null
+++ b/conf/core/reporting.json.conf
@@ -0,0 +1,8 @@
+{
+    #
+    # Definitions of core configurations for event reporting.
+    #
+    "__core__reporting": {
+        "reports_dir": "/var/mentat/reports/reporter-ng"
+    }
+}
diff --git a/lib/hawat/base.py b/lib/hawat/base.py
index 345f07a71..d30a2467e 100644
--- a/lib/hawat/base.py
+++ b/lib/hawat/base.py
@@ -231,6 +231,40 @@ class HawatFileView(HawatBaseView):
         )
 
 
+class HawatFileIdView(HawatBaseView):
+    """
+    Base class for indirrect file access views. These views can be used to access
+    and serve files from arbitrary filesystem directories (that are accessible to
+    application process). This can be very usefull for serving files like charts,
+    that are periodically generated into configurable and changeable location.
+    """
+
+    @staticmethod
+    def get_directory_path():
+        """
+        *Hook method*. Must return full path to the directory, that will be used
+        as a base path for serving files.
+        """
+        raise NotImplementedError()
+
+    def get_filename(self, fileid, filetype):
+        """
+        *Hook method*.
+        """
+        raise NotImplementedError()
+
+    def dispatch_request(self, fileid, filetype):
+        """
+        Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`.
+        Will be called by the *Flask* framework to service the request.
+        """
+        return flask.send_from_directory(
+            self.get_directory_path(),
+            self.get_filename(fileid, filetype),
+            as_attachment=True
+        )
+
+
 class HawatSimpleView(HawatBaseView):
     """
     Base class for simple views. These are the most, well, simple views, that are
diff --git a/lib/hawat/blueprints/design/templates/_macros_site.html b/lib/hawat/blueprints/design/templates/_macros_site.html
index ee21c782b..1733839c0 100644
--- a/lib/hawat/blueprints/design/templates/_macros_site.html
+++ b/lib/hawat/blueprints/design/templates/_macros_site.html
@@ -375,7 +375,7 @@
                                     {{ chitemkey }}
                                 </td>
                                 <td class="col-value">
-                                    {{ babel_format_decimal(chdata[chitemkey]) }}
+                                    {{ babel_format_decimal(chdata.get(chitemkey, 0)) }}
                                 </td>
                                 <td>
 
@@ -387,7 +387,7 @@
                             <tr>
                                 <th>&nbsp;</th>
                                 <th>Sum</th>
-                                <th>{{ babel_format_decimal(chstats['sum_' + chstatkey]) }}</th>
+                                <th>{{ babel_format_decimal(chstats.get('sum_' + chstatkey, 0)) }}</th>
                                 <th></th>
                             </tr>
                          </tfoot>
@@ -398,23 +398,23 @@
                     <table class="table table-bordered table-striped table-condensed">
                         <tr>
                             <th>{{ get_fa_icon('min') }} Min</th>
-                            <td class="col-value">{{ babel_format_decimal(chstats['min_' + chstatkey]) }}</td>
+                            <td class="col-value">{{ babel_format_decimal(chstats.get('min_' + chstatkey, 0)) }}</td>
                         </tr>
                         <tr>
                             <th>{{ get_fa_icon('max') }} Max</th>
-                            <td class="col-value">{{ babel_format_decimal(chstats['max_' + chstatkey]) }}</td>
+                            <td class="col-value">{{ babel_format_decimal(chstats.get('max_' + chstatkey, 0)) }}</td>
                         </tr>
                         <tr>
                             <th>{{ get_fa_icon('sum') }} Sum</th>
-                            <td class="col-value">{{ babel_format_decimal(chstats['sum_' + chstatkey]) }}</td>
+                            <td class="col-value">{{ babel_format_decimal(chstats.get('sum_' + chstatkey, 0)) }}</td>
                         </tr>
                         <tr>
                             <th>{{ get_fa_icon('cnt') }} Cnt</th>
-                            <td class="col-value">{{ babel_format_decimal(chstats['cnt_' + chstatkey]) }}</td>
+                            <td class="col-value">{{ babel_format_decimal(chstats.get('cnt_' + chstatkey, 0)) }}</td>
                         </tr>
                         <tr>
                             <th>{{ get_fa_icon('avg') }} Avg</th>
-                            <td class="col-value">{{ babel_format_decimal(chstats['avg_' + chstatkey]) }}</td>
+                            <td class="col-value">{{ babel_format_decimal(chstats.get('avg_' + chstatkey, 0)) }}</td>
                         </tr>
                     </table>
 
@@ -422,3 +422,31 @@
             </div>
 
 {%- endmacro %}
+
+
+{#- ----------------------------------------------------------------------------
+
+    Macros for rendering event report related widgets.
+
+----------------------------------------------------------------------------- #}
+
+
+{%- macro render_report_label_type(report) %}
+    {%- if report.type == 'summary' %}
+<span class="label label-default" title="{{ gettext('Summary report') }}" data-toggle="tooltip">{{ get_fa_icon('r-t-summary') }}</span>
+    {%- elif report.type == 'extra' %}
+<span class="label label-default" title="{{ gettext('Extra report') }}" data-toggle="tooltip">{{ get_fa_icon('r-t-extra') }}</span>
+    {%- endif %}
+{%- endmacro %}
+
+{%- macro render_report_label_severity(report) %}
+    {%- if report.severity == 'low' %}
+<span class="label label-default" title="{{ gettext('Low severity') }}" data-toggle="tooltip">{{ get_fa_icon('r-s-low') }}</span>
+    {%- elif report.severity == 'medium' %}
+<span class="label label-info" title="{{ gettext('Medium severity') }}" data-toggle="tooltip">{{ get_fa_icon('r-s-medium') }}</span>
+    {%- elif report.severity == 'high' %}
+<span class="label label-warning" title="{{ gettext('High severity') }}" data-toggle="tooltip">{{ get_fa_icon('r-s-high') }}</span>
+    {%- elif report.severity == 'critical' %}
+<span class="label label-danger" title="{{ gettext('Critical severity') }}" data-toggle="tooltip">{{ get_fa_icon('r-s-critical') }}</span>
+    {%- endif %}
+{%- endmacro %}
diff --git a/lib/hawat/blueprints/reports/__init__.py b/lib/hawat/blueprints/reports/__init__.py
index 1eeddb0cd..e8ad021b5 100644
--- a/lib/hawat/blueprints/reports/__init__.py
+++ b/lib/hawat/blueprints/reports/__init__.py
@@ -14,7 +14,7 @@ Hawat pluggable module: *reports*
 Description
 ^^^^^^^^^^^
 
-This pluggable module provides access to generated reports.
+This pluggable module provides access to event reports.
 
 """
 
@@ -23,18 +23,23 @@ __author__ = "Jan Mach <jan.mach@cesnet.cz>"
 __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
 
 
+import collections
+
 import flask
 import flask_login
 from flask_babel import lazy_gettext
 
+import mentat.const
+import mentat.stats.idea
 import hawat.base
-import hawat.db
+
+from mentat.datatype.sqldb import EventReportModel
+from hawat.blueprints.reports.forms import EventReportSearchForm
 
 
-class SearchView(hawat.base.HawatBaseView):
+class SearchView(hawat.base.HawatSearchView):
 
     decorators = [flask_login.login_required]
-    methods = ['GET']
 
     @staticmethod
     def get_view_name():
@@ -56,16 +61,116 @@ class SearchView(hawat.base.HawatBaseView):
     def get_menu_title():
         return lazy_gettext('Reports')
 
-    def get_template_context(self):
-        context = super().get_template_context()
+    @property
+    def dbmodel(self):
+        """
+        *Hook property*. Implementation of :py:func:`hawat.base.HawatDbmodelView.dbmodel` interface.
+        """
+        return EventReportModel
+
+    @staticmethod
+    def get_search_form(args):
+        return EventReportSearchForm(args, meta = {'csrf': False})
+
+    def search(self, query, model, args, context):
+        if 'dt_from' in args and args['dt_from']:
+            query = query.filter(model.dt_from >= args['dt_from'])
+        if 'dt_to' in args and args['dt_to']:
+            query = query.filter(model.dt_to <= args['dt_to'])
+        return query.order_by(model.dt_to.desc()).order_by(model.label.desc()).all()
+
+    def do_before_render(self, items, context):
+        """
+        *Hook method*. Will be called before rendering the template.
+        """
+        #context.update(
+        #    statistics = mentat.stats.idea.truncate_evaluations(
+        #        mentat.stats.idea.aggregate_stat_groups(items)
+        #    )
+        #)
+
+class ShowView(hawat.base.HawatItemShowView):
+    """
+    Detailed report view.
+    """
+
+    decorators = [flask_login.login_required]
+    methods = ['GET']
+
+    @staticmethod
+    def get_view_name():
+        return 'show'
+
+    @staticmethod
+    def get_view_title():
+        return lazy_gettext('Show report')
+
+    @staticmethod
+    def get_view_template():
+        return 'reports/show.html'
 
-        return context
+    @staticmethod
+    def get_menu_icon():
+        return 'reports'
 
-    def dispatch_request(self):
-        context = self.get_template_context()
+    @staticmethod
+    def get_menu_title():
+        return lazy_gettext('Show report')
 
-        return flask.render_template(self.get_view_template(), **context)
+    #---------------------------------------------------------------------------
 
+    @property
+    def dbmodel(self):
+        """
+        *Hook property*. Implementation of :py:func:`hawat.base.HawatDbmodelView.dbmodel` interface.
+        """
+        return EventReportModel
+
+
+class UnauthShowView(ShowView):
+    """
+    Unauthorized access to report detail view.
+    """
+    pass
+
+
+class DataView(hawat.base.HawatFileIdView):
+
+    decorators = [flask_login.login_required]
+
+    @staticmethod
+    def get_view_name():
+        return 'data'
+
+    @staticmethod
+    def get_view_title():
+        return lazy_gettext('Event report data')
+
+    @staticmethod
+    def get_menu_icon():
+        return 'reports'
+
+    @staticmethod
+    def get_menu_title():
+        return lazy_gettext('Event report data')
+
+    @staticmethod
+    def get_directory_path():
+        return flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTING][mentat.const.CKEY_CORE_REPORTING_REPORTSDIR]
+
+    def get_filename(self, fileid, filetype):
+        fileext = ''
+        if filetype == 'json':
+            fileext = 'json'
+        elif filetype == 'jsonzip':
+            fileext = 'json.zip'
+        elif filetype == 'csv':
+            fileext = 'csv'
+        elif filetype == 'csvzip':
+            fileext = 'csv.zip'
+        else:
+            raise ValueError("Requested invalid data file type '{}'".format(filetype))
+        return 'security_report_{}.{}'.format(fileid, fileext)
 
 #-------------------------------------------------------------------------------
 
@@ -74,7 +179,7 @@ class ReportsBlueprint(hawat.base.HawatBlueprint):
 
     @staticmethod
     def get_module_title():
-        return lazy_gettext('Event reports pluggable module')
+        return lazy_gettext('Event report')
 
     def register_app(self, app):
         app.menu_main.add_entry('reports', content = self.hawat_view_list['search'], position = 120)
@@ -89,12 +194,14 @@ def get_blueprint():
     instance of :py:class:`hawat.base.HawatBlueprint` or :py:class:`flask.Blueprint`.
     """
 
-    bp = ReportsBlueprint(
+    hbp = ReportsBlueprint(
         'reports',
         __name__,
         template_folder = 'templates',
         url_prefix = '/reports')
 
-    bp.register_view_class(SearchView, '/search')
+    hbp.register_view_class(SearchView, '/search')
+    hbp.register_view_class(ShowView, '/<int:item_id>/show')
+    hbp.register_view_class(DataView, '/data/<fileid>/<filetype>')
 
-    return bp
+    return hbp
diff --git a/lib/hawat/blueprints/reports/forms.py b/lib/hawat/blueprints/reports/forms.py
new file mode 100644
index 000000000..bb2eb6d4f
--- /dev/null
+++ b/lib/hawat/blueprints/reports/forms.py
@@ -0,0 +1,51 @@
+#!/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 internal geoip 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 time
+import datetime
+import ipranges
+import wtforms
+
+import flask_wtf
+from flask_babel import lazy_gettext, gettext
+
+import hawat.forms
+
+class EventReportSearchForm(flask_wtf.FlaskForm):
+    """
+    Class representing event report search form.
+    """
+    dt_from = hawat.forms.DateTimeLocalField(
+        lazy_gettext('From:'),
+        validators = [
+            wtforms.validators.Optional()
+        ],
+        format = '%Y-%m-%d %H:%M',
+        default = datetime.datetime.fromtimestamp(int(time.time() - (time.time() % 86400)))
+    )
+    dt_to = hawat.forms.DateTimeLocalField(
+        lazy_gettext('To:'),
+        validators = [
+            wtforms.validators.Optional()
+        ],
+        format = '%Y-%m-%d %H:%M'
+    )
+    submit = wtforms.SubmitField(
+        lazy_gettext('Search')
+    )
diff --git a/lib/hawat/blueprints/reports/templates/reports/search.html b/lib/hawat/blueprints/reports/templates/reports/search.html
index 79914970e..46681e49c 100644
--- a/lib/hawat/blueprints/reports/templates/reports/search.html
+++ b/lib/hawat/blueprints/reports/templates/reports/search.html
@@ -2,21 +2,81 @@
 
 {% block content %}
 
-<div class="row">
-    <div class="col-lg-12">
-        <h2>{{ hawat_view_title }}</h2>
-
-        <form class="form-inline" id="frm-whi" method="get" action="{{ url_for('reports.search') }}">
-            <div class="form-group">
-                <label class="sr-only" for="frm-whi-search">Search local whois:</label>
-                <div class="input-group">
-                    <div data-toggle="tooltip" class="input-group-addon" title="Search local whois">{{ get_fa_icon('search') }}</div>
-                    <input class="form-control" type="search" id="frm-whi-search" name="search" placeholder="IP address, range, network or abuse email" value="" size="40">
+            <div class="row">
+                <div class="col-lg-12">
+
+                    <ol class="breadcrumb">
+                        <li><a href="{{ url_for('index') }}">{{ gettext('Home') }}</a></li>
+                        <li class="active">{{ gettext('Reports') }}</li>
+                    </ol>
+                    <div class="jumbotron" style="margin-top: 1em;">
+                        <h2>{{ hawat_view_title }}</h2>
+                        <hr>
+                        <form method="GET" class="form-inline" action="{{ url_for('reports.search') }}">
+                            {{ macros_site.render_form_item_datetime(search_form.dt_from, 'datetimepicker-hm-from', False) }}
+
+                            {{ macros_site.render_form_item_datetime(search_form.dt_to, 'datetimepicker-hm-to', False) }}
+
+                            {{ search_form.submit(class_='btn btn-primary') }}
+                        </form>
+                        {%- if search_form.dt_from.errors %}
+                        <div>
+                            {{ macros_site.form_errors(search_form.dt_from.errors) }}
+                        </div>
+                        {%- endif %}
+                        {%- if search_form.dt_to.errors %}
+                        <div>
+                            {{ macros_site.form_errors(search_form.dt_to.errors) }}
+                        </div>
+                        {%- endif %}
+                    </div> <!-- /.jumbotron -->
+
+                </div> <!-- /.col-lg-12 -->
+            </div> <!-- /.row -->
+
+    {%- if items %}
+
+            <div class="row">
+                <div class="col-lg-12">
+
+                <div class="list-group">
+                    {%- for item in items %}
+                    <a href="{{ url_for('reports.show', item_id = item.id ) }}" class="list-group-item">
+                        <h4 class="list-group-item-heading">
+                            {{ macros_site.render_report_label_type(item) }}
+                            {{ macros_site.render_report_label_severity(item) }}
+                            {{ item.label }}
+                        {%- if item.flag_mailed %}
+                            <span title="{{ item.mail_res }}, {{ babel_format_datetime(item.mail_dt) }}" data-toggle="tooltip">{{ get_fa_icon('mail') }}</span>
+                        {%- endif %}
+                        {%- if item.flag_testdata %}
+                            <span title="{{ gettext('Report was generated from test data') }}" data-toggle="tooltip">{{ get_fa_icon('debug') }}</span>
+                        {%- endif %}
+                        </h4>
+                        <p class="list-group-item-text">
+                            {{ gettext('Abuse group:') }} {{ item.group.name }}
+                            &nbsp;|&nbsp;
+                            {{ gettext('Report window:') }} {{ babel_format_datetime(item.dt_from) }} - {{ babel_format_datetime(item.dt_to) }} ({{ babel_format_timedelta(item.delta) }})
+                            &nbsp;|&nbsp;
+                            {{ gettext('Created:') }} {{ babel_format_datetime(item.createtime) }} ({{ gettext('before') }} {{ babel_format_timedelta(current_datetime_utc - item.createtime) }})
+                        </p>
+                    </a>
+                    {%- endfor %}
                 </div>
-            </div>
-            <input class="btn btn-primary" type="submit" name="submit" value="Search">
-        </form>
-    </div>
-    <!-- /.col-lg-12 -->
-</div>
-{% endblock content %}
+
+                </div> <!-- /.col-lg-12 -->
+            </div> <!-- /.row -->
+
+        {%- if permission_can('developer') %}
+
+            <hr>
+
+{{ macros_site.render_raw_item_view(items) }}
+
+        {%- endif %}
+
+    {%- else %}
+            <div class="alert alert-info">{{ gettext('No data matches your search criteria.') }}</div>
+    {%- endif %}
+
+{%- endblock content %}
diff --git a/lib/hawat/blueprints/reports/templates/reports/show.html b/lib/hawat/blueprints/reports/templates/reports/show.html
new file mode 100644
index 000000000..1d1cb8352
--- /dev/null
+++ b/lib/hawat/blueprints/reports/templates/reports/show.html
@@ -0,0 +1,202 @@
+{% extends "_layout.html" %}
+
+{% block content %}
+
+            <div class="row">
+                <div class="col-lg-12">
+
+                    <ol class="breadcrumb">
+                        <li><a href="{{ url_for('index') }}">{{ gettext('Home') }}</a></li>
+                        <li><a href="{{ url_for('reports.search') }}">{{ gettext('Reports') }}</a></li>
+                        <li class="active">{{ gettext('Report detail') }}</li>
+                    </ol>
+                    <h2>{{ hawat_view_title }}</h2>
+                    <hr>
+                    <h3>
+                        {{ item.label }}
+                        {{ macros_site.render_report_label_type(item) }}
+                        {{ macros_site.render_report_label_severity(item) }}
+                        {%- if item.flag_mailed %}
+                            <span class="label label-default" title="{{ item.mail_res }}, {{ babel_format_datetime(item.mail_dt) }}" data-toggle="tooltip">{{ get_fa_icon('mail') }}</span>
+                        {%- endif %}
+                    </h3>
+                    {%- if item.flag_testdata %}
+                    <p class="alert alert-warning">{{ get_fa_icon('debug') }} {{ gettext('This report was generated from test data.') }}</p>
+                    {%- endif %}
+                    <p>
+                        <strong>{{ gettext('Target abuse group:')}}</strong>
+                        {{ item.group.name }}
+                    </p>
+                    <p>
+                        <strong>{{ gettext('Unprotected access link:')}}</strong>
+                        {{ item.handle }}
+                    </p>
+                    <div class="pull-right">
+                        <div class="btn-toolbar" role="toolbar" aria-label="{{ gettext('Action toolbar') }}">
+                            <div class="btn-group" role="group" aria-label="{{ gettext('Action buttons') }}">
+                                <a data-toggle="tooltip" role="button" class="btn btn-default btn-sm" href="{{ url_for('reports.show', item_id = item.id ) }}" title="{{ gettext('Remail report &quot;%(item)s&quot;', item = item.label) }}">{{ get_fa_icon('mail') }} {{ gettext('Remail') }}</a>
+                                <a data-toggle="tooltip" role="button" class="btn btn-default btn-sm" href="{{ url_for('reports.show', item_id = item.id ) }}" title="{{ gettext('Delete report &quot;%(item)s&quot;', item = item.label) }}">{{ get_fa_icon('trash') }} {{ gettext('Delete') }}</a>
+                                <div class="btn-group" role="group">
+                                    <a href="#" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
+                                        {{ get_fa_icon('more') }} {{ gettext('More')}} <span class="caret"></span>
+                                    </a>
+                                    <ul class="dropdown-menu dropdown-menu-right">
+                                        <li><a data-toggle="tooltip" href="{{ url_for('reports.data', fileid = item.label, filetype = 'json' ) }}" title="{{ gettext('Download data in JSON format for report &quot;%(item)s&quot;', item = item.label) }}">{{ get_fa_icon('save') }} {{ gettext('Download report data in JSON') }}</a></li>
+                                        <li><a data-toggle="tooltip" href="{{ url_for('reports.data', fileid = item.label, filetype = 'csv' ) }}" title="{{ gettext('Download data in CSV format for report &quot;%(item)s&quot;', item = item.label) }}">{{ get_fa_icon('save') }} {{ gettext('Download report data in CSV') }}</a></li>
+                                        <li role="separator" class="divider"></li>
+                                        <li><a data-toggle="tooltip" href="{{ url_for('reports.data', fileid = item.label, filetype = 'jsonzip' ) }}" title="{{ gettext('Download compressed data in JSON format for report &quot;%(item)s&quot;', item = item.label) }}">{{ get_fa_icon('save') }} {{ gettext('Download compressed report data in JSON') }}</a></li>
+                                        <li><a data-toggle="tooltip" href="{{ url_for('reports.data', fileid = item.label, filetype = 'csvzip' ) }}" title="{{ gettext('Download compressed data in CSV format for report &quot;%(item)s&quot;', item = item.label) }}">{{ get_fa_icon('save') }} {{ gettext('Download compressed report data in CSV') }}</a></li>
+                                    </ul>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <p>
+                        <small>
+                            {{ gettext('Report created:') }} {{ babel_format_datetime(item.createtime) }} ({{ gettext('before') }} {{ babel_format_timedelta(current_datetime_utc - item.createtime) }})
+                            &nbsp;|&nbsp;
+                            {{ gettext('Report window:') }} {{ babel_format_datetime(item.dt_from) }} - {{ babel_format_datetime(item.dt_to) }} ({{ babel_format_timedelta(item.delta) }})
+                        </small>
+                    </p>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-lg-12">
+
+                    <!-- Nav tabs -->
+                    <ul class="nav nav-tabs" role="tablist">
+                        <li role="presentation" class="active">
+                            <a href="#tab-report-message" aria-controls="tab-report-message" role="tab" data-toggle="tab">
+                                <strong>{{ gettext('Message') }}</strong>
+                            </a>
+                        </li>
+                        <li role="presentation">
+                            <a href="#tab-report-metadata" aria-controls="tab-report-metadata" role="tab" data-toggle="tab">
+                                <strong>{{ gettext('Metadata') }}</strong>
+                            </a>
+                        </li>
+                        <li role="presentation">
+                            <a href="#tab-report-stats" aria-controls="tab-report-stats" role="tab" data-toggle="tab">
+                                <strong>{{ gettext('Statistics') }}</strong>
+                            </a>
+                        </li>
+                        <li role="presentation">
+                            <a href="#tab-report-data-json" aria-controls="tab-report-data-json" role="tab" data-toggle="tab">
+                                <strong>{{ gettext('Data') }}</strong>
+                            </a>
+                        </li>
+                    </ul>
+
+                    <!-- Tab panes -->
+                    <div class="tab-content">
+
+                        <div role="tabpanel" class="tab-pane fade in active" id="tab-report-message">
+                            <samp>
+                                {{ item.message | replace("\n", "<br/>\n") | replace(' ', '&nbsp;') | replace("\t", '&nbsp;&nbsp;&nbsp;&nbsp;') | safe }}
+                            </samp>
+                        </div>
+
+                        <div role="tabpanel" class="tab-pane fade" id="tab-report-metadata">
+                            <table class="table table-condensed table-striped">
+                                <tr>
+                                    <th>{{ gettext('Type:') }}</th>
+                                    <td>{{ gettext(item.type) }}</td>
+                                </tr>
+                                <tr>
+                                    <th>{{ gettext('Severity:') }}</th>
+                                    <td>{{ gettext(item.severity) }}</td>
+                                </tr>
+                                <tr>
+                                    <th>{{ gettext('Report window:') }}</th>
+                                    <td>{{ babel_format_datetime(item.dt_from) }} - {{ babel_format_datetime(item.dt_to) }} ({{ babel_format_timedelta(item.delta) }})</td>
+                                </tr>
+                                <tr>
+                                    <th>{{ gettext('Report delay:') }}</th>
+                                    <td>{{ babel_format_timedelta(item.createtime - item.dt_to) }}</td>
+                                </tr>
+                                <tr>
+                                    <th>{{ gettext('Event counts:') }}</th>
+                                    <td>{{ item.statistics['cnt_alerts'] }}{% if item.evcount_all %} ({{ item.evcount_all }} {{ gettext('matched') }}, {{ item.evcount_flt_blk }} {{ gettext('filtered out') }}, {{ item.evcount_thr_blk }} {{ gettext('thresholded') }}, {{ item.evcount_rlp }} {{ gettext('relapsed') }}){% endif %}</td>
+                                </tr>
+                                <tr>
+                                    <th>{{ gettext('IP address counts:') }}</th>
+                                    <td>{{ item.statistics['cnt_ips'] }}</td>
+                                </tr>
+                                {%- if item.flag_mailed %}
+                                <tr>
+                                    <th>{{ gettext('Target mail:') }}</th>
+                                    <td>{{ item.mail_res }}, {{ babel_format_datetime(item.mail_dt) }}</td>
+                                </tr>
+                                {%- endif %}
+                            </table>
+                        </div>
+
+                        <div role="tabpanel" class="tab-pane fade" id="tab-report-stats">
+
+                            <ul class="nav nav-tabs nav-tabs-tooltipped" role="tablist">
+                                {%- for chsection in ('analyzers', 'asns', 'categories', 'category_sets', 'countries', 'detectors', 'detectorsws', 'ips') %}
+                                <li role="presentation" class="text-center{% if loop.first %} active{% endif %}">
+                                    <a role="tab" data-toggle="tab" href="#tab-report-stats-{{ chsection }}" class="chart-tab">
+                                        # {{ chsection }}
+                                    </a>
+                                </li>
+                                {%- endfor %}
+                            </ul>
+                            <div class="tab-content">
+                                {%- for chsection in ('analyzers', 'asns', 'categories', 'category_sets', 'countries', 'detectors', 'detectorsws', 'ips') %}
+                                <div role="tabpanel" class="tab-pane fade{% if loop.first %} in active{% endif %}" id="tab-report-stats-{{ chsection }}">
+                                    {{ macros_site.render_chart_pie(
+                                            'report_stats',
+                                            item.statistics,
+                                            chsection,
+                                            'Number of events per ' + chsection,
+                                        )
+                                    }}
+                                </div>
+                                {%- endfor %}
+                            </div>
+                        </div>
+
+                        <div role="tabpanel" class="tab-pane fade" id="tab-report-data-json">
+                            <samp>
+                                YET TO DO...
+                            </samp>
+                        </div>
+
+                    </div>
+
+                </div>
+            </div>
+
+{% endblock content %}
+
+{%- block css %}
+{{ super() }}
+        <link rel="stylesheet" href="{{ url_for('design.static', filename='vendor/nvd3/css/nv.d3.min.css') }}">
+{%- endblock css %}
+
+
+{%- block headjs %}
+{{ super() }}
+        <script src="{{ url_for('design.static', filename='vendor/d3/js/d3.min.js') }}"></script>
+        <script src="{{ url_for('design.static', filename='vendor/nvd3/js/nv.d3.min.js') }}"></script>
+{%- endblock headjs %}
+
+
+{%- block js %}
+{{ super() }}
+        <script src="{{ url_for('design.static', filename='vendor/datatables/js/jquery.dataTables.js') }}"></script>
+        <script src="{{ url_for('design.static', filename='vendor/datatables/js/dataTables.bootstrap.js') }}"></script>
+
+        <script>
+            $(document).ready(function() {
+                $('.hawat-datatable-nopaging').DataTable({
+                    "responsive": true,
+                    "pageLength": 50,
+                    "paging": false,
+                    "ordering": true
+                });
+            });
+        </script>
+{%- endblock js %}
diff --git a/lib/hawat/const.py b/lib/hawat/const.py
index 93a88eae3..2d13f1502 100644
--- a/lib/hawat/const.py
+++ b/lib/hawat/const.py
@@ -115,9 +115,14 @@ FA_ICONS = {
 
     'modal-question': '<i class="fa fa-fw fa-question-circle"></i>',
 
-    'briefs':      '<i class="fa fa-fw fa-trophy"></i>',
-    'rsummary':    '<i class="fa fa-fw fa-archive" title="Summary report" data-toggle="tooltip"></i>',
-    'rextra':      '<i class="fa fa-fw fa-file" title="Extra report" data-toggle="tooltip"></i>',
+    'r-t-summary':  '<i class="fa fa-fw fa-archive"></i>',
+    'r-t-extra':    '<i class="fa fa-fw fa-file"></i>',
+    'r-s-low':      '<i class="fa fa-fw fa-thermometer-1"></i>',
+    'r-s-medium':   '<i class="fa fa-fw fa-thermometer-2"></i>',
+    'r-s-high':     '<i class="fa fa-fw fa-thermometer-3"></i>',
+    'r-s-critical': '<i class="fa fa-fw fa-thermometer-4"></i>',
+
+    'debug':       '<i class="fa fa-fw fa-bug"></i>',
     'eventclss':   '<i class="fa fa-fw fa-book"></i>',
     'reference':   '<i class="fa fa-fw fa-external-link"></i>',
     'anchor':      '<i class="fa fa-fw fa-anchor"></i>',
diff --git a/lib/mentat/const.py b/lib/mentat/const.py
index 5be08c059..7fbea3dab 100644
--- a/lib/mentat/const.py
+++ b/lib/mentat/const.py
@@ -55,6 +55,18 @@ CKEY_CORE_SERVICES_WHOIS = 'whois'
 
 #-------------------------------------------------------------------------------
 
+CKEY_CORE_REPORTING = '__core__reporting'
+"""Name of the configuration key for ``core reporting`` configurations."""
+CKEY_CORE_REPORTING_REPORTSDIR = 'reports_dir'
+"""Name of the configuration subkey key for ``reports dir`` configuration in ``core reporting`` configurations."""
+
+#-------------------------------------------------------------------------------
+
+EVENT_SEVERITY_LOW      = 'low'
+EVENT_SEVERITY_MEDIUM   = 'medium'
+EVENT_SEVERITY_HIGH     = 'high'
+EVENT_SEVERITY_CRITICAL = 'critical'
+
 REPORTING_MODE_SUMMARY = 'summary'
 REPORTING_MODE_EXTRA   = 'extra'
 REPORTING_MODE_BOTH    = 'both'
@@ -112,3 +124,7 @@ REPORTING_INTERVALS = {
 }
 
 REPORTING_INTERVALS_INV = {v: k for k, v in REPORTING_INTERVALS.items()}
+
+REPORT_TYPES = (REPORTING_MODE_SUMMARY, REPORTING_MODE_EXTRA)
+
+REPORT_SEVERITIES = (EVENT_SEVERITY_LOW, EVENT_SEVERITY_MEDIUM, EVENT_SEVERITY_HIGH, EVENT_SEVERITY_CRITICAL)
-- 
GitLab