diff --git a/conf/requirements-latest.pip b/conf/requirements-latest.pip index b1b7b6774d0dea5ff04d8fbde59e0e3dbf0bbb16..407a971a95ec73e56bf5cca3b08443e34e5192f6 100644 --- a/conf/requirements-latest.pip +++ b/conf/requirements-latest.pip @@ -17,6 +17,7 @@ flask-wtf flask-script flask-sqlalchemy flask-debugtoolbar +flask-jsglue geoip2 rrdtool pydgets diff --git a/conf/requirements.pip b/conf/requirements.pip index 3f390a6510f93d15dd38a793d4f183aa164cd39b..1b439cee8c89311a17f15448160683ae9b256d97 100644 --- a/conf/requirements.pip +++ b/conf/requirements.pip @@ -17,6 +17,7 @@ flask-wtf==0.14.2 flask-script==2.0.6 flask-sqlalchemy==2.3.2 flask-debugtoolbar==0.10.1 +flask-jsglue==0.3.1 geoip2==2.9.0 rrdtool==0.1.14 pydgets==0.9 diff --git a/lib/hawat/app.py b/lib/hawat/app.py index ea4b0921ace27bfc560eab6c5bb7bb203cc09e30..3648819e4c133fb60b07e760dc8dce910854153e 100644 --- a/lib/hawat/app.py +++ b/lib/hawat/app.py @@ -37,6 +37,7 @@ import flask_login import flask_principal import flask_mail import flask_babel +import flask_jsglue # # Custom modules. @@ -154,6 +155,7 @@ def _setup_app_core(app): hawat_version = mentat.__version__, hawat_bversion = mentat._buildmeta.__bversion__, # pylint: disable=locally-disabled,protected-access hawat_bversion_full = mentat._buildmeta.__bversion_full__, # pylint: disable=locally-disabled,protected-access + hawat_current_app = flask.current_app, hawat_current_menu_main = flask.current_app.menu_main, hawat_current_menu_auth = flask.current_app.menu_auth, hawat_current_menu_anon = flask.current_app.menu_anon, @@ -177,7 +179,6 @@ def _setup_app_core(app): get_datetime_local Reference for :py:func:`hawat.app.get_datetime_local` """ - def get_endpoints_dict(): """ Return dictionary of all registered application view endpoints. @@ -420,6 +421,21 @@ def _setup_app_core(app): """ return flask.render_template('index.html') + @app.route('/hawat-main.js') + def mainjs(): # pylint: disable=locally-disabled,unused-variable + """ + Default route for main application JavaScript file. + """ + return flask.make_response( + flask.render_template('hawat-main.js'), + 200, + {'Content-Type': 'text/javascript'} + ) + + # Initialize JSGlue plugin for using `flask.url_for()` method in JavaScript. + jsglue = flask_jsglue.JSGlue() + jsglue.init_app(app) + return app diff --git a/lib/hawat/base.py b/lib/hawat/base.py index ada0ec1863b16e8f6bdb8135851d672595085378..6e958e3fa4423203fd439607da65170a28625b03 100644 --- a/lib/hawat/base.py +++ b/lib/hawat/base.py @@ -100,7 +100,7 @@ class URLParamsBuilder: This class is still proof of concept and work in progress. """ def __init__(self, skeleton = None): - self.rules = {} + self.rules = [] self.skeleton = skeleton or {} @staticmethod @@ -116,9 +116,9 @@ class URLParamsBuilder: Add new rule to URL parameter builder. """ if as_list: - self.rules[key] = self._add_vector + self.rules.append([key, self._add_vector, as_list]) else: - self.rules[key] = self._add_scalar + self.rules.append([key, self._add_scalar, as_list]) return self def get_params(self, value): @@ -126,8 +126,8 @@ class URLParamsBuilder: Get URL parameters as dictionary with filled-in value. """ tmp = copy.deepcopy(self.skeleton) - for key, rule in self.rules.items(): - rule(tmp, key, value) + for rule in self.rules: + rule[1](tmp, rule[0], value) return tmp diff --git a/lib/hawat/blueprints/design/static/js/hawat-charts.js b/lib/hawat/blueprints/design/static/js/hawat-charts.js index 1067118b832442950a5efca9c0aeb56af542416d..3ff79b8dc46ee3da4a2ed12fb332d6481635b01a 100644 --- a/lib/hawat/blueprints/design/static/js/hawat-charts.js +++ b/lib/hawat/blueprints/design/static/js/hawat-charts.js @@ -425,8 +425,43 @@ function render_table_timeline_multi(tid, table_columns, table_data, data_stats) return table; } +// Render context search action dropdown menu. +function render_action_dropdown(actions, value) { + root_elem = document.createElement('div'); + + div_slct = d3.select(root_elem) + .style('display', 'inline-block'); + + div_dropdown = div_slct.append('div') + .attr('class', 'btn-group'); + + div_dropdown.append('button') + .attr('class', 'btn btn-default btn-xs dropdown-toggle') + .attr('type', 'button') + .attr('data-toggle', 'dropdown') + .attr('aria-haspopup', 'true') + .attr('aria-expanded', 'false') + .append('span') + .attr('class', 'caret'); + + ul_elem = div_dropdown.append('ul') + .attr('class', 'dropdown-menu dropdown-menu-right') + + actions.forEach(function(action) { + try { + url = Flask.url_for(action.endpoint, action.params(value)); + ul_elem.append('li').append('a') + .attr('href', url) + .html(action.icon + action.title.replace('{name}', value)); + } + catch(err) {} + }); + + return root_elem; +} + // Render table for dict-based dataset. -function render_table_dict(tid, table_columns, table_data, data_stats) { +function render_table_dict(tid, table_columns, table_data, data_stats, action_list = null) { console.log('Rendering table: ' + tid); var value_formatter = table_value_formatter( @@ -482,20 +517,37 @@ function render_table_dict(tid, table_columns, table_data, data_stats) { }); }) .enter() - .append('td') - //.style('background-color', table_column_color_scale(0)) - .append('span') - .text(function (d, i) { + .append('td'); + //.style('background-color', table_column_color_scale(0)); + + bcells.append(function (d, i) { + element = document.createElement('span'); + // First column contains the name. if (i == 0) { - return d.value; + element.appendChild( + document.createTextNode( + d.value + ) + ); + return element; } // Second column contains the value. if (i == 1) { - return value_formatter(d.value); + element.appendChild( + document.createTextNode( + value_formatter(d.value) + ) + ); + return element; } // Last column contains the percentage. - return percent_formatter(d.value); + element.appendChild( + document.createTextNode( + percent_formatter(d.value) + ) + ); + return element; }); // Insert small circle icon with correct color for given dataset instead of @@ -505,6 +557,16 @@ function render_table_dict(tid, table_columns, table_data, data_stats) { .style('color', table_color_scale()) .attr('class', 'fas fa-fw fa-circle pull-left'); + if (action_list) { + brows.select('td') + .filter(function(d, i) { + return !(String(d.key).toLowerCase() in {'__unknown__': true, '__rest__': true}); + }) + .append(function (d, i) { + return render_action_dropdown(action_list, d.key); + }); + } + // Map of aggregation functions for table footer statistical summaries. funcmap = { 'cnt': function(a,b) { return a.length }, diff --git a/lib/hawat/blueprints/design/templates/_layout.html b/lib/hawat/blueprints/design/templates/_layout.html index 0928389ab491750a0c880fd500555e709d59d866..b05bf1df37098a35ef0bc190e0bb4af2ac7893e4 100644 --- a/lib/hawat/blueprints/design/templates/_layout.html +++ b/lib/hawat/blueprints/design/templates/_layout.html @@ -63,6 +63,9 @@ <script src="{{ url_for('design.static', filename='vendor/globalize/globalize/relative-time.js') }}"></script> <script src="{{ url_for('design.static', filename='vendor/globalize/globalize/unit.js') }}"></script> + <!-- Flask JSGlue plugin --> + {{ JSGlue.include() }} + <!-- Moment.js --> <script src="{{ url_for('design.static', filename='vendor/moment/js/moment-with-locales.min.js') }}"></script> @@ -166,6 +169,9 @@ </script> + <!-- Main application library --> + <script src="{{ url_for('mainjs') }}"></script> + <!-- Custom libraries --> <script src="{{ url_for('design.static', filename='js/hawat-head.js') }}"></script> <script src="{{ url_for('design.static', filename='js/hawat-charts.js') }}"></script> diff --git a/lib/hawat/blueprints/design/templates/_macros_chart.html b/lib/hawat/blueprints/design/templates/_macros_chart.html index 6a928a839d864190f623fd04d1ee20e611e9d36e..cb55a3ca79c87e9087351cdbf3c4dfee1ac0264a 100644 --- a/lib/hawat/blueprints/design/templates/_macros_chart.html +++ b/lib/hawat/blueprints/design/templates/_macros_chart.html @@ -143,9 +143,10 @@ string chart_id: Unique identifier of the chart. This will be used for generating all other required unique identifiers. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro _snippet_chart_timeline(chart_id) %} +{%- macro _snippet_chart_timeline(chart_id, cfg_params) %} // Render the timeline chart '{{ chart_id }}'. $(document).ready(function () { render_chart_timeline_multi( @@ -169,9 +170,10 @@ string chart_id: Unique identifier of the chart. This will be used for generating all other required unique identifiers. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro _snippet_chart_pie(chart_id) %} +{%- macro _snippet_chart_pie(chart_id, cfg_params) %} // Render pie chart for dataset '{{ chart_id }}'. $(document).ready(function () { render_chart_pie( @@ -188,9 +190,10 @@ string chart_id: Unique identifier of the chart. This will be used for generating all other required unique identifiers. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro _snippet_table_timeline(chart_id) %} +{%- macro _snippet_table_timeline(chart_id, cfg_params) %} // Render the timeline table '{{ chart_id }}'. $(document).ready(function () { render_table_timeline_multi( @@ -215,9 +218,10 @@ string chart_id: Unique identifier of the chart. This will be used for generating all other required unique identifiers. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro _snippet_table_dict(chart_id) %} +{%- macro _snippet_table_dict(chart_id, cfg_params) %} // Render table for dataset '{{ chart_id }}'. $(document).ready(function () { render_table_dict( @@ -228,7 +232,8 @@ {'ident': 'share', 'label': '{{ _("Share") }}'}, ], {{ chart_id }}_dataset, - GLOBAL_TABLE_COLS_STATS + GLOBAL_TABLE_COLS_STATS{%- if 'csag_name' in cfg_params %}, + Hawat.get_csag('{{ cfg_params['csag_name'] }}'){%- endif %} ); }); {%- endmacro %} @@ -240,9 +245,10 @@ string chart_id: Unique identifier of the chart. This will be used for generating all other required unique identifiers. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro _snippet_ecbks_timeline(chart_id) %} +{%- macro _snippet_ecbks_timeline(chart_id, cfg_params) %} // Enable necessary event callbacks to appropriate DOM elements. $(document).ready(function () { // Event handler for toggling dataset table. @@ -310,9 +316,10 @@ string chart_id: Unique identifier of the chart. This will be used for generating all other required unique identifiers. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro _snippet_ecbks_dict(chart_id) %} +{%- macro _snippet_ecbks_dict(chart_id, cfg_params) %} // Append necessary event handlers for dataset '{{ chart_id }}'. $(document).ready(function () { // Event handler for downloading chart as SVG. @@ -380,9 +387,10 @@ generating identifiers for all required HTML and JS elements. string list_keys: List of requested subkeys from which the target dataset will be constructed. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro render_chart_timeline(full_data, data_var_name, id_prefix, list_keys) %} +{%- macro render_chart_timeline(full_data, data_var_name, id_prefix, list_keys, cfg_params = {}) %} {%- set chart_id = 'chart_timeline_' + id_prefix %} <!------------------------------------------------------------------ Dataset visualisation '{{ chart_id }}' @@ -407,9 +415,9 @@ {{ chart_id }}_series ); - {{ _snippet_chart_timeline(chart_id) }} - {{ _snippet_table_timeline(chart_id) }} - {{ _snippet_ecbks_timeline(chart_id) }} + {{ _snippet_chart_timeline(chart_id, cfg_params) }} + {{ _snippet_table_timeline(chart_id, cfg_params) }} + {{ _snippet_ecbks_timeline(chart_id, cfg_params) }} </script> {%- endmacro %} @@ -427,13 +435,14 @@ generating identifiers for all required HTML and JS elements. string dict_key: Name of the subkey in given full_data dictionary structure containing data from which to actually generate the TIMELINE chart. + dict cfg_params: Additional chart configuration and customization parameters. This macro expects, that the data structure under dict_key within the full_data is going to be a dictionary. In case it does not exist appropriate information will be presented to the user instead of the chart. -#} -{%- macro render_chart_timeline_dict(full_data, data_var_name, id_prefix, dict_key) %} +{%- macro render_chart_timeline_dict(full_data, data_var_name, id_prefix, dict_key, cfg_params = {}) %} {%- set chart_id = 'chart_timeline_' + id_prefix + '_' + dict_key %} <!------------------------------------------------------------------ Dataset visualisation '{{ chart_id }}' @@ -456,9 +465,9 @@ '{{ dict_key }}' ); - {{ _snippet_chart_timeline(chart_id) }} - {{ _snippet_table_timeline(chart_id) }} - {{ _snippet_ecbks_timeline(chart_id) }} + {{ _snippet_chart_timeline(chart_id, cfg_params) }} + {{ _snippet_table_timeline(chart_id, cfg_params) }} + {{ _snippet_ecbks_timeline(chart_id, cfg_params) }} </script> {%- else %} @@ -481,13 +490,14 @@ generating identifiers for all required HTML and JS elements. string dict_key: Name of the subkey in given full_data dictionary structure containing data from which to actually generate the PIE chart. + dict cfg_params: Additional chart configuration and customization parameters. This macro expects, that the data structure under dict_key within the full_data is going to be a dictionary. In case it does not exist appropriate information will be presented to the user instead of the chart. -#} -{%- macro render_dataset_pie_dict(full_data, data_var_name, id_prefix, dict_key) %} +{%- macro render_dataset_pie_dict(full_data, data_var_name, id_prefix, dict_key, cfg_params = {}) %} {%- set chart_id = 'chart_pie_' + id_prefix + '_' + dict_key %} {%- set chart_data_var = data_var_name + '.' + dict_key %} <!------------------------------------------------------------------ @@ -502,9 +512,9 @@ var {{ chart_id }}_dataset = get_dataset_dict( {{ chart_data_var }} ); - {{ _snippet_chart_pie(chart_id) }} - {{ _snippet_table_dict(chart_id) }} - {{ _snippet_ecbks_dict(chart_id) }} + {{ _snippet_chart_pie(chart_id, cfg_params) }} + {{ _snippet_table_dict(chart_id, cfg_params) }} + {{ _snippet_ecbks_dict(chart_id, cfg_params) }} </script> @@ -529,9 +539,10 @@ generating identifiers for all required HTML and JS elements. string list_keys: List of requested subkeys from which the target dataset will be constructed. + dict cfg_params: Additional chart configuration and customization parameters. -#} -{%- macro render_dataset_pie_list(full_data, data_var_name, id_prefix, list_keys) %} +{%- macro render_dataset_pie_list(full_data, data_var_name, id_prefix, list_keys, cfg_params = {}) %} {%- set chart_id = 'chart_pie_' + id_prefix %} <!------------------------------------------------------------------ Dataset visualisation '{{ chart_id }}' @@ -555,9 +566,9 @@ {{ data_var_name }}, {{ chart_id }}_series ); - {{ _snippet_chart_pie(chart_id) }} - {{ _snippet_table_dict(chart_id) }} - {{ _snippet_ecbks_dict(chart_id) }} + {{ _snippet_chart_pie(chart_id, cfg_params) }} + {{ _snippet_table_dict(chart_id, cfg_params) }} + {{ _snippet_ecbks_dict(chart_id, cfg_params) }} </script> @@ -710,7 +721,10 @@ statistics, statistics_var_name, id_prefix.replace('-', '_'), - chsection[0] + chsection[0], + { + 'csag_name': chsection[0] + } ) }} {%- else %} diff --git a/lib/hawat/const.py b/lib/hawat/const.py index a9c14c708fab4425299b81fbd04fc044b252b173..596370573b2cfdccea7981bdb5ec5d98fc411fee 100644 --- a/lib/hawat/const.py +++ b/lib/hawat/const.py @@ -107,16 +107,16 @@ RESOURCE_BABEL = 'babel' # # List of all existing Hawat context action search groups. # -HAWAT_CSAG_ABUSE = 'abuse' -HAWAT_CSAG_ADDRESS = 'address' -HAWAT_CSAG_CATEGORY = 'category' -HAWAT_CSAG_CLASS = 'class' -HAWAT_CSAG_DETECTOR = 'detector' -HAWAT_CSAG_DETTYPE = 'detector_type' -HAWAT_CSAG_HOSTTYPE = 'host_type' -HAWAT_CSAG_PORT = 'port' -HAWAT_CSAG_PROTOCOL = 'protocol' -HAWAT_CSAG_SEVERITY = 'severity' +HAWAT_CSAG_ABUSE = 'abuses' +HAWAT_CSAG_ADDRESS = 'ips' +HAWAT_CSAG_CATEGORY = 'categories' +HAWAT_CSAG_CLASS = 'classes' +HAWAT_CSAG_DETECTOR = 'detectors' +HAWAT_CSAG_DETTYPE = 'detector_types' +HAWAT_CSAG_HOSTTYPE = 'host_types' +HAWAT_CSAG_PORT = 'ports' +HAWAT_CSAG_PROTOCOL = 'protocols' +HAWAT_CSAG_SEVERITY = 'severities' FA_ICONS = { diff --git a/lib/hawat/templates/hawat-main.js b/lib/hawat/templates/hawat-main.js new file mode 100644 index 0000000000000000000000000000000000000000..e7b58255cfee17e4fe8bd9ce294e451334e1aa02 --- /dev/null +++ b/lib/hawat/templates/hawat-main.js @@ -0,0 +1,50 @@ +// Global application module. +var Hawat = (function () { + + function _build_param_builder(skeleton, rules) { + //var _skeleton = Object.assign({}, skeleton); + var _skeleton = skeleton; + var _rules = rules; + return function(value) { + //var _result = Object.assign({}, _skeleton); + var _result = _skeleton; + _rules.forEach(function(r) { + _result[r[0]] = value; + }); + return _result; + } + } + + var _csag = { +{%- for csag_name in hawat_current_app.csag.keys() | sort %} + '{{ csag_name }}': [ + {%- for csag in hawat_current_app.csag[csag_name] %} + { + 'title': '{{ _(csag.title, name = '{name}') }}', + 'endpoint': '{{ csag.view.get_view_endpoint() }}', + 'icon': '{{ get_icon(csag.view.get_menu_icon()) }}', + 'params': _build_param_builder( + {{ csag.params.skeleton | tojson | safe }}, + {{ csag.params.rules | tojson | safe }} + ) + }{%- if not loop.last %},{%- endif %} + {%- endfor %} + ]{%- if not loop.last %},{%- endif %} +{%- endfor %} + }; + + return { + get_csags: function() { + return _csag; + }, + + get_csag: function(name) { + try { + return _csag[name]; + } + catch (err) { + return null + } + } + }; +})();