-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathConfig.py
More file actions
317 lines (250 loc) · 11.7 KB
/
Config.py
File metadata and controls
317 lines (250 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
import threading
import os
import copy
import logging
from ransack import Parser as RansackParser
from .actions.Drop import DropAction, DropMsg
from .actions.Action import Action
from .Parser import Parser
from .AddressGroup import AddressGroup
from .Rule import Rule, clearCounters, STAT_KEYPREFIX
logger = logging.getLogger(__name__)
class RepeatTimer(threading.Timer):
def run(self):
while not self.finished.wait(self.interval):
self.function(*self.args,**self.kwargs)
class Config():
"""
Configuration loaded from the file and command line arguments that tunes
functionality of the reporter module.
The class provides loading/reloading config, matching rules from config,
counters update, printing loaded config.
"""
def __init__(self, path, dry = False, trap = None, wardenargs = None, module_name = "reporter", autoreload = 0, use_namespace = True):
"""
:param path Path to Yaml configuration file
:param dry "Don't run yet" switch to parse but not execute/init
:param trap Instance of TRAP client used in TrapAction
:param wardenargs Arguments to initiate Warden Client used in WardenAction
:param module_name Name usually given to Run() by the reporter's
developer, can be related to the name of script
:param autoreload Timeout in seconds to perform automatic checking and
reload of config file, default is 0 to disable this feature.
:param use_namespace Use generated name created from namespace (from
config) and module_name; if use_namespace is set to False, user
overrided the name by -n and only the module_name is used.
"""
self.parser = RansackParser()
self.trap = trap
self.autoreload = autoreload
self.path = path
self.configdir = os.path.dirname(path)
self.conf = None
self.config_mtime = 0
self.module_name = module_name
self.use_namespace = use_namespace
self.timer = None
self.wardenclient = None
self.addrGroups = dict()
self.actions = dict()
self.rules = list()
try:
self.loadConfig()
except (SyntaxError, LookupError) as e:
logger.error("Error: Loading configuration file failed: %s " % str(e))
if not self.conf:
raise ImportError("Loading YAML file (%s) failed. Isn't it empty?" % path)
# Configuration was succsesfuly loaded, it is possible to continue with init.
if not dry:
logger.info("Subscription to configuration changes.")
if self.autoreload > 0:
self.timer = RepeatTimer(self.autoreload, self.checkConfigChanges)
self.timer.start()
if wardenargs:
try:
import warden_client
except:
logger.error("Loading warden_client module failed. Install it or remove '--warden' from the module's arguments.")
raise ImportError("Warden client module could not be imported.")
config = warden_client.read_cfg(wardenargs)
config['name'] = self.name
self.wardenclient = warden_client.Client(**config)
# update modification time of loaded config, this timestamp is used checkConfigChanges()
self.config_mtime = os.stat(self.path).st_mtime
def __del__(self):
logger.warning("Freeing configuration...")
if self.timer:
logger.info("Stopping configuration autoreload.")
self.timer.cancel()
if self.wardenclient:
self.wardenclient.close()
def printConfig(self, signum = -1, frame = None):
"""Print current configuration, this is used as a signal handler for SIGUSR2."""
print(str(self))
def checkConfigChanges(self, signum = -1, frame = None):
"""
Check if the configuration file was modified (time of modification is
newer); try to reload it using self.loadConfig().
This method is called as a signal handler (SIGUSR1) or by the timer (self.timer).
"""
if signum != -1:
logger.warning(f"Received signal {signum}.")
logger.debug("Checking for changes in configuration.")
mtime = os.stat(self.path).st_mtime
if mtime <= self.config_mtime:
logger.debug("Skipping configuration reload, we have newer version of the file.")
return
self.config_mtime = mtime
try:
self.loadConfig()
except (SyntaxError, LookupError) as e:
logger.error("Loading new configuration failed due to error(s), continue with the old one. " + str(e))
def loadConfig(self):
"""
Load new configuration and when everything is ok, replace the previous
one. When the new configuration contains errors, keep the previous one.
Raises: SyntaxError, LookupError
"""
logger.warning("Loading new configuration.")
conf = Parser(self.path)
if not conf or not conf.config:
raise SyntaxError("Yaml parsing error: " + str(e))
addrGroups = dict()
parser_context = dict()
smtp_conns = dict()
actions = dict()
rules = list()
if "namespace" not in conf:
logger.error("ERROR: 'namespace' is required but is missing. Please specify 'namespace' in YAML config to identify names of reporters (e.g., com.example.collectornemea).")
# Create all address groups if there are any
if "addressgroups" in conf:
for i in conf["addressgroups"]:
addrGroups[i["id"]] = AddressGroup(i)
parser_context[i["id"]] = addrGroups[i["id"]].content
# Pass address groups as a context for the parser
self.parser = RansackParser(parser_context)
# Check if "smtp_connections" exists when there is some "email" action in "custom_actions"
if ("custom_actions" in conf
and any([i.__contains__("email") for i in conf["custom_actions"]])
and "smtp_connections" not in conf):
raise LookupError("'smtp_connections' is required when there is at least one 'email' action but it is missing in YAML config. Check your YAML config.")
# Parse parameters for all smtp connections
if "smtp_connections" in conf:
for i in conf["smtp_connections"]:
smtp_conns[i["id"]] = i
# Parse and instantiate all custom actions
if "custom_actions" in conf:
for i in conf["custom_actions"]:
if "mark" in i:
from .actions.Mark import MarkAction
actions[i["id"]] = MarkAction(i)
elif "mongo" in i:
from .actions.Mongo import MongoAction
actions[i["id"]] = MongoAction(i)
elif "email" in i:
from .actions.Email import EmailAction
actions[i["id"]] = EmailAction(i, smtp_conns[i['email']['smtp_connection']])
elif "file" in i:
from .actions.File import FileAction
actions[i["id"]] = FileAction(i)
elif "syslog" in i:
from .actions.Syslog import SyslogAction
actions[i["id"]] = SyslogAction(i)
elif "warden" in i:
"""
Pass Warden Client instance to the Warden action
"""
if not self.wardenclient:
raise SyntaxError("Cannot use warden action if --warden argument was not provided.")
from .actions.Warden import WardenAction
actions[i["id"]] = WardenAction(i, self.wardenclient)
elif "trap" in i:
"""
Pass TRAP context instance to the TRAP action
"""
from .actions.Trap import TrapAction
actions[i["id"]] = TrapAction(i, self.trap)
elif "drop" in i:
logger.warning("Drop action mustn't be specified in custom_actions!")
continue
else:
raise SyntaxError("Undefined action: " + str(i))
actions["drop"] = DropAction()
# Parse all rules and match them with actions
# There must be at least one rule (mandatory field)
if "rules" in conf:
if conf["rules"]:
for i in conf["rules"]:
r = Rule(i, actions, parser = self.parser, module_name = self.module_name)
rules.append(r)
if not rules:
raise SyntaxError("YAML file should contain at least one `rule` in `rules`.")
else:
raise SyntaxError("YAML file must contain `rules`.")
self.conf = conf
self.rules = rules
self.actions = actions
self.addrGroups = addrGroups
self.smtp_conns = smtp_conns
if self.use_namespace:
self.name = ".".join([conf.get("namespace", "com.example"), self.module_name])
else:
self.name = self.module_name
clearCounters(STAT_KEYPREFIX, self.module_name)
logging.warning("Success: New configuration loaded, applied and counters reset.")
def match(self, msg):
"""
Check if msg matches rules from config file.
Return a list of bool values representing results of all tested rules.
"""
results = []
actionsDone = []
try:
for rule in self.rules:
self.clearActionLog()
res = rule.filter(msg)
#logger.debug("Filter by rule: %s \n message: %s\n\nresult: %s", rule, msg, res)
#print("Filter by rule: %s \n message: %s\n\nresult: %s" % (rule.rule(), msg, res))
results.append(res)
tmp_msg = copy.deepcopy(msg)
if res:
# condition is True
rule.actions(tmp_msg)
logger.info("action running")
else:
# condition is False
rule.elseactions(tmp_msg)
logger.info("else action running")
actionsDone.append(self.getActionLog())
except DropMsg:
# This exception breaks the processing of rule list.
pass
return (results, actionsDone)
def getActionLog(self):
return Action.actionLog
def clearActionLog(self):
Action.actionLog = []
def loglevel(self):
"""Get logging level
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0
"""
try:
return (self.conf["reporter"]["loglevel"]) * 10
except KeyError:
return 30
def __str__(self):
smtp = dict()
for smtp_id, conns in self.smtp_conns.items():
smtp[smtp_id] = "\n".join(["\t{}: {}".format(param, val) for param, val in conns.items()])
s = "\n".join("{}:\n{}\n".format(smtp_id, params) for smtp_id, params in smtp.items())
ag = "\n".join([str(self.addrGroups[key]) for key in self.addrGroups])
a = "\n".join([key + ":\n\t" + str(self.actions[key]) for key in self.actions])
r = "\n".join([str(val) for val in self.rules])
string = "Namespace: {0}\n----------------\nSmtp connections:\n{1}\n----------------\nAddress Groups:\n{2}\n"\
"----------------\nCustom Actions:\n{3}\n----------------\nRules:\n{4}\n".format(self.conf.get("namespace"), s, ag, a, r)
return string