#coding: utf-8 #------------------------------------------------------------------- # 宝塔Linux面板 #------------------------------------------------------------------- # Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved. #------------------------------------------------------------------- # Author: hwliang #------------------------------------------------------------------- # 系统日志 #------------------------------ import datetime import json import os import re import sys import time import public from safeModel.base import safeBase class main(safeBase): def __init__(self): ssh_cache_path = "{}/data/ssh".format(public.get_panel_path()) if not os.path.exists(ssh_cache_path): os.makedirs(ssh_cache_path,384) #*********************************************** start ssh收费模块 ****************************************************** def get_ssh_list(self,get): """ @获取SSH登录 @param get: count :数量 """ select_pl = ['Accepted', 'Failed password for'] if hasattr(get, 'select'): if get.select == "Accepted": select_pl = ['Accepted'] elif get.select == "Failed": select_pl = ['Failed password for'] p = 1 count = 20 if 'count' in get: count = int(get['count']) if 'p' in get: p = int(get['p']) result = [] min,max = (p -1) * count, p * count log_list = self.get_log_byfile(self.get_ssh_log_files(get)[0], min, max, self.__get_search_list(get, select_pl)) for log in log_list: data = self.get_ssh_log_line(log['log'],log['time']) if not data: continue result.append(data) if p < 1000000: public.set_module_logs('ssh_log','get_ssh_list') return public.return_area(result,'address') def get_ssh_error(self,get): """ @获取SSH错误次数 @param get: count :数量 """ p = 1 count = 20 if 'count' in get: count = int(get['count']) if 'p' in get: p = int(get['p']) result = [] min,max = (p -1) * count, p * count log_list = self.get_log_byfile(self.get_ssh_log_files(get)[0],min,max,self.__get_search_list(get,['Failed password for'])) for log in log_list: data = self.get_ssh_log_line(log['log'],log['time']) if not data:continue result.append(data) if p < 1000000: public.set_module_logs('ssh_log','get_ssh_list') return public.return_area(result,'address') def get_ssh_success(self,get): """ @获取SSH登录成功次数 @param get: count :数量 """ p = 1 count = 20 if 'count' in get: count = int(get['count']) if 'p' in get: p = int(get['p']) result = [] min,max = (p -1) * count,p * count log_list = self.get_log_byfile(self.get_ssh_log_files(get)[0],min,max,self.__get_search_list(get,['Accepted'])) for log in log_list: data = self.get_ssh_log_line(log['log'],log['time']) if not data: continue result.append(data) if p < 1000000: public.set_module_logs('ssh_log','get_ssh_list') return public.return_area(result,'address') def __get_search_list(self,get,slist): """ @组合搜索条件 return list 查询组合 status 1 增加查询条件 """ res = [] if 'search' in get and get['search'].strip(): search = get['search'].strip() for info in slist: res.append(info +'&' + search) if len(res) == 0: return search return res return slist def get_ssh_log_line(self,log,log_time): ''' @name 获取ssh日志行 @param log 日志行 @param log_time 前一条记录的日志时间 ''' tmps = log.replace(' ',' ').split(' ') if len(tmps) < 3: return False data = {} data['time'] = log_time if log.find('closed by authenticating user') != -1: data['user'] = tmps[10] data['address'] = tmps[11] data['port'] = tmps[13] else: data['user'] = tmps[8] data['address'] = tmps[10] data['port'] = tmps[12] data['status'] = 0 if log.find('Accepted') >= 0: data['status'] = 1 return data #*********************************************** end ssh收费模块 ****************************************************** def get_curr_log_file(self,filename,search,min,max,log_list): """ @name 获取当前日志文件 @param filename: 日志文件名 @param search: 搜索条件 @param min: 最小值 @param max: 最大值 @param log_list: 匹配的日志列表 """ log_list.clear() limit_max = '| tail -n {}'.format(max) if search[0].find('&') >= 0: limit_max = '' shells = [ "cat {}|grep -a 'Failed password for' |grep -v 'invalid' {} ".format(filename,limit_max), #登录失败 "cat {}|grep -a 'Accepted' {}".format(filename,limit_max), "cat {}|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid' {} ".format(filename,limit_max), "cat {}|grep -a 'Connection closed by authenticating user' |grep -a 'preauth' {} ".format(filename, limit_max) # 登录失败 ] if filename == 'journalctl': shells = [ "journalctl -u ssh --no-pager|grep -a 'Failed password for' |grep -v 'invalid' {} ".format(limit_max), # 登录失败 "journalctl -u ssh --no-pager|grep -a 'Accepted' {}".format(limit_max), "journalctl -u ssh --no-pager|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid' {} ".format( limit_max), "journalctl -u ssh --no-pager|grep -a 'Connection closed by authenticating user' |grep -a 'preauth' {} ".format( limit_max) # 登录失败 ] if len(search) == 1: if search[0].find('Accepted') >= 0: shells = [shells[1]] else: shells = [shells[0],shells[3]] else: shells = [shells[2]] result = [] for shell in shells: res = public.ExecShell(shell)[0].strip().split('\n') res.reverse() for log in res: result.append(log) find_idx = 0 log_time = 0 limit = max - min for log in result: log_time = self.get_log_pre_time(filename,log,log_time) if len(log_list) >= limit: break if self.__find_line_str(log,search): find_idx += 1 if find_idx > min: log = public.xssencode2(log.replace(' ',' ')) log_list.append({'log':log,'time':log_time}) return find_idx def get_sys_datetime(self,pre_time,log_time): """ @name 对比日志时间,日志时间保存年份,日志时间比前一条时间大,则判断为上一年, @param pre_time 上一条日志时间 @param log_time 当前日志时间 @return 当前日志时间(校准年份) """ if type(log_time) == str: log_time = self.__get_to_date(log_time) if type(pre_time) == str: pre_time = self.__get_to_date(pre_time) if (log_time > pre_time and pre_time > 0) or log_time >= time.time(): d = datetime.datetime.strptime(public.format_date(times =log_time), "%Y-%m-%d %H:%M:%S") n_date = self.__get_to_date('{}-{}-{} {}:{}:{}'.format(d.year-1,d.month,d.day,d.hour,d.minute,d.second)) return public.format_date(times=n_date) if type(log_time) == int: return public.format_date(times=log_time) return log_time def get_log_pre_time(self,log_file,_line,pre_time): """ @name 计算上次日志时间 @param log_file 日志文件 @param _line 日志行 @param pre_time 上次日志时间 @auther cjxin """ log_time = 0 if _line[:3] in self._months: log_time = self.to_date4(_line[:16].strip()) elif _line[:2] in ['19','20','21','22']: log_time = _line[:19].strip() elif log_file.find('alternatives') >= 0: _tmp = _line.split(": ") _last = _tmp[0].split(" ") log_time = ' '.join(_last[1:]).strip() log_time = self.get_sys_datetime(pre_time,log_time) return log_time def get_user(self): ''' @name 获取系统用户名 :return: ''' pass_file = public.readFile("/etc/passwd") pass_file = pass_file.split('\n') user_list = [] for p in pass_file: p = p.split(':', 1) user_list.append(p[0]) return user_list def get_log_byfile(self,sfile,min_num,max_num,search = None): """ @name 获取日志文件的日志 @param sfile:日志文件 @param min_num:起始行数 @param max_num:结束行数 @param search:搜索关键字 """ log_list = [] h_find = None #获取归档文件列表 for info in self.get_sys_logfiles(None): if info['log_file'] == sfile: h_find = info break 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: h_find = {'log_file': 'journalctl', "list": [], 'uptime': time.time(), 'title': '授权日志', 'size': 10000} if not h_find: return log_list #获取遍历文件列表 file_list = [h_find['log_file']] for info in h_find['list']: file_list.append(info['log_file']) find_idx = 0 log_time = 0 limit = max_num - min_num user_list = self.get_user() for filename in file_list: #处理最新文件 if filename in ['/var/log/secure', '/var/log/auth.log', 'journalctl'] and search: find_idx = self.get_curr_log_file(filename,search,min_num,max_num,log_list) continue p = 0 #分页计数器 next_file = False sfile = filename if filename[-3:] in ['.gz','.xz']: sfile = sfile[:-3] check_file,is_cache = self.__check_other_search(filename,search) if check_file: cache_path = '{}/data/ssh/{}{}'.format(public.get_panel_path(),os.path.basename(sfile),check_file) if not os.path.exists(cache_path): self.__set_ssh_log(filename,check_file) filename = cache_path #数据不够,则解压归档文件进行查询 if filename[-3:] in ['.gz','.xz']: public.ExecShell("gunzip -c " + filename + " > " + filename[:-3]) filename = filename[:-3] while not next_file: if not os.path.exists(filename): continue # 文件不存在? if len(log_list) >= limit or os.path.getsize(filename) == 0: break p += 1 #public.print_log('读取文件:{},第{}页'.format(filename,p)) #每次读取10000行,不足10000行跳转下个文件 result = self.GetNumLines(filename,10001,p).split("\n") if len(result) < 10000: next_file = True result.reverse() for _line in result: if not _line.strip(): continue log_time = self.get_log_pre_time(filename,_line,log_time) #处理搜索关键词 is_search = False if self.__find_line_str(_line,search): is_search = True #读取数量超过最大值,跳出 if len(log_list) >= limit: break if is_search: find_idx += 1 if find_idx > min_num: _line = public.xssencode2(_line.replace(' ',' ')) # 解决SSH登录日志搜索ip或用户名不准确的情况 if type(search) == list and len(search[0].split("&")) > 1: rep_str = search[0].split("&") rep = "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" re_result = re.search(rep, _line).group() if rep_str[1] in re_result: log_list.append({'log': _line, 'time': log_time}) elif rep_str[1] in user_list: log_list.append({'log': _line, 'time': log_time}) else: log_list.append({'log':_line,'time':log_time}) return log_list def __set_ssh_log(self,filename,check_file): """ @name 缓存SSH登录日志 @param filename 缓存文件 @param check_file 文件类型 """ cache_path = '{}/data/ssh/{}{}'.format(public.get_panel_path(),os.path.basename(filename),check_file) if check_file == '_success': if filename == "journalctl": shell = "journalctl -u ssh --no-pager|grep -a 'Accepted'" else: shell = "cat {}|grep -a 'Accepted'".format(filename) elif check_file == '_error': if filename == "journalctl": shell = "journalctl -u ssh --no-pager|grep -E 'Failed password for|Connection closed by authenticating user' |grep -v 'invalid'" else: shell = "cat {}|grep -E 'Failed password for|Connection closed by authenticating user' |grep -v 'invalid'".format( filename) else: if filename == "journalctl": shell = "journalctl -u ssh --no-pager|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid'" else: shell = "cat {}|grep -E 'Failed password for|Accepted|Connection closed by authenticating user' |grep -v 'invalid'".format(filename) if not os.path.exists(cache_path): res = public.ExecShell(shell)[0] public.writeFile(cache_path,res) return True def __check_other_search(self,filename,search): """ @检测是否需要缓存ssh登录日志 @filename 文件名 @search 搜索关键字 """ if not search: return False,False if filename.find('secure-') >= 0 or filename.find('auth.log.') >= 0: res = search if type(search) == list: res = ' '.join(search) is_cache = False if res.find('&') == -1: is_cache = True if len(search) == 2: return '_all',is_cache if search[0].find('Accepted') >= 0: return '_success',is_cache return '_error',is_cache return False,False def __find_line_str(self,__line,find_str): """ @ 批量搜索文件 @ __line 文件行 @ find_str 搜索关键字 """ if type(find_str) == list: if len(find_str) == 0: return True for search in find_str: if self.__find_str(__line,search.strip()): return True return False else: if find_str: return self.__find_str(__line,find_str.strip()) return True def __find_str(self,_line,find_str): """ @查找关键词 @_line 文件行 @find_str 搜索关键字 """ is_num = 0 slist = find_str.split("&") for search in slist: if search == 'Failed password for': #兼容多个系统的登录失败 if _line.find(search) >= 0 and _line.find('invalid') == -1: is_num += 1 elif _line.find('Connection closed by authenticating user') >= 0 and _line.find('preauth') >= 0: #debian系统使用宝塔SSH终端登录失败 is_num += 1 else: if _line.find(search) >= 0: is_num += 1 if is_num == len(slist): return True return False def get_log_title(self,log_name): ''' @name 获取日志标题 @author hwliang<2021-09-03> @param log_name 日志名称 @return 日志标题 ''' log_name = log_name.replace('.1','') if log_name in ['auth.log','secure'] or log_name.find('auth.') == 0: return 'Authorization log' if log_name in ['dmesg'] or log_name.find('dmesg') == 0: return 'kernel buffer log' if log_name in ['syslog'] or log_name.find('syslog') == 0: return 'System warning/error log' if log_name in ['btmp']: return 'failed login record' if log_name in ['utmp','wtmp']: return 'Logon and restart records' if log_name in ['lastlog']: return 'User last logged in' if log_name in ['yum.log']: return 'yum package manager log' if log_name in ['anaconda.log']: return 'Anaconda log' if log_name in ['dpkg.log']: return 'dpkg package manager log' if log_name in ['daemon.log']: return 'System background daemon log' if log_name in ['boot.log']: return 'Boot oog' if log_name in ['kern.log']: return 'Kern log' if log_name in ['maillog','mail.log']: return 'Mail log' if log_name.find('Xorg') == 0: return 'Xorg log' if log_name in ['cron.log']: return 'Scheduled task log' if log_name in ['alternatives.log']: return 'Update alternate information' if log_name in ['debug']: return 'Debug log' if log_name.find('apt') == 0: return 'apt-get related logs' if log_name.find('installer') == 0: return 'System installation related logs' if log_name in ['messages']: return 'Comprehensive log' return '{} log'.format(log_name.split('.')[0]) def get_history_filename(self,filepath): ''' @name 获取归档文件名称 @filepath 文件路径 ''' log_name = os.path.basename(filepath) #归档压缩文件,auth.log.1.gz if filepath[-3:] in ['.gz','.xz']: log_file = filepath[:-3] if os.path.exists(log_file): return False log_name = os.path.basename(log_file) #处理auth.log-20221024 if re.search('-(\d{8})',log_name): arrs = log_name.split('-') arrs = arrs[0 : len(arrs)-1] return '-'.join(arrs) #处理auth.log.1 if re.search('.\d{1,10}$',log_name): arrs = log_name.split('.') arrs = arrs[0 : len(arrs)-1] return '.'.join(arrs) return log_name def get_sys_logfiles(self,get): ''' @name 获取系统日志文件列表 @author hwliang<2021-09-02> @param get @return list ''' res = {} log_dir = '/var/log' for log_file in os.listdir(log_dir): if log_file in ['.','..','faillog','fontconfig.log','unattended-upgrades','tallylog']: continue filename = os.path.join(log_dir,log_file) if os.path.isfile(filename): #归档文件原名 log_name = self.get_history_filename(filename) if not log_name: continue if not log_name in res: filepath = os.path.join(log_dir,log_name) if not os.path.exists(filepath): continue res[log_name] = { 'name':log_name, 'log_file':filepath, 'size':os.path.getsize(filepath), 'title': self.get_log_title(log_name), 'uptime': os.path.getmtime(filepath), 'list':[] } if log_name != log_file: res[log_name]['list'].append({ 'name':log_file, 'size':os.path.getsize(filename), 'uptime': os.path.getmtime(filename), 'log_file':filename }) else: for next_name in os.listdir(filename): next_file = os.path.join(filename,next_name) if not os.path.isfile(next_file): continue log_name = self.get_history_filename(next_file) if not log_name: continue if not log_name in res: filepath = os.path.join(filename,log_name) if not os.path.exists(filepath): continue res[log_name] = { 'name':log_name, 'log_file':filepath, 'size':os.path.getsize(filepath), 'title': self.get_log_title(log_name), 'uptime': os.path.getmtime(filepath), 'list':[] } if log_name != next_name: res[log_name]['list'].append({ 'name':next_name, 'size':os.path.getsize(next_file), 'uptime': os.path.getmtime(next_file), 'log_file':next_file }) log_files = [] for key in res: res[key]['list'] = sorted(res[key]['list'],key=lambda x:x['name'],reverse=True) log_files.append(res[key]) log_files = sorted(log_files,key=lambda x:x['name'],reverse=True) return log_files def get_lastlog(self,get): ''' @name 获取lastlog日志 @author hwliang<2021-09-02> @param get @return list ''' cmd = '''LANG=en_US.UTF-8 lastlog|grep -v Username''' result = public.ExecShell(cmd) lastlog_list = [] p = 1 count = 20 if 'count' in get: count = int(get['count']) if 'p' in get: p = int(get['p']) search = '' if 'search' in get: search = get.search idx = 0 min,max = (p -1) * count, p * count for _line in result[0].split("\n"): if not _line: continue if search and _line.find(search) == -1: continue _line = public.xssencode2(_line) tmp = {} sp_arr = _line.split() tmp['User'] = sp_arr[0] # tmp['_line'] = _line if _line.find('Never logged in') != -1: tmp['last login time'] = '0' tmp['Last login source'] = '-' tmp['Last login port'] = '-' else: tmp['last login time'] = sp_arr[2] tmp['Last login source'] = sp_arr[1] tmp['Last login port'] = self.to_date2(' '.join(sp_arr[3:])) if idx >= min and idx < max: lastlog_list.append(tmp) idx += 1 lastlog_list = sorted(lastlog_list,key=lambda x:x['Last login port'],reverse=True) for i in range(len(lastlog_list)): if lastlog_list[i]['Last login port'] == '0': lastlog_list[i]['Last login port'] = 'never logged in' return lastlog_list def get_last(self,get): ''' @name 获取用户会话日志 @author hwliang<2021-09-02> @param get @return list ''' cmd = '''LANG=en_US.UTF-8 last -n 1000 -x -f {}|grep -v 127.0.0.1|grep -v " begins"'''.format(get.log_name) result = public.ExecShell(cmd) lastlog_list = [] search = '' if 'search' in get: search = get.search p = 1 count = 20 if 'count' in get: count = int(get['count']) if 'p' in get: p = int(get['p']) idx = 0 min,max = (p -1) * count, p * count for _line in result[0].split("\n"): if not _line: continue if search and _line.find(search) == -1: continue _line = public.xssencode2(_line) tmp = {} sp_arr = _line.split() tmp['User'] = sp_arr[0] if sp_arr[0] == 'runlevel': tmp['Source'] = sp_arr[4] tmp['Port'] = ' '.join(sp_arr[1:4]) tmp['Time'] = self.to_date3(' '.join(sp_arr[5:])) + ' ' +' '.join(sp_arr[-2:]) elif sp_arr[0] in ['reboot','shutdown']: tmp['Source'] = sp_arr[3] tmp['Port'] = ' '.join(sp_arr[1:3]) if sp_arr[-3] == '-': tmp['Time'] = self.to_date3(' '.join(sp_arr[4:])) + ' ' +' '.join(sp_arr[-3:]) else: tmp['Time'] = self.to_date3(' '.join(sp_arr[4:])) + ' ' +' '.join(sp_arr[-2:]) elif sp_arr[1] in ['tty1','tty','tty2','tty3','hvc0','hvc1','hvc2'] or len(sp_arr) == 9: tmp['Source'] = '' tmp['Port'] = sp_arr[1] tmp['Time'] = self.to_date3(' '.join(sp_arr[2:])) + ' ' +' '.join(sp_arr[-3:]) else: tmp['Source'] = sp_arr[2] tmp['Port'] = sp_arr[1] tmp['Time'] = self.to_date3(' '.join(sp_arr[3:])) + ' ' +' '.join(sp_arr[-3:]) if idx >= min and idx < max: lastlog_list.append(tmp) idx += 1 # lastlog_list = sorted(lastlog_list,key=lambda x:x['时间'],reverse=True) return lastlog_list def __get_to_date(self,times): """ 日期转时间戳 """ try: return int(time.mktime(time.strptime(times, "%Y-%m-%d %H:%M:%S"))) except: try: return int(time.mktime(time.strptime(times, "%Y/%m/%d %H:%M:%S"))) except: return 0 def get_sys_log(self,get): ''' @name 获取指定系统日志 @author hwliang<2021-09-02> @param get @return list ''' log_file = get.log_name p,limit,search = 1,5,'' if 'p' in get: p = int(get.p) if 'limit' in get: limit = int(get.limit) if 'search' in get: search = get.search sfile_name = os.path.basename(get.log_name) if sfile_name in ['wtmp','btmp','utmp'] : return self.get_last(get) if sfile_name in ['lastlog']: return self.get_lastlog(get) if get.log_name.find('sa/sa') >= 0: if get.log_name.find('sa/sar') == -1: return public.xssencode2(public.ExecShell("sar -f /var/log/{}".format(get.log_name))[0]) is_string = True result = [] min,max = (p-1) * limit , p * limit #最小值,最大值 log_list = self.get_log_byfile(log_file,min,max,search) for info in log_list: _line = info['log'] if _line[:3] in self._months: _tmps = _line.split(' ') _msg = ' '.join(_tmps[3:]) _tmp = _msg.split(": ") _act = '' if len(_tmp) > 1: _act = _tmp[0] _msg = _tmp[1] else: _msg = _tmp[0] _line = { "Time": info['time'], "Role":_act, "Even":_msg } is_string = False elif _line[:2] in ['19','20','21','22']: _msg = _line[19:] _tmp = _msg.split(" ") _act = _tmp[1] _msg = ' '.join(_tmp[2:]) _line = { "Time":info['time'], "Role":_act, "Even":_msg } is_string = False elif log_file.find('alternatives') >= 0: _tmp = _line.split(": ") _last = _tmp[0].split(" ") _act = _last[0] _msg = ' '.join(_tmp[1:]) _line = { "Time":info['time'], "Role":_act, "Even":_msg } is_string = False else: if not is_string: if type(_line) != dict: continue result.append(_line) public.set_module_logs('sys_log','get_sys_log') try: _string = [] _dict = [] _list = [] for _line in result: if isinstance(_line,str): _string.append(_line.strip()) elif isinstance(_line,dict): _dict.append(_line) elif isinstance(_line,list): _list.append(_line) else: continue _str_len = len(_string) _dict_len = len(_dict) _list_len = len(_list) if _str_len >= _dict_len + _list_len: return _string elif _dict_len >= _str_len + _list_len: return _dict else: return _list except: return '\n'.join(result) #取文件指定尾行数 def GetNumLines(self,path,num,p=1): pyVersion = sys.version_info[0] max_len = 1024*1024 * 2 try: from cgi import html if not os.path.exists(path): return "" start_line = (p - 1) * num count = start_line + num fp = open(path,'rb') buf = "" fp.seek(-1, 2) if fp.read(1) == "\n": fp.seek(-1, 2) data = [] total_len = 0 b = True n = 0 for i in range(count): while True: newline_pos = str.rfind(str(buf), "\n") pos = fp.tell() if newline_pos != -1: if n >= start_line: line = buf[newline_pos + 1:] line_len = len(line) total_len += line_len sp_len = total_len - max_len if sp_len > 0: line = line[sp_len:] try: data.insert(0,line) except: pass buf = buf[:newline_pos] n += 1 break else: if pos == 0: b = False break to_read = min(4096, pos) fp.seek(-to_read, 1) t_buf = fp.read(to_read) if pyVersion == 3: t_buf = t_buf.decode('utf-8') buf = t_buf + buf fp.seek(-to_read, 1) if pos - to_read == 0: buf = "\n" + buf if total_len >= max_len: break if not b: break fp.close() result = "\n".join(data) if not result: raise Exception('null') except: result = public.ExecShell("tail -n {} {}".format(num,path))[0] if len(result) > max_len: result = result[-max_len:] try: try: result = json.dumps(result) return json.loads(result).strip() except: if pyVersion == 2: result = result.decode('utf8',errors='ignore') else: result = result.encode('utf-8',errors='ignore').decode("utf-8",errors="ignore") return result.strip() except: return ""