diff --git a/conf/mentat-hawat-dev.py.conf b/conf/mentat-hawat-dev.py.conf index 6586d32c09840512f58f54b92caf3501665ae16b..aa3915decef8fe7d3ff7ead2dd66e95cfc11d71a 100644 --- a/conf/mentat-hawat-dev.py.conf +++ b/conf/mentat-hawat-dev.py.conf @@ -52,6 +52,7 @@ ENABLED_BLUEPRINTS = [ 'hawat.blueprints.home', 'hawat.blueprints.reports', 'hawat.blueprints.events', + 'hawat.blueprints.detectors', 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', diff --git a/conf/mentat-hawat.py.conf b/conf/mentat-hawat.py.conf index 48e422757c522a3b78b7cf819b73a33dde735640..eef420e439c1b7bccfc177c226589d933237210b 100644 --- a/conf/mentat-hawat.py.conf +++ b/conf/mentat-hawat.py.conf @@ -45,6 +45,7 @@ ENABLED_BLUEPRINTS = [ 'hawat.blueprints.home', 'hawat.blueprints.reports', 'hawat.blueprints.events', + 'hawat.blueprints.detectors', 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', diff --git a/lib/hawat/blueprints/detectors/__init__.py b/lib/hawat/blueprints/detectors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e521ed78d97b20f018c1b3bbdd87f7c4e077ebee --- /dev/null +++ b/lib/hawat/blueprints/detectors/__init__.py @@ -0,0 +1,398 @@ +#!/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 pluggable module provides access to detectors management features. These +features include: + +* general detectors listing +* detailed detector record view +* creating new detector records +* updating existing detector records +* deleting existing detector records +""" + +__author__ = "Rajmund Hruška <rajmund.hruska@cesnet.cz>" +__credits__ = "Jan Mach <jan.mach@cesnet.cz>, Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" + +import markupsafe +from flask_babel import gettext, lazy_gettext + +from sqlalchemy import or_ + +from mentat.datatype.sqldb import DetectorModel, ItemChangeLogModel + +import hawat.acl +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import ItemListView, ItemShowView, ItemCreateView, ItemUpdateView, ItemDeleteView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin +from hawat.blueprints.detectors.forms import AdminCreateDetectorForm, AdminUpdateDetectorForm, DetectorSearchForm + +BLUEPRINT_NAME = 'detectors' +"""Name of the blueprint as module global constant.""" + + +class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): + """ + General detector record listing. + """ + methods = ['GET'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Detector management') + + @property + def dbmodel(self): + return DetectorModel + + @classmethod + def get_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'create', + endpoint='detectors.create', + resptitle=True + ) + return action_menu + + @classmethod + def get_context_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'show', + endpoint='detectors.show', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'update', + endpoint='detectors.update', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'delete', + endpoint='detectors.delete', + hidetitle=True + ) + return action_menu + + @staticmethod + def get_search_form(request_args): + """ + Must return instance of :py:mod:`flask_wtf.FlaskForm` appropriate for + searching given type of items. + """ + return DetectorSearchForm( + request_args, + meta={'csrf': False} + ) + + @staticmethod + def build_query(query, model, form_args): + # Adjust query based on text search string. + if 'search' in form_args and form_args['search']: + query = query \ + .filter( + or_( + model.name.like('%{}%'.format(form_args['search'])), + model.description.like('%{}%'.format(form_args['search'])), + ) + ) + # Adjust query based on lower time boudary selection. + if 'dt_from' in form_args and form_args['dt_from']: + query = query.filter(model.createtime >= form_args['dt_from']) + # Adjust query based on upper time boudary selection. + if 'dt_to' in form_args and form_args['dt_to']: + query = query.filter(model.createtime <= form_args['dt_to']) + # Adjust query based on record source selection. + if 'source' in form_args and form_args['source']: + query = query \ + .filter(model.source == form_args['source']) + if 'sortby' in form_args and form_args['sortby']: + sortmap = { + 'createtime.desc': lambda x, y: x.order_by(y.createtime.desc()), + 'createtime.asc': lambda x, y: x.order_by(y.createtime.asc()), + 'name.desc': lambda x, y: x.order_by(y.name.desc()), + 'name.asc': lambda x, y: x.order_by(y.name.asc()), + 'credibility.desc': lambda x, y: x.order_by(y.credibility.desc()), + 'credibility.asc': lambda x, y: x.order_by(y.credibility.asc()) + } + query = sortmap[form_args['sortby']](query, model) + return query + + +class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): + """ + Detailed detector record view. + """ + methods = ['GET'] + + authentication = True + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'View details of detector record "%(item)s"', + item=markupsafe.escape(kwargs['item'].name) + ) + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Show detector record details') + + @property + def dbmodel(self): + return DetectorModel + + @classmethod + def authorize_item_action(cls, **kwargs): + return hawat.acl.PERMISSION_POWER.can() + + @classmethod + def get_action_menu(cls): + action_menu = hawat.menu.Menu() + + action_menu.add_entry( + 'endpoint', + 'update', + endpoint='detectors.update' + ) + action_menu.add_entry( + 'endpoint', + 'delete', + endpoint='detectors.delete' + ) + + return action_menu + + def do_before_response(self, **kwargs): + item = self.response_context['item'] + if self.can_access_endpoint('detectors.update', item=item) and self.has_endpoint('changelogs.search'): + self.response_context.update( + context_action_menu_changelogs=self.get_endpoint_class( + 'changelogs.search' + ).get_context_action_menu() + ) + + item_changelog = self.dbsession.query(ItemChangeLogModel). \ + filter(ItemChangeLogModel.model == item.__class__.__name__). \ + filter(ItemChangeLogModel.model_id == item.id). \ + order_by(ItemChangeLogModel.createtime.desc()). \ + limit(100). \ + all() + self.response_context.update(item_changelog=item_changelog) + + +class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for creating new detector records. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Create detector record') + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Create new detector record') + + @property + def dbmodel(self): + return DetectorModel + + @property + def dbchlogmodel(self): + return ItemChangeLogModel + + @classmethod + def authorize_item_action(cls, **kwargs): + return hawat.acl.PERMISSION_POWER.can() + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Detector record <strong>%(item_id)s</strong> was successfully created.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext('Unable to create new detector record.') + + @staticmethod + def get_message_cancel(**kwargs): + return gettext('Canceled creating new detector record.') + + @staticmethod + def get_item_form(item): + return AdminCreateDetectorForm() + + +class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for updating existing detector records. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Update details of detector record "%(item)s"', + item=markupsafe.escape(kwargs['item'].name) + ) + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Update detector record details') + + @property + def dbmodel(self): + return DetectorModel + + @property + def dbchlogmodel(self): + return ItemChangeLogModel + + @classmethod + def authorize_item_action(cls, **kwargs): + return hawat.acl.PERMISSION_POWER.can() + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Detector record <strong>%(item_id)s</strong> was successfully updated.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to update detector record <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled updating detector record <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_item_form(item): + return AdminUpdateDetectorForm(db_item_id=item.id, obj=item) + + +class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for deleting existing detector records. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Delete detector record "%(item)s"', + item=markupsafe.escape(kwargs['item'].name) + ) + + @property + def dbmodel(self): + return DetectorModel + + @property + def dbchlogmodel(self): + return ItemChangeLogModel + + @classmethod + def authorize_item_action(cls, **kwargs): + return hawat.acl.PERMISSION_POWER.can() + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Detector record <strong>%(item_id)s</strong> was successfully deleted.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to delete detector record <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled deleting detector record <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + +# ------------------------------------------------------------------------------- + + +class DetectorsBlueprint(HawatBlueprint): + """Pluggable module - detector management (*detectors*).""" + + @classmethod + def get_module_title(cls): + return lazy_gettext('Detector management') + + def register_app(self, app): + app.menu_main.add_entry( + 'view', + 'admin.{}'.format(BLUEPRINT_NAME), + position=71, + view=ListView + ) + + +# ------------------------------------------------------------------------------- + + +def get_blueprint(): + """ + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or + :py:class:`flask.Blueprint`. + """ + + hbp = DetectorsBlueprint( + BLUEPRINT_NAME, + __name__, + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) + ) + + hbp.register_view_class(ListView, '/list') + hbp.register_view_class(CreateView, '/create') + hbp.register_view_class(ShowView, '/<int:item_id>/show') + hbp.register_view_class(UpdateView, '/<int:item_id>/update') + hbp.register_view_class(DeleteView, '/<int:item_id>/delete') + + return hbp diff --git a/lib/hawat/blueprints/detectors/forms.py b/lib/hawat/blueprints/detectors/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..a25833599d4b17fe6767b890148a35e523a400a0 --- /dev/null +++ b/lib/hawat/blueprints/detectors/forms.py @@ -0,0 +1,209 @@ +#!/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 detector management forms for Hawat. +""" + +__author__ = "Rajmund Hruška <rajmund.hruska@cesnet.cz>" +__credits__ = "Jan Mach <jan.mach@cesnet.cz>, Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" + +import wtforms + +# +# Flask related modules. +# +from flask_babel import lazy_gettext, gettext + +# +# Custom modules. +# +import hawat.const +import hawat.forms +import hawat.db + +from mentat.datatype.sqldb import DetectorModel + + +def get_available_sources(): + """ + Query the database for list of network record sources. + """ + result = hawat.db.db_query(DetectorModel) \ + .distinct(DetectorModel.source) \ + .order_by(DetectorModel.source) \ + .all() + return [x.source for x in result] + + +def check_name_uniqueness(form, field): + """ + Callback for validating names during detector update action. + """ + item = hawat.db.db_get().session.query(DetectorModel). \ + filter(DetectorModel.name == field.data). \ + filter(DetectorModel.id != form.db_item_id). \ + all() + if not item: + return + raise wtforms.validators.ValidationError(gettext('Detector with this name already exists.')) + + +class BaseDetectorForm(hawat.forms.BaseItemForm): + """ + Class representing base detector record form. + """ + source = wtforms.HiddenField( + default='manual', + validators=[ + wtforms.validators.DataRequired(), + wtforms.validators.Length(min=3, max=50) + ] + ) + credibility = wtforms.FloatField( + lazy_gettext('Credibility:'), + validators=[ + wtforms.validators.Optional(), + wtforms.validators.NumberRange(min=0, max=1) + ] + ) + description = wtforms.TextAreaField( + lazy_gettext('Description:') + ) + submit = wtforms.SubmitField( + lazy_gettext('Submit') + ) + cancel = wtforms.SubmitField( + lazy_gettext('Cancel') + ) + + +class AdminCreateDetectorForm(BaseDetectorForm): + """ + Class representing detector record create form. + """ + name = wtforms.StringField( + lazy_gettext('Name:'), + validators=[ + wtforms.validators.DataRequired(), + wtforms.validators.Length(min=3, max=250), + hawat.forms.check_null_character, + check_name_uniqueness + ] + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.db_item_id = None + +class AdminUpdateDetectorForm(BaseDetectorForm): + """ + Class representing detector record create form. + """ + name = wtforms.StringField( + lazy_gettext('Name:'), + validators=[ + wtforms.validators.DataRequired(), + wtforms.validators.Length(min=3, max=250), + hawat.forms.check_null_character, + check_name_uniqueness + ] + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Store the ID of original item in database to enable the ID uniqueness + # check with check_name_uniqueness() validator. + self.db_item_id = kwargs['db_item_id'] + + +class DetectorSearchForm(hawat.forms.BaseSearchForm): + """ + Class representing simple user search form. + """ + search = wtforms.StringField( + lazy_gettext('Name, description:'), + validators=[ + wtforms.validators.Optional(), + wtforms.validators.Length(min=3, max=100), + hawat.forms.check_null_character + ], + description=lazy_gettext( + 'Detector`s name or description. Search is performed even in the middle of the strings.') + ) + dt_from = hawat.forms.SmartDateTimeField( + lazy_gettext('Creation time from:'), + validators=[ + wtforms.validators.Optional() + ], + description=lazy_gettext( + 'Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + ) + dt_to = hawat.forms.SmartDateTimeField( + lazy_gettext('Creation time to:'), + validators=[ + wtforms.validators.Optional() + ], + description=lazy_gettext( + 'Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + ) + + source = wtforms.SelectField( + lazy_gettext('Record source:'), + validators=[ + wtforms.validators.Optional() + ], + default='' + ) + + sortby = wtforms.SelectField( + lazy_gettext('Sort by:'), + validators=[ + wtforms.validators.Optional() + ], + choices=[ + ('createtime.desc', lazy_gettext('by creation time descending')), + ('createtime.asc', lazy_gettext('by creation time ascending')), + ('name.desc', lazy_gettext('by name descending')), + ('name.asc', lazy_gettext('by name ascending')), + ('credibility.asc', lazy_gettext('by credibility ascending')), + ('credibility.asc', lazy_gettext('by credibility descending')) + ], + default='name.asc' + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # + # Handle additional custom keywords. + # + + # The list of choices for 'roles' attribute comes from outside of the + # form to provide as loose tie as possible to the outer application. + # Another approach would be to load available choices here with: + # + # roles = flask.current_app.config['ROLES'] + # + # That would mean direct dependency on flask.Flask application. + source_list = get_available_sources() + self.source.choices = [('', lazy_gettext('Nothing selected'))] + list(zip(source_list, source_list)) + + @staticmethod + def is_multivalue(field_name): + """ + Check, if given form field is a multivalue field. + + :param str field_name: Name of the form field. + :return: ``True``, if the field can contain multiple values, ``False`` otherwise. + :rtype: bool + """ + return False diff --git a/lib/hawat/blueprints/detectors/templates/detectors/creatupdate.html b/lib/hawat/blueprints/detectors/templates/detectors/creatupdate.html new file mode 100644 index 0000000000000000000000000000000000000000..f8e3a59d3377577dc94cc95da330099a927263e8 --- /dev/null +++ b/lib/hawat/blueprints/detectors/templates/detectors/creatupdate.html @@ -0,0 +1,11 @@ +{%- extends "_layout_creatupdate.html" %} + +{%- block itemform_fields %} + + {{ macros_form.render_form_item_default(form.name) }} + + {{ macros_form.render_form_item_default(form.description) }} + + {{ macros_form.render_form_item_default(form.credibility) }} + +{%- endblock itemform_fields %} diff --git a/lib/hawat/blueprints/detectors/templates/detectors/list.html b/lib/hawat/blueprints/detectors/templates/detectors/list.html new file mode 100644 index 0000000000000000000000000000000000000000..cc75aa51504ce1987c2bf52c29bb2d9d4cc99f65 --- /dev/null +++ b/lib/hawat/blueprints/detectors/templates/detectors/list.html @@ -0,0 +1,69 @@ +{%- extends "_layout_list.html" %} + +{%- block searchformfields %} + + <div class="row"> + <div class="col-sm-4"> + {{ macros_form.render_form_item_default(g.search_form.search) }} + </div> + <div class="col-sm-4"> + {{ macros_form.render_form_item_datetime(g.search_form.dt_from, 'datetimepicker-hm-from') }} + </div> + <div class="col-sm-4"> + {{ macros_form.render_form_item_datetime(g.search_form.dt_to, 'datetimepicker-hm-to') }} + </div> + </div> + <div class="row"> + <div class="col-sm-6"> + {{ macros_form.render_form_item_select(g.search_form.source) }} + </div> + </div> + +{%- endblock searchformfields %} + +{%- block contentinner %} + + <table class="table table-bordered table-hover table-striped"> + <thead> + <tr> + <th> + {{ _('Name') }} {{ macros_site.render_sorter(request.endpoint, query_params, 'name') }} + </th> + <th> + {{ _('Source') }} + </th> + <th> + {{ _('Credibility') }} {{ macros_site.render_sorter(request.endpoint, query_params, 'credibility') }} + </th> + <th> + {{ _('Description') }} + </th> + <th data-toggle="tooltip" title="{{ _('Contextual item actions') }}"> + {{ get_icon('actions') }} {{ _('Actions') }} + </th> + </tr> + </thead> + <tbody> + {%- for item in items %} + <tr> + <td> + {{ item.name | default(_('<< unknown >>'), True) }} + </td> + <td> + {{ item.source | default(_('<< unknown >>'), True) }} + </td> + <td> + {{ item.credibility | default(_('<< unknown >>'), True) }} + </td> + <td> + {{ item.description | default(_('<< unknown >>'), True) | truncate(50) }} + </td> + <td class="column-actions"> + {{ macros_page.render_menu_context_actions(item) }} + </td> + </tr> + {%- endfor %} + </tbody> + </table> + +{%- endblock contentinner %} diff --git a/lib/hawat/blueprints/detectors/templates/detectors/show.html b/lib/hawat/blueprints/detectors/templates/detectors/show.html new file mode 100644 index 0000000000000000000000000000000000000000..f54dab447a69cbff0e576829761763186c833dcd --- /dev/null +++ b/lib/hawat/blueprints/detectors/templates/detectors/show.html @@ -0,0 +1,113 @@ +{%- extends "_layout.html" %} + +{%- block content %} + + <div class="row"> + <div class="col-lg-12"> + {{ macros_page.render_breadcrumbs(item) }} + + <h2>{{ hawat_current_view.get_view_title() }}</h2> + <hr> + <h3>{{ item.name }}</h3> + <div class="pull-right"> + {{ macros_page.render_menu_actions(item) }} + + </div> + <p> + <small> + <strong>{{ _('Detector created') }}:</strong> {{ babel_format_datetime(item.createtime) }} ({{ _('%(delta)s ago', delta = babel_format_timedelta(current_datetime_utc - item.createtime)) }}) + </small> + </p> + <br> + + <!-- Nav tabs --> + <ul class="nav nav-tabs" role="tablist"> + <li role="presentation" class="active"> + <a href="#tab-general" aria-controls="tab-general" role="tab" data-toggle="tab"> + <strong>{{ get_icon('alert-info') }} {{ _('General information') }}</strong> + </a> + </li> + {%- if can_access_endpoint('detectors.update', item) %} + <li role="presentation"> + <a href="#tab-changelog" aria-controls="tab-changelog" role="tab" data-toggle="tab"> + <strong>{{ get_icon('module-changelogs') }} {{ _('Changelogs') }}</strong> <span class="badge">{{ item_changelog | length }}</span> + </a> + </li> + {%- endif %} + </ul> + + <!-- Tab panes --> + <div class="tab-content"> + + <div role="tabpanel" class="tab-pane fade in active" id="tab-general"> + <br> + <table class="table table-striped"> + <tbody> + <tr> + <th> + {{ _('Name') }}: + </th> + <td> + {{ item.name | default(_('<< unknown >>'), True) }} + </td> + </tr> + <tr> + <th> + {{ _('Source') }}: + </th> + <td> + {{ item.source | default(_('<< unknown >>'), True) }} + </td> + </tr> + <tr> + <th> + {{ _('Description') }}: + </th> + <td> + {{ item.description | default(_('<< unknown >>'), True) }} + </td> + </tr> + <tr> + <th> + {{ _('Credibility') }}: + </th> + <td> + {{ item.credibility }} + </td> + </tr> + </tbody> + </table> + </div> + + {%- if can_access_endpoint('detectors.update', item) %} + <div role="tabpanel" class="tab-pane fade" id="tab-changelog"> + <br> + {%- if item_changelog %} + {{ macros_page.render_changelog_records(item_changelog, context_action_menu_changelogs) }} + <p> + <small> + {{ _('Displaying only latest %(count)s changelogs', count = 100) }} + </small> + </p> + {%- else %} + {%- call macros_site.render_alert('info', False) %} + {{ _('This object does not have any changelog records at the moment.') }} + {%- endcall %} + {%- endif %} + </div> + {%- endif %} + + </div> + + </div><!-- /.col-lg-12 --> + </div><!-- /.row --> + + {%- if permission_can('developer') %} + + <hr> + + {{ macros_site.render_raw_item_view(item) }} + + {%- endif %} + +{%- endblock content %} diff --git a/lib/hawat/config.py b/lib/hawat/config.py index 8f09ca9cb91958fbab1782ade0e889d24544103d..c35b323f092fe3c54264e04f17cc82d7abe9fb2d 100644 --- a/lib/hawat/config.py +++ b/lib/hawat/config.py @@ -162,6 +162,7 @@ class Config: # pylint: disable=locally-disabled,too-few-public-methods 'hawat.blueprints.home', 'hawat.blueprints.reports', 'hawat.blueprints.events', + 'hawat.blueprints.detectors', 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', @@ -313,6 +314,7 @@ class DevelopmentConfig(Config): # pylint: disable=locally-disabled,too-few-pub 'hawat.blueprints.home', 'hawat.blueprints.reports', 'hawat.blueprints.events', + 'hawat.blueprints.detectors', 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', @@ -363,6 +365,7 @@ class TestingConfig(Config): # pylint: disable=locally-disabled,too-few-public- 'hawat.blueprints.home', 'hawat.blueprints.reports', 'hawat.blueprints.events', + 'hawat.blueprints.detectors', 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', diff --git a/lib/hawat/const.py b/lib/hawat/const.py index 88823315b846185d57be41a24cb3d6b4ef17ccd7..3e340f7aaa5510e6aa63b49cff8c983b84c90b7f 100644 --- a/lib/hawat/const.py +++ b/lib/hawat/const.py @@ -236,6 +236,7 @@ ICONS = { 'module-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', 'module-dbstatus': '<i class="fas fa-fw fa-database"></i>', 'module-design': '<i class="fas fa-fw fa-palette"></i>', + 'module-detectors': '<i class="fas fa-fw fa-user-secret"></i>', 'module-devtools': '<i class="fas fa-fw fa-bug"></i>', 'module-dnsr': '<i class="fas fa-fw fa-directions"></i>', 'module-events': '<i class="fas fa-fw fa-bell"></i>',