From cba52cf3d8ceced08541a02d8440657811600245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20I=C3=B1iguez=20Goia?= Date: Sat, 21 Jan 2023 00:27:31 +0100 Subject: [PATCH] ui: added Actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added ability to perform actions on different parts of the GUI, based on conditions defined in json files. There's only one Action of type Highlight for now, to colorize cells and rows. There're 3 Highlight actions defined by default: - rules: applied to the rules view to colorize the columns Enabled and Action. - firewall: applied to the fw rules to colorize the columns Action and Enabled. - common: applied to the rest of the views to colorize the column Action. Users can add new actions to the directory ~/.config/opensnitch/actions/, as .json files. The format is defined below. Example of a Highlight action to colorize cells and rows, based on different texts (simple texts/strings for now): { "name": "commonDelegateConfig", "actions": { "highlight": { "cells": [ { "text": ["allow", "✓ online"], "cols": [1, 2, 3], "color": "green", "bgcolor": "", "alignment": ["center"] } ], "rows": [ { "text": ["block-domains"], "cols": [8], "color": "white", "bgcolor": "darkMagenta", "alignment": [] } ] } } Closes: #555 --- ui/opensnitch/actions/__init__.py | 157 ++++++++ ui/opensnitch/actions/default_configs.py | 128 +++++++ ui/opensnitch/actions/highlight.py | 234 ++++++++++++ ui/opensnitch/actions/utils.py | 8 + .../customwidgets/colorizeddelegate.py | 77 ++++ ui/opensnitch/customwidgets/main.py | 33 +- ui/opensnitch/dialogs/stats.py | 357 ++++++++---------- ui/opensnitch/res/stats.ui | 3 + 8 files changed, 766 insertions(+), 231 deletions(-) create mode 100644 ui/opensnitch/actions/__init__.py create mode 100644 ui/opensnitch/actions/default_configs.py create mode 100644 ui/opensnitch/actions/highlight.py create mode 100644 ui/opensnitch/actions/utils.py create mode 100644 ui/opensnitch/customwidgets/colorizeddelegate.py diff --git a/ui/opensnitch/actions/__init__.py b/ui/opensnitch/actions/__init__.py new file mode 100644 index 0000000000..7348be4b9d --- /dev/null +++ b/ui/opensnitch/actions/__init__.py @@ -0,0 +1,157 @@ +from PyQt5.QtCore import QObject + +import json +import os +import glob +import sys + +from opensnitch.actions import highlight +from opensnitch.actions.default_configs import commonDelegateConfig, rulesDelegateConfig, fwDelegateConfig + +class Actions(QObject): + """List of actions to perform on the data that is displayed on the GUI. + Whenever an item matches a condition an action is applied, for example: + - if the text of a cell matches a condition for the given columns, + then the properties of the cell/row and the text are customized. + + There's only 1 action supported right now: + - highlight: for customizing rows and cells appearance. + + There're 3 actions by default of type Highlight: + - rules: applied to the rules to colorize the columns Enabled and + Action + - firewall: applied to the fw rules to colorize the columns Action and + Target. + - common: applied to the rest of the views to colorize the column + Action. + + Users can modify the default actions, by adding more patterns to colorize. + At the same time they can also create new actions to be applied on certain views. + + The format of the actions is JSON: + { + "created": "....", + "name": "...", + "actions": { + "highlight": { + "cells": [ + { + "text": ["allow", "True", "online"], + "cols": [3,5,6], + "color": "green", + }, + { + "text": ["deny", "False", "offline"], + "cols": [3,5,6], + "color": "red", + } + ], + "rows": [] + } + } + + """ + __instance = None + + # list of loaded actions + _actions = None + + + KEY_ACTIONS = "actions" + KEY_NAME = "name" + KEY_TYPE = "type" + + # TODO: emit a signal when the actions are (re)loaded + # reloaded_signal = pyQtSignal() + + # default paths to look for actions + _paths = [ + os.path.dirname(sys.modules[__name__].__file__) + "/data/", + os.path.expanduser("~/.config/opensnitch/actions/") + ] + + @staticmethod + def instance(): + if Actions.__instance == None: + Actions.__instance = Actions() + return Actions.__instance + + def __init__(self, parent=None): + QObject.__init__(self) + self._actions_list = {} + try: + os.makedirs(os.path.expanduser("~/.config/opensnitch/actions/"), 0o700) + except: + pass + + def _load_default_configs(self): + self._actions_list[commonDelegateConfig[Actions.KEY_NAME]] = self.compile(commonDelegateConfig) + self._actions_list[rulesDelegateConfig[Actions.KEY_NAME]] = self.compile(rulesDelegateConfig) + self._actions_list[fwDelegateConfig[Actions.KEY_NAME]] = self.compile(fwDelegateConfig) + + def loadAll(self): + """look for actions firstly on default system path, secondly on + user's home. + If a user customizes existing configurations, they'll be saved under + the user's home directory. + + Action files are .json files. + """ + self._load_default_configs() + + for path in self._paths: + for jfile in glob.glob(os.path.join(path, '*.json')): + self.load(jfile) + + def load(self, action_file): + """read a json file from disk and create the action.""" + with open(action_file, 'r') as fd: + data=fd.read() + obj = json.loads(data) + self._actions_list[obj[Actions.KEY_NAME]] = self.compile(obj) + + def compile(self, obj): + try: + if Actions.KEY_NAME not in obj or obj[Actions.KEY_NAME] == "": + return None + if obj.get(Actions.KEY_ACTIONS) == None: + return None + + for action in obj[Actions.KEY_ACTIONS]: + if action == highlight.Highlight.NAME: + h = highlight.Highlight(obj[Actions.KEY_ACTIONS][action]) + h.compile() + obj[Actions.KEY_ACTIONS][action]= h + else: + print("Actions exception: Action '{0}' not supported yet".format(obj[Actions.KEY_NAME])) + + return obj + except Exception as e: + print("Actions.compile() exception:", e) + return None + + + + def getAll(self): + return self._actions_list + + def deleteAll(self): + self._actions_list = {} + + def get(self, name): + try: + return self._actions_list[name] + except Exception as e: + print("get() exception:", e) + return None + + def delete(self, name): + try: + del self._actions_list[name] + # TODO: + # self.reloaded_signal.emit() + except: + pass + + def isValid(self): + pass diff --git a/ui/opensnitch/actions/default_configs.py b/ui/opensnitch/actions/default_configs.py new file mode 100644 index 0000000000..fae8b8b332 --- /dev/null +++ b/ui/opensnitch/actions/default_configs.py @@ -0,0 +1,128 @@ + +# common configuration to highlight Action column +commonDelegateConfig = { + "name": "commonDelegateConfig", + "created": "", + "updated": "", + "actions": { + "highlight": { + "cells": [ + { + "text": ["allow", "\u2713 online"], + "operator": "==", + "cols": [1, 2, 3], + "color": "green", + "bgcolor": "", + "alignment": ["center"] + }, + { + "text": ["deny", "\u2613 offline"], + "cols": [1, 2, 3], + "color": "red", + "bgcolor": "", + "alignment": ["center"] + }, + { + "text": ["reject"], + "cols": [1, 2, 3], + "color": "purple", + "bgcolor": "", + "alignment": ["center"] + } + ], + "rows": [] + } + } +} + +# firewall rules configuration to highlight Enabled and Action columns +fwDelegateConfig = { + "name": "defaultFWDelegateConfig", + "created": "", + "updated": "", + "actions": { + "highlight": { + "cells": [ + { + "text": [ + "allow", + "True", + "accept", + "jump", + "masquerade", + "snat", + "dnat", + "tproxy", + "queue", + "redirect", + "True", + "ACCEPT" + ], + "cols": [7, 10], + "color": "green", + "bgcolor": "", + "alignment": ["center"] + }, + { + "text": [ + "deny", + "False", + "drop", + "DROP", + "stop" + ], + "cols": [7, 10], + "color": "red", + "bgcolor": "", + "alignment": ["center"] + }, + { + "text": [ + "reject", + "return" + ], + "cols": [7, 10], + "color": "purple", + "bgcolor": "", + "alignment": ["center"] + } + ], + "rows": [] + } + } +} + +# rules configuration to highlight Enabled and Action columns +rulesDelegateConfig = { + "name": "defaultRulesDelegateConfig", + "created": "", + "updated": "", + "actions": { + "highlight": { + "cells": [ + { + "text": ["allow", "True"], + "cols": [3, 4], + "color": "green", + "bgcolor": "", + "alignment": ["center"] + }, + { + "text": ["deny", "False"], + "cols": [3, 4], + "color": "red", + "bgcolor": "", + "alignment": ["center"] + }, + { + "text": ["reject"], + "cols": [3, 4], + "color": "purple", + "bgcolor": "", + "alignment": ["center"] + } + ], + "rows": [] + } + } +} diff --git a/ui/opensnitch/actions/highlight.py b/ui/opensnitch/actions/highlight.py new file mode 100644 index 0000000000..e373702a6a --- /dev/null +++ b/ui/opensnitch/actions/highlight.py @@ -0,0 +1,234 @@ +from PyQt5 import Qt, QtCore +from PyQt5.QtGui import QColor, QStandardItemModel, QStandardItem + +class Highlight(): + """Customizes QTablewView cells via QItemDelegates. + Format: + [ + { + 'text': {"allow", "True", "online"}, + 'cols': {1,4,5}, + 'color': "green", + 'bgcolor': None, + 'alignment': ["center"], + #"margins': [0, 0] + #'font': {} + }, + ] + + text: will match any of the given texts. + cols: look for patterns on these columns. + color: colorizes the color of the text. + bgcolor: colorizes the background color of the cell. + etc. + """ + + NAME = "highlight" + + MARGINS = "margins" + ALIGNMENT = "alignment" + # QtCore.Qt.AlignCenter + ALIGN_CENTER = "center" + # QtCore.Qt.AlignHCenter + ALIGN_HCENTER = "hcenter" + # QtCore.Qt.AlignVCenter + ALIGN_VCENTER = "vcenter" + + COLOR = "color" + BGCOLOR = "bgcolor" + FONT = "font" + CELLS = "cells" + ROWS = "rows" + COLS = "cols" + TEXT = "text" + + def __init__(self, config): + # original json config received + self._config = config + self._last_visited_row = -1 + self._rowcells = "" + + def compile(self): + """transform json items to Qt objects. + These items are transformed: + - color (to QColor), bgcolor (to QColor), alignment (to Qt.Align*), + font (to QFont TODO) + + Return the original json object transformed. + """ + # cells, rows + for idx in self._config: + cells = self._config[idx] + for cell in cells: + for item in cell: + # colors + if (item == Highlight.COLOR or item == Highlight.BGCOLOR): + if cell[item] != "" and cell[item] is not None: + cell[item] = QColor(cell[item]) + else: + cell[item] = None + + # alignments + if item == Highlight.ALIGNMENT: + cell[item] = self.getAlignment(cell[item]) + + # fonts + if item == Highlight.FONT: + self.getFont(cell[item]) + + return self._config + + + def run(self, args): + """Highlight cells or rows based on patterns. + + Return if the cell was modified. + + Keyword arguments: + args -- tuple of options. + """ + painter = args[0] + option = args[1] + index = args[2] + style = args[3] + modelColumns = args[4] + curRow = args[5] + curColumn = args[6] + defaultPen = args[7] + defaultBrush = args[8] + cellAlignment = args[9] + cellRect = args[10] + cellValue = args[11] + + # signal that this cell has been modified + modified = False + + cells = self._config.get(Highlight.CELLS) + rows = self._config.get(Highlight.ROWS) + + if cells: + for cell in cells: + if curColumn not in cell[Highlight.COLS]: + continue + if cellValue not in cell[Highlight.TEXT]: + continue +# TODO +# if cell['operator'] == 'simple' and cellValue != cell['text']: +# continue +# elif cell['text'] not in cellValue: +# continue + + cellColor = cell.get(Highlight.COLOR) + cellBgColor = cell.get(Highlight.BGCOLOR) + if cell.get(Highlight.ALIGNMENT) != None: + cellAlignment = cell[Highlight.ALIGNMENT] + if cell.get(Highlight.MARGINS) != None: + cellRect.adjust( + int(cell[Highlight.MARGINS][self.HMARGIN]), + int(cell[Highlight.MARGINS][self.VMARGIN]), + -defaultPen.width(), + -defaultPen.width() + ) + + modified=True + self.paintCell( + style, + painter, + option, + index, + defaultPen, + defaultBrush, + cellAlignment, + cellRect, + cellColor, + cellBgColor, + cellValue) + + if len(rows) == 0: + return (modified,) + + # get row's cells only for the first cell of the row, + # then reuse them for the rest of the cells of the current row. + if curRow != self._last_visited_row: + self._rowcells = " ".join( + [index.sibling(curRow, col).data() for col in range(0, modelColumns)] + ) + self._last_visited_row = curRow + + for row in rows: + skip = True + for text in row[Highlight.TEXT]: + if text in self._rowcells: + skip = False + if skip: + continue + + cellColor = row.get(Highlight.COLOR) + cellBgColor = row.get(Highlight.BGCOLOR) + if row.get(Highlight.ALIGNMENT) != None: + cellAlignment = row[Highlight.ALIGNMENT] + if row.get(Highlight.MARGINS) != None: + cellRect.adjust( + int(row[Highlight.MARGINS][self.HMARGIN]), + int(row[Highlight.MARGINS][self.VMARGIN]), + -defaultPen.width(), + -defaultPen.width() + ) + + modified=True + self.paintCell( + style, + painter, + option, + index, + defaultPen, + defaultBrush, + cellAlignment, + cellRect, + cellColor, + cellBgColor, + cellValue) + + return (modified,) + + def paintCell(self, style, painter, option, index, defaultPen, defaultBrush, cellAlignment, cellRect, cellColor, cellBgColor, cellValue): + cellSelected = option.state & Qt.QStyle.State_Selected + + painter.save() + # don't customize selected state + if not cellSelected: + if cellBgColor != None: + painter.fillRect(option.rect, cellBgColor) + + if cellColor is not None: + defaultPen.setColor(cellColor) + painter.setPen(defaultPen) + + # setting option.displayAlignment has no effect here, so we need to + # draw the text. + # FIXME: Drawing the text though, the background color of the SelectedState is + # altered. + # If we called super().paint(), modifying option.palette.* would be + # enough to change the text color, but it wouldn't be aligned: + # option.palette.setColor(QPalette.Text, cellColor) + style.drawItemText(painter, cellRect, cellAlignment, option.palette, True, cellValue) + painter.restore() + + def getAlignment(self, alignments): + alignFlags = 0 + for align in alignments: + if align == Highlight.ALIGN_CENTER: + alignFlags |= QtCore.Qt.AlignCenter + elif align == Highlight.ALIGN_HCENTER: + alignFlags |= QtCore.Qt.AlignHCenter + elif align == Highlight.ALIGN_VCENTER: + alignFlags |= QtCore.Qt.AlignVCenter + + if alignFlags == 0: + return None + + return alignFlags + + def getFont(self, font): + # TODO + pass diff --git a/ui/opensnitch/actions/utils.py b/ui/opensnitch/actions/utils.py new file mode 100644 index 0000000000..5c435bed76 --- /dev/null +++ b/ui/opensnitch/actions/utils.py @@ -0,0 +1,8 @@ +from PyQt5.QtGui import QColor + +def getColorNames(): + """Return the built-in color names that can be used to choose new colors: + https://doc.qt.io/qtforpython-5/PySide2/QtGui/QColor.html#predefined-colors + https://www.w3.org/TR/SVG11/types.html#ColorKeywords + """ + return QColor.colorNames() diff --git a/ui/opensnitch/customwidgets/colorizeddelegate.py b/ui/opensnitch/customwidgets/colorizeddelegate.py new file mode 100644 index 0000000000..24e0910bdb --- /dev/null +++ b/ui/opensnitch/customwidgets/colorizeddelegate.py @@ -0,0 +1,77 @@ +from PyQt5 import Qt, QtCore +from PyQt5.QtWidgets import QApplication + +class ColorizedDelegate(Qt.QItemDelegate): + HMARGIN = 0 + VMARGIN = 1 + + def __init__(self, parent=None, *args, actions={}): + Qt.QItemDelegate.__init__(self, parent, *args) + self._actions = actions + self.modelColumns = parent.model().columnCount() + self._style = QApplication.style() + + def setConfig(self, actions): + self._actions = actions + + #@profile_each_line + def paint(self, painter, option, index): + """Override default widget style to personalize it with our own. + """ + if self._actions.get('actions') == None: + return super().paint(painter, option, index) + if not index.isValid(): + return super().paint(painter, option, index) + cellValue = index.data(QtCore.Qt.DisplayRole) + if cellValue == None: + return super().paint(painter, option, index) + + # initialize new QStyleOptionViewItem with the default options of this + # cell. + option = Qt.QStyleOptionViewItem(option) + + # by default use item's default attributes. + # if we modify any of them, set it to False + nocolor=True + + # don't call these functions in for-loops + cellRect = Qt.QRect(option.rect) + curColumn = index.column() + curRow = index.row() + cellAlignment = option.displayAlignment + defaultPen = painter.pen() + defaultBrush = painter.brush() + + # get default margins in order to respect them. + # option.widget is the QTableView + hmargin = self._style.pixelMetric( + self._style.PM_FocusFrameHMargin, None, option.widget + ) + 1 + vmargin = self._style.pixelMetric( + self._style.PM_FocusFrameVMargin, None, option.widget + ) + 1 + + # set default margins for this cell + cellRect.adjust(hmargin, vmargin, -painter.pen().width(), -painter.pen().width()) + + for a in self._actions['actions']: + action = self._actions['actions'][a] + modified = action.run( + (painter, + option, + index, + self._style, + self.modelColumns, + curRow, + curColumn, + defaultPen, + defaultBrush, + cellAlignment, + cellRect, + cellValue) + ) + if modified[0]: + nocolor=False + + if nocolor: + super().paint(painter, option, index) diff --git a/ui/opensnitch/customwidgets/main.py b/ui/opensnitch/customwidgets/main.py index 4d096f0010..4812c49d16 100644 --- a/ui/opensnitch/customwidgets/main.py +++ b/ui/opensnitch/customwidgets/main.py @@ -1,4 +1,4 @@ -from PyQt5 import Qt, QtCore +from PyQt5 import QtCore from PyQt5.QtGui import QColor, QStandardItemModel, QStandardItem from PyQt5.QtSql import QSqlQueryModel, QSqlQuery, QSql from PyQt5.QtWidgets import QTableView @@ -7,37 +7,6 @@ import math from PyQt5.QtCore import QCoreApplication as QC - -class ColorizedDelegate(Qt.QItemDelegate): - def __init__(self, parent=None, *args, config=None): - Qt.QItemDelegate.__init__(self, parent, *args) - self._config = config - self._alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignHCenter - - def paint(self, painter, option, index): - if not index.isValid(): - return super().paint(painter, option, index) - - nocolor=True - - value = index.data(QtCore.Qt.DisplayRole) - for _, what in enumerate(self._config): - if what == value: - nocolor=False - painter.save() - painter.setPen(self._config[what]) - if 'alignment' in self._config: - self._alignment = self._config['alignment'] - - if option.state & Qt.QStyle.State_Selected: - painter.setBrush(painter.brush()) - painter.setPen(painter.pen()) - painter.drawText(option.rect, self._alignment, value) - painter.restore() - - if nocolor == True: - super().paint(painter, option, index) - class ColorizedQSqlQueryModel(QSqlQueryModel): """ model=CustomQSqlQueryModel( diff --git a/ui/opensnitch/dialogs/stats.py b/ui/opensnitch/dialogs/stats.py index 3fdc0c3f7f..0520b6a5c1 100644 --- a/ui/opensnitch/dialogs/stats.py +++ b/ui/opensnitch/dialogs/stats.py @@ -17,17 +17,15 @@ from opensnitch.dialogs.preferences import PreferencesDialog from opensnitch.dialogs.ruleseditor import RulesEditorDialog from opensnitch.dialogs.processdetails import ProcessDetailsDialog -from opensnitch.customwidgets.main import ColorizedDelegate, ConnectionsTableModel +from opensnitch.customwidgets.colorizeddelegate import ColorizedDelegate from opensnitch.customwidgets.firewalltableview import FirewallTableModel from opensnitch.customwidgets.generictableview import GenericTableModel from opensnitch.customwidgets.addresstablemodel import AddressTableModel from opensnitch.utils import Message, QuickHelp, AsnDB, Icons +from opensnitch.actions import Actions DIALOG_UI_PATH = "%s/../res/stats.ui" % os.path.dirname(sys.modules[__name__].__file__) class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): - RED = QtGui.QColor(0xff, 0x63, 0x47) - GREEN = QtGui.QColor(0x2e, 0x90, 0x59) - PURPLE = QtGui.QColor(0x7f, 0x00, 0xff) _trigger = QtCore.pyqtSignal(bool, bool) settings_saved = QtCore.pyqtSignal() @@ -120,205 +118,162 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): # try to restore last selection LAST_SELECTED_ITEM = "" - commonDelegateConf = { - Config.ACTION_DENY: RED, - Config.ACTION_REJECT: PURPLE, - Config.ACTION_ALLOW: GREEN, - 'alignment': QtCore.Qt.AlignCenter | QtCore.Qt.AlignHCenter - } - - commonTableConf = { - "name": "", + TABLES = { + TAB_MAIN: { + "name": "connections", "label": None, "cmd": None, + "cmdCleanStats": None, "view": None, + "filterLine": None, "model": None, - "delegate": commonDelegateConf, - "display_fields": "*" - } - - TABLES = { - TAB_MAIN: { - "name": "connections", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": commonDelegateConf, - "display_fields": "time as Time, " \ - "node as Node, " \ - "action as Action, " \ - "CASE dst_host WHEN ''" \ - " THEN dst_ip || ' -> ' || dst_port " \ - " ELSE dst_host || ' -> ' || dst_port " \ - "END Destination, " \ - "protocol as Protocol, " \ - "process as Process, " \ - "process_args as Cmdline, " \ - "rule as Rule", - "group_by": LAST_GROUP_BY, - "last_order_by": "1", - "last_order_to": 1 - }, - TAB_NODES: { - "name": "nodes", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": { - Config.ACTION_DENY: RED, - Config.ACTION_REJECT: PURPLE, - Config.ACTION_ALLOW: GREEN, - Nodes.OFFLINE: RED, - Nodes.ONLINE: GREEN, - 'alignment': QtCore.Qt.AlignCenter | QtCore.Qt.AlignHCenter - }, - "display_fields": "last_connection as LastConnection, "\ - "addr as Addr, " \ - "status as Status, " \ - "hostname as Hostname, " \ - "daemon_version as Version, " \ - "daemon_uptime as Uptime, " \ - "daemon_rules as Rules," \ - "cons as Connections," \ - "cons_dropped as Dropped," \ - "version as Version", - "header_labels": [], - "last_order_by": "1", - "last_order_to": 1 - }, - TAB_RULES: { - "name": "rules", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": commonDelegateConf, - "display_fields": "time as Time," \ - "node as Node," \ - "name as Name," \ - "enabled as Enabled," \ - "action as Action," \ - "duration as Duration," \ - "description as Description", - "header_labels": [], - "last_order_by": "2", - "last_order_to": 0 - }, - TAB_FIREWALL: { - "name": "firewall", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": { - Config.ACTION_DENY: RED, - Config.ACTION_DROP: RED, - Config.ACTION_STOP: RED, - "DROP": RED, - "ACCEPT": GREEN, - Config.ACTION_REJECT: PURPLE, - Config.ACTION_RETURN: PURPLE, - Config.ACTION_ACCEPT: GREEN, - Config.ACTION_JUMP: GREEN, - Config.ACTION_MASQUERADE: GREEN, - Config.ACTION_SNAT: GREEN, - Config.ACTION_DNAT: GREEN, - Config.ACTION_TPROXY: GREEN, - Config.ACTION_QUEUE: GREEN, - "True": GREEN, - "False": RED, - 'alignment': QtCore.Qt.AlignCenter | QtCore.Qt.AlignHCenter - }, - "display_fields": "*", - "header_labels": [], - "last_order_by": "2", - "last_order_to": 0 - }, - TAB_HOSTS: { - "name": "hosts", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": commonDelegateConf, - "display_fields": "*", - "header_labels": [], - "last_order_by": "2", - "last_order_to": 1 - }, - TAB_PROCS: { - "name": "procs", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": commonDelegateConf, - "display_fields": "*", - "header_labels": [], - "last_order_by": "2", - "last_order_to": 1 - }, - TAB_ADDRS: { - "name": "addrs", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": commonDelegateConf, - "display_fields": "*", - "header_labels": [], - "last_order_by": "2", - "last_order_to": 1 - }, - TAB_PORTS: { - "name": "ports", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": commonDelegateConf, - "display_fields": "*", - "header_labels": [], - "last_order_by": "2", - "last_order_to": 1 - }, - TAB_USERS: { - "name": "users", - "label": None, - "cmd": None, - "cmdCleanStats": None, - "view": None, - "filterLine": None, - "model": None, - "delegate": commonDelegateConf, - "display_fields": "*", - "header_labels": [], - "last_order_by": "2", - "last_order_to": 1 - } - } + "delegate": "commonDelegateConfig", + "display_fields": "time as Time, " \ + "node as Node, " \ + "action as Action, " \ + "CASE dst_host WHEN ''" \ + " THEN dst_ip || ' -> ' || dst_port " \ + " ELSE dst_host || ' -> ' || dst_port " \ + "END Destination, " \ + "protocol as Protocol, " \ + "process as Process, " \ + "process_args as Cmdline, " \ + "rule as Rule", + "group_by": LAST_GROUP_BY, + "last_order_by": "1", + "last_order_to": 1 + }, + TAB_NODES: { + "name": "nodes", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "commonDelegateConfig", + "display_fields": "last_connection as LastConnection, "\ + "addr as Addr, " \ + "status as Status, " \ + "hostname as Hostname, " \ + "daemon_version as Version, " \ + "daemon_uptime as Uptime, " \ + "daemon_rules as Rules," \ + "cons as Connections," \ + "cons_dropped as Dropped," \ + "version as Version", + "header_labels": [], + "last_order_by": "1", + "last_order_to": 1 + }, + TAB_RULES: { + "name": "rules", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "defaultRulesDelegateConfig", + "display_fields": "time as Time," \ + "node as Node," \ + "name as Name," \ + "enabled as Enabled," \ + "action as Action," \ + "duration as Duration," \ + "description as Description", + "header_labels": [], + "last_order_by": "2", + "last_order_to": 0 + }, + TAB_FIREWALL: { + "name": "firewall", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "defaultFWDelegateConfig", + "display_fields": "*", + "header_labels": [], + "last_order_by": "2", + "last_order_to": 0 + }, + TAB_HOSTS: { + "name": "hosts", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "commonDelegateConfig", + "display_fields": "*", + "header_labels": [], + "last_order_by": "2", + "last_order_to": 1 + }, + TAB_PROCS: { + "name": "procs", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "commonDelegateConfig", + "display_fields": "*", + "header_labels": [], + "last_order_by": "2", + "last_order_to": 1 + }, + TAB_ADDRS: { + "name": "addrs", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "commonDelegateConfig", + "display_fields": "*", + "header_labels": [], + "last_order_by": "2", + "last_order_to": 1 + }, + TAB_PORTS: { + "name": "ports", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "commonDelegateConfig", + "display_fields": "*", + "header_labels": [], + "last_order_by": "2", + "last_order_to": 1 + }, + TAB_USERS: { + "name": "users", + "label": None, + "cmd": None, + "cmdCleanStats": None, + "view": None, + "filterLine": None, + "model": None, + "delegate": "commonDelegateConfig", + "display_fields": "*", + "header_labels": [], + "last_order_by": "2", + "last_order_to": 1 + } + } def __init__(self, parent=None, address=None, db=None, dbname="db", appicon=None): super(StatsDialog, self).__init__(parent) - QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint) self._current_desktop = os.environ['XDG_CURRENT_DESKTOP'] if os.environ.get("XDG_CURRENT_DESKTOP") != None else None @@ -367,6 +322,8 @@ def __init__(self, parent=None, address=None, db=None, dbname="db", appicon=None self._nodes = Nodes.instance() self._fw = Firewall().instance() self._fw.rules.rulesUpdated.connect(self._cb_fw_rules_updated) + self._actions = Actions().instance() + self._actions.loadAll() # TODO: allow to display multiples dialogs self._proc_details_dialog = ProcessDetailsDialog(appicon=appicon) @@ -2474,9 +2431,6 @@ def _setup_table(self, widget, tableWidget, table_name, fields="*", group_by="", tableWidget.setSortingEnabled(True) if model == None: model = self._db.get_new_qsql_model() - if delegate != None: - tableWidget.setItemDelegate(ColorizedDelegate(self, config=delegate)) - if verticalScrollBar != None: tableWidget.setVerticalScrollBar(verticalScrollBar) tableWidget.verticalScrollBar().sliderPressed.connect(self._cb_scrollbar_pressed) @@ -2485,6 +2439,11 @@ def _setup_table(self, widget, tableWidget, table_name, fields="*", group_by="", self.setQuery(model, "SELECT " + fields + " FROM " + table_name + group_by + " ORDER BY " + order_by + " " + sort_direction + limit) tableWidget.setModel(model) + if delegate != None: + action = self._actions.get(delegate) + if action != None: + tableWidget.setItemDelegate(ColorizedDelegate(tableWidget, actions=action)) + header = tableWidget.horizontalHeader() if header != None: header.sortIndicatorChanged.connect(self._cb_table_header_clicked) diff --git a/ui/opensnitch/res/stats.ui b/ui/opensnitch/res/stats.ui index ef093d406a..25c9f5f84a 100644 --- a/ui/opensnitch/res/stats.ui +++ b/ui/opensnitch/res/stats.ui @@ -897,6 +897,9 @@ QAbstractItemView::SelectRows + + QAbstractItemView::ScrollPerPixel + false