File size: 15,980 Bytes
a757bd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
366
367
368
369
370
371
372
373
# coding: utf-8
# -------------------------------------------------------------------
# 宝塔Linux面板
# -------------------------------------------------------------------
# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved.
# -------------------------------------------------------------------
# Author: hwliang <hwl@bt.cn>
# -------------------------------------------------------------------

# ssh信息
# ------------------------------
import json
import os
import re
import time

import public
from safeModel.base import safeBase
from datetime import datetime


class main(safeBase):

    def __init__(self):
        pass

    # 获取当天登陆失败/登陆成功计数
    def __get_today_stats(self):
        today_err_num1 = int(public.ExecShell(
            "journalctl -u ssh --no-pager -S today |grep -a 'Failed password for' |grep -v 'invalid' |wc -l")[0])
        today_err_num2 = int(public.ExecShell(
            "journalctl -u ssh --no-pager -S today |grep -a 'Connection closed by authenticating user' |grep -a 'preauth' |wc -l")[0])
        today_success = int(public.ExecShell("journalctl -u ssh --no-pager -S today |grep -a 'Accepted' |wc -l")[0])
        return today_err_num1 + today_err_num2, today_success

    # 更新ssh统计记录
    def __update_record_with_today_stats(self, record):
        today_err_num, today_success = self.__get_today_stats()
        if record["today_success"] < today_success: record["success"] += today_success
        if record["today_error"] < today_err_num: record["error"] += today_err_num
        record['today_error'] = today_err_num
        record['today_success'] = today_success

    # 获取终端执行命令记录
    def ssh_cmd_history(self, get):
        try:
            result = []
            file_path = "/root/.bash_history"
            data = public.readFile(file_path) if os.path.exists(file_path) else None

            danger_cmd = ['rm', 'rmi', 'kill', 'stop', 'pause', 'unpause', 'restart', 'update', 'exec', 'init',
                          'shutdown', 'reboot', 'chmod', 'chown', 'dd', 'fdisk', 'killall', 'mkfs', 'mkswap', 'mount',
                          'swapoff', 'swapon', 'umount', 'userdel', 'usermod', 'passwd', 'groupadd', 'groupdel',
                          'groupmod', 'chpasswd', 'chage', 'usermod', 'useradd', 'userdel', 'pkill']

            if data:
                data_list = data.split("\n")
                for i in data_list:
                    if len(result) >= 200: break
                    if not i or i.startswith("#"):
                        continue

                    is_dangerous = any(cmd in i for cmd in danger_cmd)

                    result.append({
                        "command": i,
                        "is_dangerous": is_dangerous
                    })
            else:
                result = []

            return public.returnMsg(True, {
                "data": result,
                "total": len(result)
            })
        except:
            return public.returnMsg(False, {
                "data": [],
                "total": 0
            })

    def get_ssh_intrusion(self, get):
        """
        @获取SSH爆破次数
        @param get:
        """
        result = {'error': 0, 'success': 0, 'today_error': 0, 'today_success': 0}

        # debian系统处理
        if os.path.exists("/etc/debian_version"):
            version = public.readFile('/etc/debian_version').strip()
            if 'bookworm' in version or 'jammy' in version or 'impish' in version:
                version = 12
            else:
                try:
                    version = float(version)
                except:
                    version = 11

            if version >= 12:
                try:
                    # # 优先取缓存
                    pkey = "version_12_ssh_login_counts"
                    if public.cache_get(pkey):
                        return public.cache_get(pkey)

                    # 读取记录文件
                    filepath = "/www/server/panel/data/ssh_login_counts.json"
                    filedata = public.readFile(filepath) if os.path.exists(filepath) else public.writeFile(filepath, "[]")
                    today = datetime.now().strftime('%Y-%m-%d')
                    # 解析记录文件的内容
                    try:
                        data_list = json.loads(filedata)
                    except:
                        data_list = []

                    if data_list:
                        for index, record in enumerate(data_list):
                            # 如果记录中有当天的数据,则直接返回
                            if record['date'] == today:
                                self.__update_record_with_today_stats(record)
                                if index == 0:  # 确保只在首次找到匹配项时返回
                                    data_list[0] = record
                                    # 设置缓存
                                    public.cache_set(pkey, record, 30)
                                    return record
                            else:
                                record = data_list[0]
                                self.__update_record_with_today_stats(record)
                                # 设置缓存
                                public.cache_set(pkey, record, 30)
                                return record

                    # 没有记录文件   按原先的方式获取
                    err_num1 = int(public.ExecShell(
                        "journalctl -u ssh --no-pager |grep -a 'Failed password for' |grep -v 'invalid' |wc -l")[0])
                    err_num2 = int(public.ExecShell(
                        "journalctl -u ssh --no-pager --grep='Connection closed by authenticating user|preauth' |wc -l")[0])
                    result['error'] = err_num1 + err_num2
                    result['success'] = int(public.ExecShell("journalctl -u ssh --no-pager|grep -a 'Accepted' |wc -l")[0])

                    today_err_num, today_success = self.__get_today_stats()
                    result['today_error'] = today_err_num
                    result['today_success'] = today_success
                    # 设置缓存
                    public.cache_set(pkey, result, 30)
                except:
                    pass
                return result

        # 记录文件
        ssh_intrusion_file = '/www/server/panel/config/ssh_intrusion.json'
        today = datetime.now().strftime('%Y-%m-%d')
        wf = True
        # 读取文件
        try:
            ssh_intrusion_data = json.loads(public.readFile(ssh_intrusion_file))
            if "time" in ssh_intrusion_data and ssh_intrusion_data['time'] == today:
                wf = False
                result['error'] = ssh_intrusion_data["data"]["error"]
                result['success'] = ssh_intrusion_data["data"]["success"]
        except:
            ssh_intrusion_data = {'time': '', 'data': result}

        logs_path_info = self.get_ssh_log_files_list(None)
        time_formatted = time.strftime('%b  %d', time.localtime())
        month, day = time_formatted.split()
        day = day.lstrip('0')

        formatted_time = "{}  {}".format(month, day)
        formatted_time1 = "{} {} ".format(month, day)

        for sfile in logs_path_info:
            if not os.path.exists(sfile):
                continue

            for stype in result.keys():
                try:
                    if stype in ["error", "success"] and ssh_intrusion_data and ssh_intrusion_data["time"] == today\
                            and ssh_intrusion_data["data"][stype] != 0:
                        continue

                    if stype == 'error':
                        cmds = [
                            "cat {} | grep -a 'Failed password for' | grep -v 'invalid' | awk '{{print $5}}'".format(sfile),
                            "cat {} | grep -a 'Connection closed by authenticating user' | grep -a 'preauth' | awk '{{print $5}}'".format(sfile),
                            "cat {} | grep -a 'PAM service(sshd) ignoring max retries' | awk '{{print $5}}'".format(sfile)
                        ]
                    elif stype == 'success':
                        cmds = [
                            "cat {} | grep -a 'Accepted' | awk '{{print $5}}'".format(sfile),
                            "cat {} | grep -a 'sshd\\[.*session opened for user' | awk '{{print $5}}'".format(sfile)
                        ]
                    elif stype == 'today_error' and sfile in ["/var/log/secure", "/var/log/auth.log"]:
                        cmds = [
                            "cat {} | grep -a 'Failed password for' | grep -v 'invalid' | grep -aE '{}|{}' | awk '{{print $5}}'".format(sfile, formatted_time, formatted_time1),
                            "cat {} | grep -a 'Connection closed by authenticating user' | grep -a 'preauth' | grep -aE '{}|{}' | awk '{{print $5}}'".format(sfile, formatted_time, formatted_time1),
                            "cat {} | grep -a 'PAM service(sshd) ignoring max retries' | grep -aE '{}|{}' | awk '{{print $5}}'".format(sfile, formatted_time, formatted_time1)
                        ]
                    elif stype == 'today_success' and sfile in ["/var/log/secure", "/var/log/auth.log"]:
                        cmds = [
                            "cat {} | grep -a 'Accepted' | grep -aE '{}|{}' | awk '{{print $5}}'".format(sfile, formatted_time, formatted_time1),
                            "cat {} | grep -a 'sshd\\[.*session opened for user' | grep -aE '{}|{}' | awk '{{print $5}}'".format(sfile, formatted_time, formatted_time1)
                        ]
                    else:
                        continue

                    log_entries = []
                    for cmd in cmds:
                        output = public.ExecShell(cmd)[0].strip()
                        if output:
                            log_entries.extend(output.split('\n'))

                        # 去重处理
                        if stype in ["success", "today_success"]:
                            count = len(set(log_entries))
                        else:
                            count = len(log_entries)

                        result[stype] += count

                except Exception as e:
                    continue

            result['success'] = result['today_success'] if result['today_success'] >= result['success'] else result['success'] + result['today_success']
            result['error'] = result['today_error'] if result['today_error'] >= result['error'] else result['error'] + result['today_error']
            # 写入到文件中
            if wf:
                ssh_intrusion_data = {'time': today, 'data': result}
                public.writeFile(ssh_intrusion_file, json.dumps(ssh_intrusion_data))

        return result

    def get_ssh_cache(self):
        """
        @获取换成ssh记录
        """
        file = '{}/data/ssh_cache.json'.format(public.get_panel_path())
        cache_data = {'success': {}, 'error': {}, 'today_success': {}, 'today_error': {}}
        if not os.path.exists(file):
            public.writeFile(file, json.dumps(cache_data))
            return cache_data

        try:
            data = json.loads(public.readFile(file))
        except:
            public.writeFile(file, json.dumps(cache_data))
            data = cache_data

        return data

    def set_ssh_cache(self, data):
        """
        @设置ssh缓存
        """
        file = '{}/data/ssh_cache.json'.format(public.get_panel_path())
        public.writeFile(file, json.dumps(data))
        return True

    def GetSshInfo(self, get):
        """
        @获取SSH登录信息

        """
        port = public.get_sshd_port()
        status = public.get_sshd_status()
        isPing = True
        try:
            file = '/etc/sysctl.conf'
            conf = public.readFile(file)
            rep = r"#*net\.ipv4\.icmp_echo_ignore_all\s*=\s*([0-9]+)"
            tmp = re.search(rep, conf).groups(0)[0]
            if tmp == '1': isPing = False
        except:
            isPing = True

        data = {}
        data['port'] = port
        data['status'] = status
        data['status_text'] = '运行中' if status else '已停止'
        data['ping'] = isPing
        data['firewall_status'] = self.CheckFirewallStatus()
        # data['error'] = self.get_ssh_intrusion(get)
        data['fail2ban'] = self._get_ssh_fail2ban()
        ban_job = public.M('crontab').where("name='BT-SSH爆破IP封禁[安全-SSH管理-登录日志中添加]'", ()).count()
        data['ban_cron_job'] =  ban_job > 0
        return data

    def get_ssh_login_info(self, get):
        """
        @获取SSH登录信息
        """
        return self.get_ssh_intrusion(get)

    @staticmethod
    def _get_ssh_fail2ban():
        """
        @name 获取fail2ban的服务和SSH防爆破状态
        @return:
        """
        plugin_path = "/www/server/panel/plugin/fail2ban"
        result_data = {"status": 0, "installed": 1}
        if not os.path.exists("{}".format(plugin_path)):
            result_data['installed'] = 0
            return result_data

        sock = "{}/fail2ban.sock".format(plugin_path)
        if not os.path.exists(sock):
            return result_data

        s_file = '{}/plugin/fail2ban/config.json'.format(public.get_panel_path())
        if os.path.exists(s_file):
            try:
                data = json.loads(public.readFile(s_file))
                if 'sshd' in data:
                    if data['sshd']['act'] == 'true':
                        result_data['status'] = 1
                        return result_data
            except:
                pass

        return result_data

    # 改远程端口
    def SetSshPort(self, get):
        port = get.port
        if int(port) < 22 or int(port) > 65535: return public.returnMsg(False, 'FIREWALL_SSH_PORT_ERR')
        ports = ['21', '25', '80', '443', '8080', '888', '8888']
        if port in ports: return public.returnMsg(False, '请不要使用常用程序的默认端口!')
        file = '/etc/ssh/sshd_config'
        conf = public.readFile(file)

        rep = r"#*Port\s+([0-9]+)\s*\n"
        conf = re.sub(rep, "Port " + port + "\n", conf)
        public.writeFile(file, conf)

        if self.__isFirewalld:
            public.ExecShell('firewall-cmd --permanent --zone=public --add-port=' + port + '/tcp')
            public.ExecShell('setenforce 0')
            public.ExecShell('sed -i "s#SELINUX=enforcing#SELINUX=disabled#" /etc/selinux/config')
            # public.ExecShell("systemctl restart sshd.service")
        elif self.__isUfw:
            public.ExecShell('ufw allow ' + port + '/tcp')
            # public.ExecShell("service ssh restart")
        else:
            public.ExecShell('iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport ' + port + ' -j ACCEPT')
            # public.ExecShell("/etc/init.d/sshd restart")

        public.set_sshd_status(status_act="restart")

        self.FirewallReload()
        public.M('firewall').where("ps=? or ps=? or port=?", ('SSH远程管理服务', 'SSH远程服务', port)).delete()
        public.M('firewall').add('port,ps,addtime', (port, 'SSH远程服务', time.strftime('%Y-%m-%d %X', time.localtime())))
        public.WriteLog("TYPE_FIREWALL", "FIREWALL_SSH_PORT", (port,))
        return public.returnMsg(True, 'EDIT_SUCCESS')

    def SetSshStatus(self, get):
        """
        @设置SSH状态
        """
        get.exists(["status"])
        if int(get['status']) == 1:
            msg = public.getMsg('FIREWALL_SSH_STOP')
            act = 'stop'
        else:
            msg = public.getMsg('FIREWALL_SSH_START')
            act = 'start'

        public.set_sshd_status(status_act=act)

        public.WriteLog("TYPE_FIREWALL", msg)
        return public.returnMsg(True, 'SUCCESS')