Skip to content
Snippets Groups Projects
Commit a158a21c authored by Jan Mach's avatar Jan Mach
Browse files

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)
parent 7d174ad8
No related branches found
No related tags found
No related merge requests found
Showing
with 813 additions and 210 deletions
......@@ -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,
......
......@@ -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.
#
......
......@@ -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',
......
......@@ -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):
......
......@@ -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()
)
......
......@@ -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;
}
......@@ -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'.
......
......@@ -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
......
......@@ -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 %}
......
{%- import '_macros_site.html' as macros_site with context -%}
{%- call macros_site.render_alert(category) %}
{{ message | safe }}
{%- endcall %}
......@@ -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
......@@ -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:'),
......
......@@ -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 %}
......
{%- 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>
......@@ -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 %}
......
{%- 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 %}
......
#!/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
......@@ -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
......
......@@ -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 -->
......
{%- 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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment