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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
|
import textwrap
import time
import traceback
from itertools import chain
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from html2text import HTML2Text
from common.utils import lazyproperty
from common.utils.timezone import local_now
from notifications.backends import BACKEND
from settings.utils import get_login_title
from terminal.const import RiskLevelChoices
from users.models import User
from .models import SystemMsgSubscription, UserMsgSubscription
__all__ = ('SystemMessage', 'UserMessage', 'system_msgs', 'Message')
system_msgs = []
user_msgs = []
class MessageType(type):
def __new__(cls, name, bases, attrs: dict):
clz = type.__new__(cls, name, bases, attrs)
if 'message_type_label' in attrs \
and 'category' in attrs \
and 'category_label' in attrs:
message_type = clz.get_message_type()
msg = {
'message_type': message_type,
'message_type_label': attrs['message_type_label'],
'category': attrs['category'],
'category_label': attrs['category_label'],
}
if issubclass(clz, SystemMessage):
system_msgs.append(msg)
elif issubclass(clz, UserMessage):
user_msgs.append(msg)
return clz
@shared_task(
verbose_name=_('Publish the station message'),
description=_(
"""This task needs to be executed for sending internal messages for system alerts,
work orders, and other notifications"""
)
)
def publish_task(receive_user_ids, backends_msg_mapper):
Message.send_msg(receive_user_ids, backends_msg_mapper)
def send_dingtalk_task(message):
# Send to custom webhook
import hmac, hashlib, base64, urllib.parse
timestamp = str(round(time.time() * 1000))
# JumpServer credentials
access_token = '<access token>'
secret = '<secret>'
secret_enc = secret.encode('utf-8')
string_to_sign = f'{timestamp}\n{secret}'
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
form_data = {
"msgtype": "markdown",
"markdown": {
"title": "JumpServer Monitoring Alert",
"text": message
},
"at": {
"atMobiles": [
"<mobile>"
],
"isAtAll": False
}
}
import requests
try:
res = requests.post(
url=f'https://oapi.dingtalk.com/robot/send?access_token={access_token}×tamp={timestamp}&sign={sign}',
json=form_data)
except Exception as e:
with open('/opt/error.log', 'a+') as f:
f.write(str(e))
class Message(metaclass=MessageType):
"""
What's encapsulated here?
Templates for different messages with a unified sending interface
- publish: Implementation relates to message subscription table structure
- send_msg
"""
message_type_label: str
category: str
category_label: str
text_msg_ignore_links = True
command = None
@classmethod
def get_message_type(cls):
return cls.__name__
def publish_async(self):
self.publish(is_async=True)
@classmethod
def gen_test_msg(cls):
raise NotImplementedError
def publish(self, is_async=False):
raise NotImplementedError
def get_backend_msg_mapper(self, backends):
backends = set(backends)
backends.add(BACKEND.SITE_MSG) # Site message is mandatory
backends_msg_mapper = {}
for backend in backends:
backend = BACKEND(backend)
if not backend.is_enable:
continue
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
msg = get_msg_method()
backends_msg_mapper[backend] = msg
return backends_msg_mapper
@staticmethod
def send_msg(receive_user_ids, backends_msg_mapper):
for backend, msg in backends_msg_mapper.items():
try:
backend = BACKEND(backend)
client = backend.client()
users = User.objects.filter(id__in=receive_user_ids).all()
client.send_msg(users, **msg)
except NotImplementedError:
continue
except:
traceback.print_exc()
@classmethod
def send_test_msg(cls, ding=True, wecom=False):
msg = cls.gen_test_msg()
if not msg:
return
from users.models import User
users = User.objects.filter(username='admin')
backends = []
if ding:
backends.append(BACKEND.DINGTALK)
if wecom:
backends.append(BACKEND.WECOM)
msg.send_msg(users, backends)
@staticmethod
def get_common_msg() -> dict:
return {'subject': '', 'message': ''}
def get_html_msg(self) -> dict:
return self.get_common_msg()
@staticmethod
def html_to_markdown(html_msg):
h = HTML2Text()
h.body_width = 0
content = html_msg['message']
html_msg['message'] = h.handle(content)
return html_msg
def get_markdown_msg(self) -> dict:
return self.html_to_markdown(self.get_html_msg())
def get_text_msg(self) -> dict:
h = HTML2Text()
h.body_width = 90
msg = self.get_html_msg()
content = msg['message']
h.ignore_links = self.text_msg_ignore_links
msg['message'] = h.handle(content)
return msg
@lazyproperty
def common_msg(self) -> dict:
return self.get_common_msg()
@lazyproperty
def text_msg(self) -> dict:
msg = self.get_text_msg()
return msg
@lazyproperty
def markdown_msg(self):
return self.get_markdown_msg()
@lazyproperty
def get_jumpserver_dingtalk_msg(self) -> str:
date_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
msg = f"""## <font color="#FF0000">[JumpServer Monitoring Alert](https://jumpserver.example.com/)</font>π₯
### <font color="#FF0000">Alert Status</font>: {RiskLevelChoices.get_label(self.command['risk_level'])}
### <font color="#FF0000">Target Host</font>: {self.command['asset']}
### <font color="#FF0000">Target User</font>: {self.command['user']}
### <font color="#FF0000">Executed Command</font>: `{self.command['input']}`
### <font color="#FF0000">Alert Details</font>: User {self.command['user']} executed a risky command `{self.command['input']}` on {self.command['asset']}, please handle promptly!
### <font color="#FF0000">Trigger Time</font>: {date_str}"""
return msg
@lazyproperty
def html_msg(self) -> dict:
msg = self.get_html_msg()
return msg
@lazyproperty
def html_msg_with_sign(self):
msg = self.get_html_msg()
msg['message'] = textwrap.dedent("""
{}
<small>
<br />
β
<br />
{}
</small>
""").format(msg['message'], self.signature)
return msg
@lazyproperty
def text_msg_with_sign(self):
msg = self.get_text_msg()
msg['message'] = textwrap.dedent("""
{}
β
{}
""").format(msg['message'], self.signature)
return msg
@lazyproperty
def signature(self):
return get_login_title()
# --------------------------------------------------------------
# Support different messaging formats
def get_dingtalk_msg(self) -> dict:
# DingTalk restricts identical messages within a day, add timestamp suffix
message = self.markdown_msg['message']
time = local_now().strftime('%Y-%m-%d %H:%M:%S')
suffix = '\n{}: {}'.format(_('Time'), time)
return {
'subject': self.markdown_msg['subject'],
'message': message + suffix
}
def get_wecom_msg(self) -> dict:
return self.markdown_msg
def get_feishu_msg(self) -> dict:
return self.markdown_msg
def get_lark_msg(self) -> dict:
return self.markdown_msg
def get_email_msg(self) -> dict:
return self.html_msg_with_sign
def get_site_msg_msg(self) -> dict:
return self.html_msg
def get_slack_msg(self) -> dict:
return self.markdown_msg
def get_sms_msg(self) -> dict:
return self.text_msg_with_sign
@classmethod
def get_all_sub_messages(cls):
def get_subclasses(cls):
"""Returns all subclasses of argument, cls"""
if issubclass(cls, type):
subclasses = cls.__subclasses__(cls)
else:
subclasses = cls.__subclasses__()
for subclass in subclasses:
subclasses.extend(get_subclasses(subclass))
return subclasses
messages_cls = get_subclasses(cls)
return messages_cls
@classmethod
def test_all_messages(cls, ding=True, wecom=False):
messages_cls = cls.get_all_sub_messages()
for _cls in messages_cls:
try:
_cls.send_test_msg(ding=ding, wecom=wecom)
except NotImplementedError:
continue
class SystemMessage(Message):
def publish(self, is_async=False):
subscription = SystemMsgSubscription.objects.get(
message_type=self.get_message_type()
)
# Only send through enabled backends
receive_backends = subscription.receive_backends
receive_backends = BACKEND.filter_enable_backends(receive_backends)
users = [
*subscription.users.all(),
*chain(*[g.users.all() for g in subscription.groups.all()])
]
receive_user_ids = [u.id for u in users]
backends_msg_mapper = self.get_backend_msg_mapper(receive_backends)
if is_async:
send_dingtalk_task(self.get_jumpserver_dingtalk_msg)
# publish_task.delay(receive_user_ids, backends_msg_mapper)
else:
self.send_msg(receive_user_ids, backends_msg_mapper)
@classmethod
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
pass
@classmethod
def gen_test_msg(cls):
raise NotImplementedError
class UserMessage(Message):
user: User
def __init__(self, user):
self.user = user
def publish(self, is_async=False):
"""
Send messages through user-configured channels
"""
sub = UserMsgSubscription.objects.get(user=self.user)
backends_msg_mapper = self.get_backend_msg_mapper(sub.receive_backends)
receive_user_ids = [self.user.id]
if is_async:
send_dingtalk_task(self.get_jumpserver_dingtalk_msg)
# publish_task.delay(receive_user_ids, backends_msg_mapper)
else:
self.send_msg(receive_user_ids, backends_msg_mapper)
@classmethod
def get_test_user(cls):
from users.models import User
return User.objects.all().first()
@classmethod
def gen_test_msg(cls):
raise NotImplementedError
|