Skip to content

Commit 2a233c1

Browse files
ui: introducing plugins, improved actions
Up until now we had a way of customizing the views, by defining "actions". See for context cba52cf The configuration syntax has not changed, but now every "action" is a python plugin, for example when loading this configuration: { "name": "commonDelegateConfig", "actions": { "highlight": { "enabled": true, "cells": [ { "text": ["allow", "✓ online"], "color": "white", "bgcolor: "green", we'll try to load "highlight" as plugin, which should exist under opensnitch/plugins/highligh/highlight.py Three new plugins has been added: - Highlight: colorize cells or rows based on patterns. - Downloader: a simple downloader which downloads files to local directories, for example to download blocklists. - Virustotal: a plugin to analyze IPs, domains and checksums with the API of virustotal when a new popup is fired. There're 3 points where the plugins are configured and executed: - opensnitch/service.py - _load_plugins() (background/global plugins) - opensnitch/dialogs/prompt/__init__.py - _configure_plugins(), _post_popup_plugins() - opensnitch/dialogs/processdetails.py - _configure_plugins() Plugins can't be configured from the GUI (yet). For more details, read: opensnitch/plugins/__init__.py opensnitch/actions/__init__.py opensnitch/plugins/downloader/downloader.py opensnitch/plugins/virustotal/virustotal.py
1 parent 791e28d commit 2a233c1

File tree

19 files changed

+1671
-233
lines changed

19 files changed

+1671
-233
lines changed

ui/opensnitch/actions/__init__.py

Lines changed: 146 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,97 @@
1-
from PyQt5.QtCore import QObject
2-
31
import json
42
import os
53
import glob
64
import sys
75

8-
from opensnitch.utils.xdg import xdg_config_home
9-
from opensnitch.actions import highlight
10-
from opensnitch.actions.default_configs import commonDelegateConfig, rulesDelegateConfig, fwDelegateConfig
6+
from PyQt5.QtCore import QObject
117

12-
class Actions(QObject):
13-
"""List of actions to perform on the data that is displayed on the GUI.
14-
Whenever an item matches a condition an action is applied, for example:
15-
- if the text of a cell matches a condition for the given columns,
16-
then the properties of the cell/row and the text are customized.
8+
from opensnitch.utils.xdg import xdg_config_home
9+
from opensnitch.actions.default_configs import (
10+
commonDelegateConfig,
11+
rulesDelegateConfig,
12+
fwDelegateConfig
13+
)
1714

18-
There's only 1 action supported right now:
19-
- highlight: for customizing rows and cells appearance.
15+
from opensnitch.plugins import PluginsList
16+
from opensnitch.plugins import PluginsManager
2017

21-
There're 3 actions by default of type Highlight:
22-
- rules: applied to the rules to colorize the columns Enabled and
23-
Action
24-
- firewall: applied to the fw rules to colorize the columns Action and
25-
Target.
26-
- common: applied to the rest of the views to colorize the column
27-
Action.
18+
class Actions(QObject):
19+
"""List of actions to perform on the data that is displayed on the GUI,
20+
defined in JSON files.
2821
29-
Users can modify the default actions, by adding more patterns to colorize.
30-
At the same time they can also create new actions to be applied on certain views.
22+
Whenever an item (popup, cell, etc) matches a condition, an action
23+
(config/plugin) is applied. For example:
24+
- if the text of a cell matches a condition for the given columns,
25+
then the properties of the cell/row and the text are colorized.
26+
- if the result of an analysis of a domain is malicious, colorize
27+
popups' text labels in red + add a tab with the result of the analysis.
3128
32-
The format of the actions is JSON:
29+
The actions are defined in JSON format:
3330
{
3431
"created": "....",
3532
"name": "...",
33+
"type": ["views"],
3634
"actions": {
3735
"highlight": {
3836
"cells": [
39-
{
40-
"text": ["allow", "True", "online"],
41-
"cols": [3,5,6],
42-
"color": "green",
43-
},
44-
{
45-
"text": ["deny", "False", "offline"],
46-
"cols": [3,5,6],
47-
"color": "red",
48-
}
49-
],
37+
{
38+
"text": ["allow", "True", "online"],
39+
"cols": [3,5,6],
40+
"color": "green",
41+
},
42+
{
43+
"text": ["deny", "False", "offline"],
44+
"cols": [3,5,6],
45+
"color": "red",
46+
}
47+
],
5048
"rows": []
5149
}
5250
}
5351
52+
"type" field is the area of the GUI where the actions will be applied:
53+
54+
"global" -> global actions, like background tasks.
55+
"views" -> applies to Views (lists of items), QItemDelegate
56+
"popups" -> applies to popups.
57+
"main-dialog" -> applies to the main window.
58+
"proc-dialog" -> applies to the Process dialog.
59+
"procs-list" -> applies to the Procs view. (TODO)
60+
"domains-list" -> applies to the Domains view. (TODO)
61+
"ips-list" -> applies to the IPs view. (TODO)
62+
"db" -> applies to the DB. Modify items before inserting, react to
63+
data being added, etc. (TODO)
64+
65+
"actions" is the list of actions to execute:
66+
67+
- the name of the action defines the python plugin to load:
68+
"highligh" -> plugins/highligh/highlight.py
69+
"downloader" -> plugins/downloader/downloader.py, etc.
70+
- every action has its own plugin (*.py file) which is in charge
71+
of parse and compile to configuration if needed.
72+
For example for "highlight" action, "color": "red" is compiled
73+
to QtColor("red")
74+
75+
There're 3 hardcoded actions by default of type Highlight:
76+
- rules: applied to the rules to colorize the columns Enabled and
77+
Action
78+
- firewall: applied to the fw rules to colorize the columns Action and
79+
Target.
80+
- common: applied to the rest of the views to colorize the column
81+
Action.
82+
83+
Users can modify the default actions, by adding more patterns to colorize,
84+
and saving them to $XDG_CONFIG_HOME/opensnitch/actions/myaction.json
85+
86+
At the same time they can also create new actions in that directory
87+
to be applied on certain views.
88+
5489
"""
5590
__instance = None
5691

5792
# list of loaded actions
58-
_actions = None
93+
_actions_list = {}
94+
_plugins = []
5995

6096

6197
KEY_ACTIONS = "actions"
@@ -67,7 +103,7 @@ class Actions(QObject):
67103

68104
# default paths to look for actions
69105
_paths = [
70-
os.path.dirname(sys.modules[__name__].__file__) + "/data/",
106+
#os.path.dirname(sys.modules[__name__].__file__) + "/data/",
71107
"{0}/{1}".format(xdg_config_home, "/opensnitch/actions/")
72108
]
73109

@@ -80,17 +116,33 @@ def instance():
80116
def __init__(self, parent=None):
81117
QObject.__init__(self)
82118
self._actions_list = {}
119+
self._plugin_mgr = PluginsManager.instance()
83120
try:
84121
base_dir = "{0}/{1}".format(xdg_config_home, "/opensnitch/actions/")
85122
os.makedirs(base_dir, 0o700)
86-
except:
87-
pass
123+
except Exception as e:
124+
print("actions.__init__ exception:", e)
125+
#print("ActionsLists:", PluginsList.actions)
88126

89127
def _load_default_configs(self):
128+
# may be overwritten by user choice
90129
self._actions_list[commonDelegateConfig[Actions.KEY_NAME]] = self.compile(commonDelegateConfig)
91130
self._actions_list[rulesDelegateConfig[Actions.KEY_NAME]] = self.compile(rulesDelegateConfig)
92131
self._actions_list[fwDelegateConfig[Actions.KEY_NAME]] = self.compile(fwDelegateConfig)
93132

133+
def load(self, action_file):
134+
"""read a json file from disk and create the action."""
135+
try:
136+
with open(action_file, 'r') as fd:
137+
data=fd.read()
138+
obj = json.loads(data)
139+
action = self.compile(obj)
140+
return obj, action
141+
except:
142+
pass
143+
144+
return None, None
145+
94146
def loadAll(self):
95147
"""look for actions firstly on default system path, secondly on
96148
user's home.
@@ -103,37 +155,53 @@ def loadAll(self):
103155

104156
for path in self._paths:
105157
for jfile in glob.glob(os.path.join(path, '*.json')):
106-
self.load(jfile)
107-
108-
def load(self, action_file):
109-
"""read a json file from disk and create the action."""
110-
with open(action_file, 'r') as fd:
111-
data=fd.read()
112-
obj = json.loads(data)
113-
self._actions_list[obj[Actions.KEY_NAME]] = self.compile(obj)
158+
#print("Actions.loadconf()", jfile)
159+
obj, action = self.load(jfile)
160+
if obj and action:
161+
self._actions_list[obj[Actions.KEY_NAME]] = action
114162

115-
def compile(self, obj):
163+
def compile(self, json_obj):
164+
"""translates json definitions to python objects"""
116165
try:
117-
if Actions.KEY_NAME not in obj or obj[Actions.KEY_NAME] == "":
166+
if Actions.KEY_NAME not in json_obj or json_obj[Actions.KEY_NAME] == "":
118167
return None
119-
if obj.get(Actions.KEY_ACTIONS) == None:
168+
if json_obj.get(Actions.KEY_ACTIONS) == None:
120169
return None
121170

122-
for action in obj[Actions.KEY_ACTIONS]:
123-
if action == highlight.Highlight.NAME:
124-
h = highlight.Highlight(obj[Actions.KEY_ACTIONS][action])
125-
h.compile()
126-
obj[Actions.KEY_ACTIONS][action]= h
127-
else:
128-
print("Actions exception: Action '{0}' not supported yet".format(obj[Actions.KEY_NAME]))
129-
130-
return obj
171+
# "actions": { "highlight": ..., "virustotal": ..., }
172+
#print("plugins >>", PluginsList.names)
173+
for action_name in json_obj[Actions.KEY_ACTIONS]:
174+
action_obj = json_obj[Actions.KEY_ACTIONS][action_name]
175+
if action_obj == None or action_obj.get('enabled') == None or action_obj.get('enabled') == False:
176+
print("actions.compile() skipping disabled action '{0}'".format(action_name))
177+
# FIXME: if one of the action is not enabled, we're
178+
# invalidating all the configured actions.
179+
return None
180+
181+
# see if the plugin is loaded, if it's not, try to load it.
182+
if PluginsList.names.get(action_name.capitalize()) == None:
183+
if self._plugin_mgr.load_plugin_byname(action_name, force=True) == False:
184+
print("actions.compile() unable to load plugin name '{0}'".format(action_name))
185+
return None
186+
187+
# allow to use "Plugin" or "plugin" to name actions in json
188+
# files.
189+
# python class will be always capitalized.
190+
_p = PluginsList.names.get(action_name.capitalize())
191+
plug = _p(action_obj)
192+
193+
# compile the plugin, preparing/configuring the plugin
194+
# before it's used.
195+
plug.compile()
196+
197+
# save the "compiled" action to the action list
198+
json_obj[Actions.KEY_ACTIONS][action_name]= plug
199+
200+
return json_obj
131201
except Exception as e:
132202
print("Actions.compile() exception:", e)
133203
return None
134204

135-
136-
137205
def getAll(self):
138206
return self._actions_list
139207

@@ -144,7 +212,25 @@ def get(self, name):
144212
try:
145213
return self._actions_list[name]
146214
except Exception as e:
147-
print("get() exception:", e)
215+
print("actions.get() exception:", e, "name:", name)
216+
return None
217+
218+
def getByType(self, acttype):
219+
try:
220+
actlist = {}
221+
for name in self._actions_list:
222+
act = self._actions_list[name]
223+
if act == None:
224+
print("actions.getByType() none:", name)
225+
continue
226+
types = act.get('type')
227+
if types == None:
228+
continue
229+
if acttype in types:
230+
actlist[name] = self._actions_list[name]
231+
return actlist
232+
except Exception as e:
233+
print("actions.getByType() ({0}) exception: {1}".format(acttype, e))
148234
return None
149235

150236
def delete(self, name):

0 commit comments

Comments
 (0)