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
|
#!/usr/bin/env python3
#
# Copyright (C) 2022-2023 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import json
import re
import select
from copy import deepcopy
from os import getpid, environ
from pathlib import Path
from signal import signal, SIGTERM, SIGINT
from sys import exit
from systemd import journal
from vyos.utils.dict import dict_search
from vyos.utils.process import run
# Identify this script
my_pid = getpid()
my_name = Path(__file__).stem
# handle termination signal
def handle_signal(signal_type, frame):
if signal_type == SIGTERM:
journal.send('Received SIGTERM signal, stopping normally',
SYSLOG_IDENTIFIER=my_name)
if signal_type == SIGINT:
journal.send('Received SIGINT signal, stopping normally',
SYSLOG_IDENTIFIER=my_name)
exit(0)
# Class for analyzing and process messages
class Analyzer:
# Initialize settings
def __init__(self, config: dict) -> None:
self.config = {}
# Prepare compiled regex objects
for event_id, event_config in config.items():
script = dict_search('script.path', event_config)
# Check for arguments
if dict_search('script.arguments', event_config):
script_arguments = dict_search('script.arguments', event_config)
script = f'{script} {script_arguments}'
# Prepare environment
environment = deepcopy(environ)
# Check for additional environment options
if dict_search('script.environment', event_config):
for env_variable, env_value in dict_search(
'script.environment', event_config).items():
environment[env_variable] = env_value.get('value')
# Create final config dictionary
pattern_raw = event_config['filter']['pattern']
pattern_compiled = re.compile(
rf'{event_config["filter"]["pattern"]}')
pattern_config = {
pattern_compiled: {
'pattern_raw':
pattern_raw,
'syslog_id':
dict_search('filter.syslog-identifier', event_config),
'pattern_script': {
'path': script,
'environment': environment
}
}
}
self.config.update(pattern_config)
# Execute script safely
def script_run(self, pattern: str, script_path: str,
script_env: dict) -> None:
try:
run(script_path, env=script_env)
journal.send(
f'Pattern found: "{pattern}", script executed: "{script_path}"',
SYSLOG_IDENTIFIER=my_name)
except Exception as err:
journal.send(
f'Pattern found: "{pattern}", failed to execute script "{script_path}": {err}',
SYSLOG_IDENTIFIER=my_name)
# Analyze a message
def process_message(self, message: dict) -> None:
for pattern_compiled, pattern_config in self.config.items():
# Check if syslog id is presented in config and matches
syslog_id = pattern_config.get('syslog_id')
if syslog_id and message['SYSLOG_IDENTIFIER'] != syslog_id:
continue
if pattern_compiled.fullmatch(message['MESSAGE']):
# Add message to environment variables
pattern_config['pattern_script']['environment'][
'message'] = message['MESSAGE']
# Run script
self.script_run(
pattern=pattern_config['pattern_raw'],
script_path=pattern_config['pattern_script']['path'],
script_env=pattern_config['pattern_script']['environment'])
if __name__ == '__main__':
# Parse command arguments and get config
parser = argparse.ArgumentParser()
parser.add_argument('-c',
'--config',
action='store',
help='Path to even-handler configuration',
required=True,
type=Path)
args = parser.parse_args()
try:
config_path = Path(args.config)
config = json.loads(config_path.read_text())
# Create an object for analazyng messages
analyzer = Analyzer(config)
except Exception as err:
print(
f'Configuration file "{config_path}" does not exist or malformed: {err}'
)
exit(1)
# Prepare for proper exitting
signal(SIGTERM, handle_signal)
signal(SIGINT, handle_signal)
# Set up journal connection
data = journal.Reader()
data.seek_tail()
data.get_previous()
p = select.poll()
p.register(data, data.get_events())
journal.send(f'Started with configuration: {config}',
SYSLOG_IDENTIFIER=my_name)
while p.poll():
if data.process() != journal.APPEND:
continue
for entry in data:
message = entry['MESSAGE']
pid = entry['_PID']
# Skip empty messages and messages from this process
if message and pid != my_pid:
try:
analyzer.process_message(entry)
except Exception as err:
journal.send(f'Unable to process message: {err}',
SYSLOG_IDENTIFIER=my_name)
|