UI: fixed race condition, improved UI performance

- Fixed race condition when adding stats to the db, specially when there
  were several nodes connected.
- Colorized allow/deny and online/offline words, to improve visual
  experience.
- UI performance has been improved, specially when there're multiple
  nodes sendings stats.
This commit is contained in:
Gustavo Iñiguez Goia 2020-04-26 19:54:52 +02:00
parent dac78eb883
commit 039a393ab1
4 changed files with 365 additions and 171 deletions

View file

@ -0,0 +1,68 @@
from PyQt5 import Qt, QtCore
from PyQt5.QtGui import QColor, QPen, QBrush
from PyQt5.QtSql import QSqlDatabase, QSqlQueryModel
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(
modelData=
{
'colorize':
{'offline': (QColor(QtCore.Qt.red), 2)},
'alignment': { Qt.AlignLeft, 2 }
}
)
"""
RED = QColor(QtCore.Qt.red)
GREEN = QColor(QtCore.Qt.green)
def __init__(self, modelData={}):
QSqlQueryModel.__init__(self)
self._model_data = modelData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return QSqlQueryModel.data(self, index, role)
column = index.column()
row = index.row()
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignCenter
if role == QtCore.Qt.TextColorRole:
for _, what in enumerate(self._model_data):
d = QSqlQueryModel.data(self, self.index(row, self._model_data[what][1]), QtCore.Qt.DisplayRole)
if column == self._model_data[what][1] and what in d:
return self._model_data[what][0]
return QSqlQueryModel.data(self, index, role)

View file

@ -9,19 +9,21 @@ import time
from PyQt5 import Qt, QtCore, QtGui, uic, QtWidgets
from PyQt5.QtSql import QSqlDatabase, QSqlDatabase, QSqlQueryModel, QSqlQuery, QSqlTableModel
from PyQt5.QtGui import QColor
import ui_pb2
from database import Database
from config import Config
from version import version
from nodes import Nodes
from dialogs.preferences import PreferencesDialog
from customwidgets import ColorizedDelegate
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)
_trigger = QtCore.pyqtSignal()
_trigger = QtCore.pyqtSignal(bool, bool)
_shown_trigger = QtCore.pyqtSignal()
_notification_trigger = QtCore.pyqtSignal(ui_pb2.Notification)
@ -31,6 +33,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
LIMITS = ["LIMIT 50", "LIMIT 100", "LIMIT 200", "LIMIT 300", ""]
LAST_GROUP_BY = ""
COL_TIME = 0
COL_NODE = 1
COL_ACTION = 2
COL_DSTIP = 3
@ -38,6 +41,9 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
COL_PROCS = 5
COL_RULES = 6
COL_WHAT = 0
TAB_MAIN = 0
TAB_NODES = 1
TAB_RULES = 2
TAB_HOSTS = 3
@ -46,13 +52,21 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
TAB_PORTS = 6
TAB_USERS = 7
commonDelegateConf = {
'deny': RED,
'allow': GREEN,
'alignment': QtCore.Qt.AlignCenter | QtCore.Qt.AlignHCenter
}
TABLES = {
0: {
TAB_MAIN: {
"name": "connections",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"model": None,
"delegate": commonDelegateConf,
"display_fields": "time as Time, " \
"node as Node, " \
"action as Action, " \
@ -62,12 +76,18 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"rule as Rule",
"group_by": LAST_GROUP_BY
},
1: {
TAB_NODES: {
"name": "nodes",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"model": None,
"delegate": {
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, " \
@ -79,75 +99,93 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"cons_dropped as Dropped," \
"version as Version" \
},
2: {
TAB_RULES: {
"name": "rules",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"delegate": None,
"model": None,
"delegate": commonDelegateConf,
"display_fields": "*"
},
3: {
TAB_HOSTS: {
"name": "hosts",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"delegate": None,
"model": None,
"delegate": commonDelegateConf,
"display_fields": "*"
},
4: {
TAB_PROCS: {
"name": "procs",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"delegate": None,
"model": None,
"delegate": commonDelegateConf,
"display_fields": "*"
},
5: {
TAB_ADDRS: {
"name": "addrs",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"delegate": None,
"model": None,
"delegate": commonDelegateConf,
"display_fields": "*"
},
6: {
TAB_PORTS: {
"name": "ports",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"delegate": None,
"model": None,
"delegate": commonDelegateConf,
"display_fields": "*"
},
7: {
TAB_USERS: {
"name": "users",
"label": None,
"tipLabel": None,
"cmd": None,
"view": None,
"delegate": None,
"model": None,
"delegate": commonDelegateConf,
"display_fields": "*"
}
}
def __init__(self, parent=None, address=None, dbname="db"):
def __init__(self, parent=None, address=None, db=None, dbname="db"):
super(StatsDialog, self).__init__(parent)
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
self.setWindowFlags(QtCore.Qt.Window)
self.setupUi(self)
self._db = Database.instance()
self._db = db
self._db_sqlite = self._db.get_db()
self._db_name = dbname
self._cfg = Config.get()
self._nodes = Nodes.instance()
self.daemon_connected = False
self._lock = threading.Lock()
self._lock = threading.RLock()
self._address = address
self._stats = None
self._prefs_dialog = PreferencesDialog()
self._trigger.connect(self._on_update_triggered)
@ -161,45 +199,74 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.filterLine.textChanged.connect(self._cb_events_filter_line_changed)
self.limitCombo.currentIndexChanged.connect(self._cb_limit_combo_changed)
self.cmdCleanSql.clicked.connect(self._cb_clean_sql_clicked)
self.tabWidget.currentChanged.connect(self._cb_tab_changed)
self.TABLES[0]['view'] = self._setup_table(QtWidgets.QTreeView, self.eventsTable, "connections",
self.TABLES[0]['display_fields'],
self.TABLES[self.TAB_MAIN]['view'] = self._setup_table(QtWidgets.QTreeView, self.eventsTable, "connections",
self.TABLES[self.TAB_MAIN]['display_fields'],
order_by="1",
group_by=self.TABLES[0]['group_by'],
resize_cols=(StatsDialog.COL_ACTION, StatsDialog.COL_PROTO, StatsDialog.COL_NODE))
self.TABLES[1]['view'] = self._setup_table(QtWidgets.QTableView, self.nodesTable, "nodes",
self.TABLES[1]['display_fields'], order_by="3,2,1")
self.TABLES[2]['view'] = self._setup_table(QtWidgets.QTableView, self.rulesTable, "rules", order_by="1")
self.TABLES[3]['view'] = self._setup_table(QtWidgets.QTableView, self.hostsTable, "hosts", order_by="2,1")
self.TABLES[4]['view'] = self._setup_table(QtWidgets.QTableView, self.procsTable, "procs", order_by="2,1")
self.TABLES[5]['view'] = self._setup_table(QtWidgets.QTableView, self.addrTable, "addrs", order_by="2,1")
self.TABLES[6]['view'] = self._setup_table(QtWidgets.QTableView, self.portsTable, "ports", order_by="2,1")
self.TABLES[7]['view'] = self._setup_table(QtWidgets.QTableView, self.usersTable, "users", order_by="2,1")
group_by=self.TABLES[self.TAB_MAIN]['group_by'],
delegate=self.TABLES[self.TAB_MAIN]['delegate'],
resize_cols=(self.COL_TIME, self.COL_ACTION, self.COL_PROTO, self.COL_NODE))
self.TABLES[self.TAB_NODES]['view'] = self._setup_table(QtWidgets.QTableView, self.nodesTable, "nodes",
self.TABLES[self.TAB_NODES]['display_fields'],
order_by="3,2,1",
resize_cols=(self.COL_NODE,),
delegate=self.TABLES[self.TAB_NODES]['delegate'])
self.TABLES[self.TAB_RULES]['view'] = self._setup_table(QtWidgets.QTableView,
self.rulesTable, "rules",
resize_cols=(self.COL_WHAT,),
delegate=self.TABLES[self.TAB_RULES]['delegate'],
order_by="1")
self.TABLES[self.TAB_HOSTS]['view'] = self._setup_table(QtWidgets.QTableView,
self.hostsTable, "hosts",
resize_cols=(self.COL_WHAT,),
delegate=self.TABLES[self.TAB_HOSTS]['delegate'],
order_by="2")
self.TABLES[self.TAB_PROCS]['view'] = self._setup_table(QtWidgets.QTableView,
self.procsTable, "procs",
resize_cols=(self.COL_WHAT,),
delegate=self.TABLES[self.TAB_PROCS]['delegate'],
order_by="2")
self.TABLES[self.TAB_ADDRS]['view'] = self._setup_table(QtWidgets.QTableView,
self.addrTable, "addrs",
resize_cols=(self.COL_WHAT,),
delegate=self.TABLES[self.TAB_ADDRS]['delegate'],
order_by="2")
self.TABLES[self.TAB_PORTS]['view'] = self._setup_table(QtWidgets.QTableView,
self.portsTable, "ports",
resize_cols=(self.COL_WHAT,),
delegate=self.TABLES[self.TAB_PORTS]['delegate'],
order_by="2")
self.TABLES[self.TAB_USERS]['view'] = self._setup_table(QtWidgets.QTableView,
self.usersTable, "users",
resize_cols=(self.COL_WHAT,),
delegate=self.TABLES[self.TAB_USERS]['delegate'],
order_by="2")
self.TABLES[1]['label'] = self.nodesLabel
self.TABLES[1]['tipLabel'] = self.tipNodesLabel
self.TABLES[2]['label'] = self.ruleLabel
self.TABLES[2]['tipLabel'] = self.tipRulesLabel
self.TABLES[3]['label'] = self.hostsLabel
self.TABLES[3]['tipLabel'] = self.tipHostsLabel
self.TABLES[4]['label'] = self.procsLabel
self.TABLES[4]['tipLabel'] = self.tipProcsLabel
self.TABLES[5]['label'] = self.addrsLabel
self.TABLES[5]['tipLabel'] = self.tipAddrsLabel
self.TABLES[6]['label'] = self.portsLabel
self.TABLES[6]['tipLabel'] = self.tipPortsLabel
self.TABLES[7]['label'] = self.usersLabel
self.TABLES[7]['tipLabel'] = self.tipUsersLabel
self.TABLES[self.TAB_NODES]['label'] = self.nodesLabel
self.TABLES[self.TAB_NODES]['tipLabel'] = self.tipNodesLabel
self.TABLES[self.TAB_RULES]['label'] = self.ruleLabel
self.TABLES[self.TAB_RULES]['tipLabel'] = self.tipRulesLabel
self.TABLES[self.TAB_HOSTS]['label'] = self.hostsLabel
self.TABLES[self.TAB_HOSTS]['tipLabel'] = self.tipHostsLabel
self.TABLES[self.TAB_PROCS]['label'] = self.procsLabel
self.TABLES[self.TAB_PROCS]['tipLabel'] = self.tipProcsLabel
self.TABLES[self.TAB_ADDRS]['label'] = self.addrsLabel
self.TABLES[self.TAB_ADDRS]['tipLabel'] = self.tipAddrsLabel
self.TABLES[self.TAB_PORTS]['label'] = self.portsLabel
self.TABLES[self.TAB_PORTS]['tipLabel'] = self.tipPortsLabel
self.TABLES[self.TAB_USERS]['label'] = self.usersLabel
self.TABLES[self.TAB_USERS]['tipLabel'] = self.tipUsersLabel
self.TABLES[1]['cmd'] = self.cmdNodesBack
self.TABLES[2]['cmd'] = self.cmdRulesBack
self.TABLES[3]['cmd'] = self.cmdHostsBack
self.TABLES[4]['cmd'] = self.cmdProcsBack
self.TABLES[5]['cmd'] = self.cmdAddrsBack
self.TABLES[6]['cmd'] = self.cmdPortsBack
self.TABLES[7]['cmd'] = self.cmdUsersBack
self.TABLES[self.TAB_NODES]['cmd'] = self.cmdNodesBack
self.TABLES[self.TAB_RULES]['cmd'] = self.cmdRulesBack
self.TABLES[self.TAB_HOSTS]['cmd'] = self.cmdHostsBack
self.TABLES[self.TAB_PROCS]['cmd'] = self.cmdProcsBack
self.TABLES[self.TAB_ADDRS]['cmd'] = self.cmdAddrsBack
self.TABLES[self.TAB_PORTS]['cmd'] = self.cmdPortsBack
self.TABLES[self.TAB_USERS]['cmd'] = self.cmdUsersBack
self.TABLES[0]['view'].doubleClicked.connect(self._cb_main_table_double_clicked)
self.TABLES[self.TAB_MAIN]['view'].doubleClicked.connect(self._cb_main_table_double_clicked)
for idx in range(1,8):
self.TABLES[idx]['cmd'].setVisible(False)
self.TABLES[idx]['cmd'].clicked.connect(lambda: self._cb_cmd_back_clicked(idx))
@ -208,14 +275,14 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._load_settings()
self._tables = ( \
self.TABLES[0]['view'],
self.TABLES[1]['view'],
self.TABLES[2]['view'],
self.TABLES[3]['view'],
self.TABLES[4]['view'],
self.TABLES[5]['view'],
self.TABLES[6]['view'],
self.TABLES[7]['view']
self.TABLES[self.TAB_MAIN]['view'],
self.TABLES[self.TAB_NODES]['view'],
self.TABLES[self.TAB_RULES]['view'],
self.TABLES[self.TAB_HOSTS]['view'],
self.TABLES[self.TAB_PROCS]['view'],
self.TABLES[self.TAB_ADDRS]['view'],
self.TABLES[self.TAB_PORTS]['view'],
self.TABLES[self.TAB_USERS]['view']
)
self._file_names = ( \
'events.csv',
@ -236,9 +303,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
window_title = "OpenSnitch Network Statistics for %s" % self._address
self.nodeLabel.setText(self._address)
self.setWindowTitle(window_title)
def get_db(self):
return self._db
self._refresh_active_table()
def _load_settings(self):
dialog_geometry = self._cfg.getSettings("statsDialog/geometry")
@ -264,6 +329,9 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._cfg.setSettings("statsDialog/last_tab", self.tabWidget.currentIndex())
self._cfg.setSettings("statsDialog/general_limit_results", self.limitCombo.currentIndex())
def _cb_tab_changed(self, index):
self._refresh_active_table()
def _cb_table_header_clicked(self, pos, sortIdx):
model = self._get_active_table().model()
self.LAST_ORDER_BY = pos+1
@ -407,6 +475,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
return self.TABLES[self.tabWidget.currentIndex()]['view']
def _set_nodes_query(self, data):
s = "AND c.src_ip='%s'" % data if '/' not in data else ''
model = self._get_active_table().model()
self.setQuery(model, "SELECT " \
"n.last_connection as LastConnection, " \
@ -418,9 +487,9 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"c.dst_ip as DstIP, " \
"c.process as Process, " \
"c.process_args as Args, " \
"count(c.process) as ProcessesExec " \
"count(c.process) as Hits " \
"FROM nodes as n, connections as c " \
"WHERE n.addr = '%s' GROUP BY c.process,c.dst_host %s" % (data, self._get_order()))
"WHERE n.addr = '%s' %s GROUP BY c.process %s" % (data, s, self._get_order()))
def _set_rules_query(self, data):
model = self._get_active_table().model()
@ -521,27 +590,6 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"FROM users as u, connections as c " \
"WHERE u.what = '%s' AND u.what LIKE '%%(' || c.uid || ')' GROUP BY c.dst_ip %s" % (data, self._get_order()))
# launched from a thread
def update(self, addr=None, stats=None):
# lock mandatory when there're multiple clients
with self._lock:
if stats is not None:
self._stats = stats
# do not update any tab if the window is not visible
if self.isVisible() and self.isMinimized() == False:
self._trigger.emit()
def update_status(self):
self.startButton.setDown(self.daemon_connected)
self.startButton.setChecked(self.daemon_connected)
self.startButton.setDisabled(not self.daemon_connected)
if self.daemon_connected:
self.statusLabel.setText("running")
self.statusLabel.setStyleSheet('color: green')
else:
self.statusLabel.setText("not running")
self.statusLabel.setStyleSheet('color: red')
def _on_save_clicked(self):
tab_idx = self.tabWidget.currentIndex()
@ -571,10 +619,12 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
values.append(table.model().index(row, col).data())
w.writerow(values)
def _setup_table(self, widget, tableWidget, table_name, fields="*", group_by="", order_by="2", limit="", resize_cols=(), model=None):
def _setup_table(self, widget, tableWidget, table_name, fields="*", group_by="", order_by="2", limit="", resize_cols=(), model=None, delegate=None):
tableWidget.setSortingEnabled(True)
if model == None:
model = QSqlQueryModel()
if delegate != None:
tableWidget.setItemDelegate(ColorizedDelegate(self, config=delegate))
self.setQuery(model, "SELECT " + fields + " FROM " + table_name + group_by + " ORDER BY " + order_by + " DESC" + limit)
tableWidget.setModel(model)
@ -587,21 +637,34 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
header.sortIndicatorChanged.connect(self._cb_table_header_clicked)
for _, col in enumerate(resize_cols):
header.setSectionResizeMode(col, QtWidgets.QHeaderView.ResizeToContents)
for _, col in enumerate(resize_cols):
header.setSectionResizeMode(col, QtWidgets.QHeaderView.ResizeToContents)
return tableWidget
def _show_local_stats(self, show):
self.daemonVerLabel.setVisible(show)
self.uptimeLabel.setVisible(show)
self.rulesLabel.setVisible(show)
self.consLabel.setVisible(show)
self.droppedLabel.setVisible(show)
# launched from a thread
def update(self, is_local=True, stats=None, need_query_update=True):
# lock mandatory when there're multiple clients
with self._lock:
if stats is not None:
self._stats = stats
# do not update any tab if the window is not visible
if self.isVisible() and self.isMinimized() == False:
self._trigger.emit(is_local, need_query_update)
@QtCore.pyqtSlot()
def _on_update_triggered(self):
def update_status(self):
self.startButton.setDown(self.daemon_connected)
self.startButton.setChecked(self.daemon_connected)
self.startButton.setDisabled(not self.daemon_connected)
if self.daemon_connected:
self.statusLabel.setText("running")
self.statusLabel.setStyleSheet('color: green')
else:
self.statusLabel.setText("not running")
self.statusLabel.setStyleSheet('color: red')
@QtCore.pyqtSlot(bool, bool)
def _on_update_triggered(self, is_local, need_query_update=False):
if self._stats is None:
self.daemonVerLabel.setText("")
self.uptimeLabel.setText("")
@ -609,16 +672,21 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.consLabel.setText("")
self.droppedLabel.setText("")
else:
rows = self.TABLES[1]['view'].model().rowCount()
self._show_local_stats(rows <= 1)
if rows <= 1:
self.daemonVerLabel.setText(self._stats.daemon_version)
nodes = self._nodes.count()
self.daemonVerLabel.setText(self._stats.daemon_version)
if nodes <= 1:
self.uptimeLabel.setText(str(datetime.timedelta(seconds=self._stats.uptime)))
self.rulesLabel.setText("%s" % self._stats.rules)
self.consLabel.setText("%s" % self._stats.connections)
self.droppedLabel.setText("%s" % self._stats.dropped)
else:
self.uptimeLabel.setText("")
self.rulesLabel.setText("")
self.consLabel.setText("")
self.droppedLabel.setText("")
self._refresh_active_table()
if need_query_update:
self._refresh_active_table()
# prevent a click on the window's x
# from quitting the whole application
@ -634,5 +702,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
def setQuery(self, model, q):
with self._lock:
model.setQuery(q, self._db_sqlite)
model.query().clear()
try:
model.query().clear()
model.setQuery(q, self._db_sqlite)
except Exception as e:
print(self._address, "setQuery() exception: ", e)

View file

@ -160,12 +160,14 @@
<item row="2" column="1" colspan="4">
<widget class="QTreeView" name="eventsTable">
<property name="styleSheet">
<string notr="true">*[action=&quot;deny&quot;] { text-color: rgb(239, 41, 41);}
*[action=&quot;allow&quot;] { text-color: rgb(115, 210, 22);}</string>
<string notr="true"/>
</property>
<property name="autoScroll">
<bool>false</bool>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
@ -236,7 +238,7 @@
<item row="1" column="0">
<widget class="QLabel" name="tipNodesLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:7pt;&quot;&gt;(double click on the Name column to view details of an item)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:7pt;&quot;&gt;(double click on the Addr column to view details of an item)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
@ -649,7 +651,7 @@
<item row="4" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<layout class="QHBoxLayout" name="statsLayout">
<item>
<widget class="QLabel" name="label_5">
<property name="sizePolicy">

View file

@ -26,9 +26,11 @@ from dialogs.preferences import PreferencesDialog
from nodes import Nodes
from config import Config
from version import version
from database import Database
class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
_new_remote_trigger = QtCore.pyqtSignal(str, ui_pb2.PingRequest)
_update_stats_trigger = QtCore.pyqtSignal(str, str, ui_pb2.PingRequest)
_version_warning_trigger = QtCore.pyqtSignal(str, str)
_status_change_trigger = QtCore.pyqtSignal()
@ -37,6 +39,8 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
def __init__(self, app, on_exit):
super(UIService, self).__init__()
self._db = Database.instance()
self._db_sqlite = self._db.get_db()
self._cfg = Config.init()
self._last_ping = None
self._version_warning_shown = False
@ -48,7 +52,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._exit = False
self._msg = QtWidgets.QMessageBox()
self._prompt_dialog = PromptDialog()
self._stats_dialog = StatsDialog(dbname="general")
self._stats_dialog = StatsDialog(dbname="general", db=self._db)
self._remote_lock = Lock()
self._remote_stats = {}
@ -63,7 +67,14 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._nodes = Nodes.instance()
self.last_stats = None
self._last_stats = {}
self._last_items = {
'hosts':{},
'procs':{},
'addrs':{},
'ports':{},
'users':{}
}
# https://gist.github.com/pklaus/289646
def _setup_interfaces(self):
@ -89,6 +100,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._version_warning_trigger.connect(self._on_diff_versions)
self._status_change_trigger.connect(self._on_status_change)
self._new_remote_trigger.connect(self._on_new_remote)
self._update_stats_trigger.connect(self._on_update_stats)
self._stats_dialog._notification_trigger.connect(self._on_new_notification)
self._stats_dialog._shown_trigger.connect(self._on_stats_dialog_shown)
@ -159,11 +171,17 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._msg.show()
self._version_warning_shown = True
@QtCore.pyqtSlot(str, str, ui_pb2.PingRequest)
def _on_update_stats(self, proto, addr, request):
main_need_refresh, details_need_refresh = self._populate_stats(self._db, proto, addr, request.stats)
is_local_request = self._is_local_request(proto, addr)
self._stats_dialog.update(is_local_request, request.stats, main_need_refresh or details_need_refresh)
@QtCore.pyqtSlot(str, ui_pb2.PingRequest)
def _on_new_remote(self, addr, request):
self._remote_stats[addr] = {
'last_ping': datetime.now(),
'dialog': StatsDialog(address=addr, dbname=addr)
'dialog': StatsDialog(address=addr, dbname=addr, db=self._db)
}
self._remote_stats[addr]['dialog'].daemon_connected = True
self._remote_stats[addr]['dialog'].update(addr, request.stats)
@ -202,13 +220,11 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._status_change_trigger.emit()
was_connected = self._connected
def _is_local_request(self, context):
peer = context.peer()
if peer.startswith("unix:"):
def _is_local_request(self, proto, addr):
if proto == "unix":
return True
elif peer.startswith("ipv4:"):
_, addr = self._get_peer(peer)
elif proto == "ipv4" or proto == "ipv6":
for name, ip in self._interfaces.items():
if addr == ip:
return True
@ -229,29 +245,50 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
p = peer.split(":")
return p[0], p[1]
def _delete_node(self, peer):
try:
proto, addr = self._get_peer(peer)
if addr in self._last_stats:
del self._last_stats[addr]
for table in self._last_items:
if addr in self._last_items[table]:
del self._last_items[table][addr]
self._nodes.update(self._db, proto, addr, Nodes.OFFLINE)
self._nodes.delete(peer)
self._stats_dialog.update(True, None, True)
except Exception as e:
print("_delete_node() exception:", e)
def _populate_stats(self, db, proto, addr, stats):
fields = []
values = []
if db == None:
print("populate_stats() db None")
return
_node = self._nodes.get_node(proto+":"+addr)
if _node == None:
return
version = _node['data'].version if _node != None else ""
hostname = _node['data'].name if _node != None else ""
db.insert("nodes",
"(addr, status, hostname, daemon_version, daemon_uptime, " \
"daemon_rules, cons, cons_dropped, version, last_connection)",
(addr, Nodes.ONLINE, hostname, stats.daemon_version, str(timedelta(seconds=stats.uptime)), stats.rules, stats.connections, stats.dropped,
version, datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
main_need_refresh = False
details_need_refresh = False
try:
if db == None:
print("populate_stats() db None")
return main_need_refresh, details_need_refresh
_node = self._nodes.get_node(proto+":"+addr)
if _node == None:
return main_need_refresh, details_need_refresh
version = _node['data'].version if _node != None else ""
hostname = _node['data'].name if _node != None else ""
db.insert("nodes",
"(addr, status, hostname, daemon_version, daemon_uptime, " \
"daemon_rules, cons, cons_dropped, version, last_connection)",
(addr, Nodes.ONLINE, hostname, stats.daemon_version, str(timedelta(seconds=stats.uptime)), stats.rules, stats.connections, stats.dropped,
version, datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
if addr not in self._last_stats:
self._last_stats[addr] = []
for row, event in enumerate(stats.events):
if self.last_stats != None and event in self.last_stats.events:
if event in self._last_stats[addr]:
continue
need_refresh=True
db.insert("connections",
"(time, node, action, protocol, src_ip, src_port, dst_ip, dst_host, dst_port, uid, process, process_args, rule)",
(str(datetime.now()), addr, event.rule.action, event.connection.protocol, event.connection.src_ip, str(event.connection.src_port),
@ -267,37 +304,59 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
event.rule.operator.operand + ": " + event.rule.operator.data),
action_on_conflict="IGNORE")
last_items = self.last_stats.by_host.items() if self.last_stats != None else ''
self._populate_stats_events(db, stats, "hosts", ("what", "hits"), (1,2), stats.by_host.items(), last_items)
last_items = self.last_stats.by_executable.items() if self.last_stats != None else ''
self._populate_stats_events(db, stats, "procs", ("what", "hits"), (1,2), stats.by_executable.items(), last_items)
last_items = self.last_stats.by_address.items() if self.last_stats != None else ''
self._populate_stats_events(db, stats, "addrs", ("what", "hits"), (1,2), stats.by_address.items(), last_items)
last_items = self.last_stats.by_port.items() if self.last_stats != None else ''
self._populate_stats_events(db, stats, "ports", ("what", "hits"), (1,2), stats.by_port.items(), last_items)
last_items = self.last_stats.by_uid.items() if self.last_stats != None else ''
self._populate_stats_events(db, stats, "users", ("what", "hits"), (1,2), stats.by_uid.items(), last_items)
self.last_stats = stats
details_need_refresh = self._populate_stats_details(db, addr, stats)
self._last_stats[addr] = stats.events
except Exception as e:
print("_populate_stats() exception: ", e)
def _populate_stats_events(self, db, stats, table, colnames, cols, items, last_items):
return main_need_refresh, details_need_refresh
def _populate_stats_details(self, db, addr, stats):
need_refresh = False
changed = self._populate_stats_events(db, addr, stats, "hosts", ("what", "hits"), (1,2), stats.by_host.items())
if changed: need_refresh = True
changed = self._populate_stats_events(db, addr, stats, "procs", ("what", "hits"), (1,2), stats.by_executable.items())
if changed: need_refresh = True
changed = self._populate_stats_events(db, addr, stats, "addrs", ("what", "hits"), (1,2), stats.by_address.items())
if changed: need_refresh = True
changed = self._populate_stats_events(db, addr, stats, "ports", ("what", "hits"), (1,2), stats.by_port.items())
if changed: need_refresh = True
changed = self._populate_stats_events(db, addr, stats, "users", ("what", "hits"), (1,2), stats.by_uid.items())
if changed: need_refresh = True
return need_refresh
def _populate_stats_events(self, db, addr, stats, table, colnames, cols, items):
fields = []
values = []
for row, event in enumerate(items):
if last_items != '' and event in last_items:
continue
what, hits = event
# FIXME: this is suboptimal
# BUG: there can be users with same id on different machines but with different names
if table == "users":
what = self._get_user_id(what)
fields.append(what)
values.append(int(hits))
# FIXME: default acion on conflict is to replace. If there're multiple nodes connected,
# stats are painted once on each update per node.
db.insert_batch(table, colnames, cols, fields, values)
need_refresh = False
try:
if addr not in self._last_items[table].keys():
self._last_items[table][addr] = {}
if items == self._last_items[table][addr]:
return need_refresh
for row, event in enumerate(items):
if event in self._last_items[table][addr]:
continue
need_refresh = True
what, hits = event
# FIXME: this is suboptimal
# BUG: there can be users with same id on different machines but with different names
if table == "users":
what = self._get_user_id(what)
fields.append(what)
values.append(int(hits))
# FIXME: default action on conflict is to replace. If there're multiple nodes connected,
# stats are painted once per node on each update.
if need_refresh:
db.insert_batch(table, colnames, cols, fields, values)
self._last_items[table][addr] = items
except Exception as e:
print("details exception: ", e)
return need_refresh
def Ping(self, request, context):
try:
@ -306,13 +365,8 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._version_warning_trigger.emit(request.stats.daemon_version, version)
proto, addr = self._get_peer(context.peer())
# global stats must be updated regardles if there're 1 or more clients
self._populate_stats(self._stats_dialog.get_db(), proto, addr, request.stats)
if self._is_local_request(context):
self._stats_dialog.update(addr, request.stats)
else:
with self._remote_lock:
self._stats_dialog.update(addr, request.stats)
# do not update db here, do it on the main thread
self._update_stats_trigger.emit(proto, addr, request)
#else:
# with self._remote_lock:
# # XXX: disable this option for now
@ -330,7 +384,8 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
def AskRule(self, request, context):
self._asking = True
rule, timeout_triggered = self._prompt_dialog.promptUser(request, self._is_local_request(context), context.peer())
proto, addr = self._get_peer(context.peer())
rule, timeout_triggered = self._prompt_dialog.promptUser(request, self._is_local_request(proto, addr), context.peer())
if timeout_triggered:
_title = request.process_path
if _title == "":
@ -354,19 +409,17 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
try:
_node = self._nodes.add(context, node_iter.next())
proto, addr = self._get_peer(context.peer())
self._nodes.update(self._stats_dialog.get_db(), proto, addr)
self._nodes.update(self._db, proto, addr)
stop_event = Event()
def _on_client_closed():
stop_event.set()
proto, addr = self._get_peer(context.peer())
self._nodes.update(self._stats_dialog.get_db(), proto, addr, Nodes.OFFLINE)
self._nodes.delete(context.peer())
self._delete_node(context.peer())
context.add_callback(_on_client_closed)
except Exception as e:
print("[Notifications] exception adding new node", e)
print("[Notifications] exception adding new node:", e)
context.cancel()
return node_iter
while self._exit == False:
@ -379,7 +432,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
_node['notifications'].task_done()
yield noti
except Exception as e:
print("[Notifications] exception getting notification from queue", e)
print("[Notifications] exception getting notification from queue:", e)
context.cancel()
return node_iter