Initial YakPanel commit

This commit is contained in:
Niranjan
2026-04-07 02:04:22 +05:30
commit 2826d3e7f3
5359 changed files with 1390724 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
#coding: utf-8
import public,re,time,sys,os
from datetime import datetime
class safeBase:
__isUfw = False
__isFirewalld = False
_months = {'Jan':'01','Feb':'02','Mar':'03','Apr':'04','May':'05','Jun':'06','Jul':'07','Aug':'08','Sep':'09','Sept':'09','Oct':'10','Nov':'11','Dec':'12'}
def __init__(self):
if os.path.exists('/usr/sbin/firewalld'): self.__isFirewalld = True
if os.path.exists('/usr/sbin/ufw'): self.__isUfw = True
#转换时间格式
def to_date(self,date_str):
tmp = re.split(r'\s+',date_str)
if len(tmp) < 3: return date_str
s_date = str(datetime.now().year) + '-' + self._months.get(tmp[0]) + '-' + tmp[1] + ' ' + tmp[2]
time_array = time.strptime(s_date, "%Y-%m-%d %H:%M:%S")
time_stamp = int(time.mktime(time_array))
return time_stamp
def to_date2(self,date_str):
tmp = date_str.split()
if len(tmp) < 4: return date_str
s_date = str(tmp[-1]) + '-' + self._months.get(tmp[1],tmp[1]) + '-' + tmp[2] + ' ' + tmp[3]
return s_date
def to_date3(self,date_str):
tmp = date_str.split()
if len(tmp) < 4: return date_str
s_date = str(datetime.now().year) + '-' + self._months.get(tmp[1],tmp[1]) + '-' + tmp[2] + ' ' + tmp[3]
return s_date
def to_date4(self,date_str):
tmp = date_str.split()
if len(tmp) < 3: return date_str
s_date = str(datetime.now().year) + '-' + self._months.get(tmp[0],tmp[0]) + '-' + tmp[1] + ' ' + tmp[2]
return s_date
#取防火墙状态
def CheckFirewallStatus(self):
if self.__isUfw:
res = public.ExecShell('ufw status verbose')[0]
if res.find('inactive') != -1: return False
return True
if self.__isFirewalld:
res = public.ExecShell("systemctl status firewalld")[0]
if res.find('active (running)') != -1: return True
if res.find('disabled') != -1: return False
if res.find('inactive (dead)') != -1: return False
else:
res = public.ExecShell("/etc/init.d/iptables status")[0]
if res.find('not running') != -1: return False
return True
return False
def get_ssh_log_files(self,get):
"""
获取ssh日志文件
"""
s_key = 'secure'
if not os.path.exists('/var/log/secure'):
s_key = 'auth.log'
if os.path.exists('/var/log/secure') and os.path.getsize('/var/log/secure') == 0:
s_key = 'auth.log'
res = []
spath = '/var/log/'
for fname in os.listdir(spath):
fpath = '{}{}'.format(spath,fname)
if fname.find(s_key) == -1 or fname == s_key:
continue
#debian解压日志
if fname[-3:] in ['.gz','.xz']:
if os.path.exists(fpath[:-3]):
continue
public.ExecShell("gunzip -c " + fpath + " > " + fpath[:-3])
res.append(fpath[:-3])
else:
res.append(fpath)
res = sorted(res,reverse=True)
res.insert(0,spath + s_key)
return res
def get_ssh_log_files_list(self,get):
"""
获取ssh日志文件
"""
s_key = 'secure'
if not os.path.exists('/var/log/secure'):
s_key = 'auth.log'
if os.path.exists('/var/log/secure') and os.path.getsize('/var/log/secure') == 0:
s_key = 'auth.log'
res = []
spath = '/var/log/'
for fname in os.listdir(spath):
fpath = '{}{}'.format(spath,fname)
if fname.find(s_key) == -1 or fname == s_key:
continue
#debian解压日志
if fname[-3:] in ['.gz','.xz']:
continue
if os.path.getsize(fpath) > 1024 * 1024 * 100:
continue
#判断文件数量为15个
if len(res) > 15:
break
res.append(fpath)
res = sorted(res,reverse=True)
res.insert(0,spath + s_key)
return res

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
#coding: utf-8
#-------------------------------------------------------------------
# YakPanel
#-------------------------------------------------------------------
# Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
#-------------------------------------------------------------------
# Author: cjxin <cjxin@yakpanel.com>
#-------------------------------------------------------------------
# 免费IP库
#------------------------------
import os,re,json,time
from safeModelV2.base import safeBase
import public
class main(safeBase):
_sfile = '{}/data/free_ip_area.json'.format(public.get_panel_path())
def __init__(self):
try:
self.user_info = public.get_user_info()
except:
self.user_info = None
def get_ip_area(self,get):
"""
@获取IP地址所在地
@param get: dict/array
"""
ips = get['ips']
arrs,result = [],{}
for ip in ips:arrs.append(ip)
if len(arrs) > 0:
data = self.__get_cloud_ip_info(arrs)
for ip in data:
result[ip] = data[ip]
return result
def __get_cloud_ip_info(self,ips):
"""
@获取IP地址所在地
@得判断是否是我们的用户
@param ips:
"""
result = {}
if public.is_self_hosted():
for ip in ips:
result[ip] = {'info': 'Unknown IP'}
return result
try:
'''
@从云端获取IP地址所在地
@param data 是否是YakPanel 用户,如果不是则不返回
@param ips: IP地址
'''
data = {}
data['ip'] = ','.join(ips)
data['uid'] = self.user_info['uid']
# 与面板字段差异
data["serverid"]=self.user_info["server_id"]
#如果不是我们的用户,那么不返回数据
res = public.httpPost('https://wafapi2.yakpanel.com/api/ip/info',data)
res = json.loads(res)
data = self.get_ip_area_cache()
for key in res:
info = res[key]
if public.is_local_ip(key):
res[key]['city']="Intranet"
if not res[key]['city']: continue
if not res[key]['city'].strip() and not res[key]['continent'].strip():
info = {'info':'Unknown IP'}
else:
info['info'] = '{} {} {} {}'.format(info['carrier'],info['country'],info['province'],info['city']).strip()
data[key] = info
result[key] = info
self.set_ip_area_cache(data)
except:
pass
return result
def get_ip_area_cache(self):
"""
@获取IP地址所在地
@param get:
"""
data = {}
try:
data = json.loads(public.readFile(self._sfile))
except:
public.writeFile(self._sfile,json.dumps({}))
return data
def set_ip_area_cache(self,data):
"""
@设置IP地址所在地
@param data:
"""
public.writeFile(self._sfile,json.dumps(data))
return True

View File

@@ -0,0 +1,119 @@
QRASP55VO/1DQ98p1csw9A==
I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M=
dBZyCsfrbwqvA0sbdGrIGg==
I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M=
n+0ptngHIPIjFuMNQ53bfj+Na/fdhk6k1yTpAwW7j353Dw920mEqQQZjykAHeRmp0ZD/P3ftGifsmPOMf2b7XdEqyZH0yl9kjaUugj3dYPI=
I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M=
jh3cxzkA2htccqfZRKAUdI8r17q57nOGP4OxbJlL1NAnrF4weHnS0MpT6C6jbrX5
I8MGJUwtjfcKc5w4E0SmjHtl5KRuv0WI7NyW3SlEwCrZmcmaREiC99KS4CzwW5Su330khbLdQaeuAWr4x/NqQCTep2zIARzdXiKXPh1Fe+M=
1u+XjG/2+GSQRv6EzCaWRQ==
rUv3hasppUA6pOugYcQZyA==
I8MGJUwtjfcKc5w4E0SmjHwLsErBQ84ek459TV1n0Iir/P4mdpfwDI34s6+8CBN0
iEprTHe70MI/8Rhzd8EK+QnVQn6aZyZ8dBODiF5pySg=
P1nWGQOfbECkszATyvUnYMoMB+zOtupYIF89n5X+cOkvqsM/qxPIR+8JRAch8USo
XIfdJ79nObMM+vyAmKbTmw==
1u+XjG/2+GSQRv6EzCaWRQ==
1u+XjG/2+GSQRv6EzCaWRQ==
hbDorme8BqzUEmeXCAWmYa52umm3PBCcfSvATzTG02w=
1u+XjG/2+GSQRv6EzCaWRQ==
giysigZ4bhExZB4emZkX3a9fe6PGGMNE9ZZnk7xPqW8QW2Ij1yNFf5XEj5pPPlMwdhA6dIcO2ZX23qJszvnq1fcl6NckihyZ+Uf83rbYjeQ=
9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U=
b4OJVZe8QyIpjuTpKXDL9A==
32pdC9DD05OE2l0oXazDFI+hlwIzzkFTo7tZH3axSGoKNgh7EevAzaB+FO2ZLVS/maKOtFAjIeKErPBj7dsfmg==
mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU=
32pdC9DD05OE2l0oXazDFOQO8mg+tgs322u5NSxm1hnE+dYVd/hkfCsat3ixeRAk
1u+XjG/2+GSQRv6EzCaWRQ==
Fq/4dKD9ssEKrNEHrSuygxu04P7BPF8PboiRMtr4FMM=
Z8UsPk1Q7HtwjRd4g01ryw==
KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD
ho/Q+jrWDtBeg9J9ZTnKi7MMSdcDWkuDSRbkOa0slC0=
Z8UsPk1Q7HtwjRd4g01ryw==
1aJ5h9ef6qScYRSEXHxMz/JV+hrqnP7g6CgzmGbTA34=
CH5utP+NORdjI2nqATw4gJ30bQaw4oV4TkWtZlCiO9A=
lbIj6ug3LX3xS019kmbRSTcfm4XASPCYnVO8MD2z14s=
iZaIsa48RXfE+uf/yF/rQD9SK8CJ49+yPAMIKmMZPD4=
lOh2GtzHjjMM8E9J40AuOc+/vc1yhUL+xJx/Mivlb25pg5HBJ1HJ91Rfq30bJq9S
UbJuac0dxLiN+5ocS3w2vRp92UK1M7Voei4ApHZyTgY=
VmVrGQo2zRokW/ZuO9bN69BDpCzHoJtTjTgNlAOe5sUQUaSnZ1Z1hl5M9Ym+7tCG
VmVrGQo2zRokW/ZuO9bN61V3/TpT/zrd2QvdUtMgGKRq8f0CD0RNnZ4rTqSInsSj
VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY=
VmVrGQo2zRokW/ZuO9bN657tYrblt+z3q06zgiQZcYA=
VmVrGQo2zRokW/ZuO9bN66aFKP1IdEjWKaMooH0pyYReSxgrF6gfc+R+CrZAggrY
sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q=
VmVrGQo2zRokW/ZuO9bN69KBqHrgU1XKz6C1sxuKNo2/9c/EcmFt/gfo+iaOu2+K
1u+XjG/2+GSQRv6EzCaWRQ==
n+FHKmWLbioNpC38yMj4WmiXmXqyLuzKUmIAM1Ft5eM=
NhnIR3Ilo4H2su9/cTNo/MME7jTfPzQcknP67wyItNzcCacoNjdvCuiW0x8udsSO/edftRARPJ4xwm1wQrVT2A==
wlLHv6kT3Q/RmtMBN4nDATwL/9Oa0sOvSxwVwQfq99U=
VmVrGQo2zRokW/ZuO9bN68Jg91voF9Ce3nf2o3e+jkofqu9SSuRXcnpG7smrmLLo
ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8=
1u+XjG/2+GSQRv6EzCaWRQ==
6d8NLnHX3WuS3g79bJvyhMRKs67DB9ZOIiBDrB02YSQctSNS1aqQlPvqVprQ2WDG
Z8UsPk1Q7HtwjRd4g01ryw==
fXZfnC6cxxgiN+onu+xJ1wUhirDdf4kByrL5vhQHFnCYKlzy/eVgdjzy55WiEFTz
LikBEoRjt8wwWzUZNw+jJgsad0LKWFNZK+YO+SfR6UU=
Z8UsPk1Q7HtwjRd4g01ryw==
1u+XjG/2+GSQRv6EzCaWRQ==
W3A5Dt+8AxWSKsTYeXZoL+tHA8UsZgQuAUkDUOPJ9p9irMcLKjcsuwS2q6lHXAKR
L0eUthVnpkGsmKFAX6d+uOlJx8vHVMiotg8sk086vHQ=
TQRmC0vOvJ1P+lfyS3Os4X1xcTKiQ06lXIJEwRwWbq4ieECEmB4BmDhbitv1CsLg
L0eUthVnpkGsmKFAX6d+uLqKlmsh+FpVhvWy0B6mjhM=
1u+XjG/2+GSQRv6EzCaWRQ==
YabJCT93a3/IohXaIkr3oS6/BpCE446GJgDFFvo/yjjYAG9E6229ssUIWmf519v1
hBWY30cr8RshUe3F5HK1cRZ9KmhsMz0lNR6pLxg9eoM=
L0eUthVnpkGsmKFAX6d+uFd45MXPp4WlUdlXMx7fhE8=
VMfVrQporTLzVAs+KagENs0jVofd/mPyGKFNcwoEK/8=
1u+XjG/2+GSQRv6EzCaWRQ==
1u+XjG/2+GSQRv6EzCaWRQ==
avrdBvQ7h/91pEC4PcvqMSR2TJ0t+8EMPSsnARb+XVHHkBrQO1HsCg6QcbTZvZ2W
Z8UsPk1Q7HtwjRd4g01ryw==
KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD
LikBEoRjt8wwWzUZNw+jJpxJaQXGUN8g/Hr0khQ9A1Q=
Z8UsPk1Q7HtwjRd4g01ryw==
/0ULpLqgTvInFD0r5hHANoosZ+xywkOR5dozStfmlYk=
b4OJVZe8QyIpjuTpKXDL9A==
1u+XjG/2+GSQRv6EzCaWRQ==
NhnIR3Ilo4H2su9/cTNo/F9IByyW2CHxX2+3PSbgZmE=
NhnIR3Ilo4H2su9/cTNo/JEWIdEQUX/MlMcOaFZ8tGgjCfBcJNHhmedart7PDRhF
NhnIR3Ilo4H2su9/cTNo/E4e/PRk3DEtgyjsUDeOpSOwGpZehbHYaLN6kP4SrhN+PFbXCI6E8Z+8865ULB7IOw==
LVgN8QZ1tkuTtU9+mDfpHbABBExfuPmmfo3E06+A/BMU5jZqv2ruVk1zj4XyVDv6
NhnIR3Ilo4H2su9/cTNo/C/mkLay+Rx5WJjwGcYV7pSktyrOzy78W2NcadJ49lO1TawFf1/X1sBVYqIElsTA2w==
lOh2GtzHjjMM8E9J40AuOVVeyoh6rSy98QMdDBotkjUYMcj06/T1f5ZVZDqV5ETNirJLe7KoAXCQK3c8Qho2koBEtVenSlmCeJkB2oKSLrLmKk6D8i/XBHEv4KZZOLgp
lOh2GtzHjjMM8E9J40AuOeMCiZGQsphriaBgvIdFiyOx9f7gw874ih7YoSVMDwrD
1u+XjG/2+GSQRv6EzCaWRQ==
NhnIR3Ilo4H2su9/cTNo/A5SzFv4xHkReni2nLCW9wf/+pP3k795kKoMZAQXwXD6
wlLHv6kT3Q/RmtMBN4nDASCRBoPIX3WHiVt75UTjznI=
VmVrGQo2zRokW/ZuO9bN63WVsNPEwwprYYkAUqx5H2WQM74qCCEzrB3qJy/Mpn1CTpqeZGmRvqbYKmI/uSdTZw==
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN61LJQniFugoi6x2ujjnJM7tldXKimSA8e3cVKdvY80W2
VmVrGQo2zRokW/ZuO9bN64CPlXy2HmOeksJNoNOmCTRN2iWrExIMUdvTGpj/NNWq+ALT/bwKyDsjcfkGB9eQcrQ3H8UC+ywFIkXx32JcLCKuolLfVZKLqPTsQHTpYF9P
VmVrGQo2zRokW/ZuO9bN6yDw6frMtVHHQaPzsZSKK0aES6D4Kud9zFd68X/jO6wJ
VmVrGQo2zRokW/ZuO9bN65hAGaICagbU0z0X3nArVjY=
VmVrGQo2zRokW/ZuO9bN61V3/TpT/zrd2QvdUtMgGKQFi9VDtwL4Vc83iegeMAft36zpJ+t/eeWtmA4Eamfb77jKlCL/2MWCO4n+tmFfKcs5bo8I5tFr5Qu31v4FEN0K0c0Au7xRuJmPMknxTzJ1AQ4L8Z8QbLuQ3EJTDAgG6zw=
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN6wKjnvaTwlMeHWSuJ/EAxZ8Z1KtkmFIaU/d9KNNMifcN
VmVrGQo2zRokW/ZuO9bN62ExR0OHxzNY3oC4Mfi694VYJyilbRPMd6JDlCBDdbCe
32pdC9DD05OE2l0oXazDFLdmyVEUr33LQ7qI5CZQN57igfVF4gan9s5C0Jnc3Ki1
6ZPJI/HSoc4xA2zncU65FpfH+eGtPlOYNU1YCnnAis8=
ruTTNFTdswRs4Mc+srnRk818d9d/TGGXzgPjSrVtMi8=
1u+XjG/2+GSQRv6EzCaWRQ==
1u+XjG/2+GSQRv6EzCaWRQ==
Fq/4dKD9ssEKrNEHrSuyg9mm2ubmmEvtKER/ZGhfGKuM0YKPX8D/je6PexvVvgfs
Z8UsPk1Q7HtwjRd4g01ryw==
KNy3a/ETENhyr8ymSkKe9l1RX/SenKsGMtRp3/Nl0CxdpK2w2cyo43SfPqzKitUD
ho/Q+jrWDtBeg9J9ZTnKi+3gjgJLBNWc0CWgOki/1ZE=
Z8UsPk1Q7HtwjRd4g01ryw==
JbnXIK/axHVmA2plDNFCpPYlXWvPB/7lIz9ynMkifTY=
b4OJVZe8QyIpjuTpKXDL9A==
NhnIR3Ilo4H2su9/cTNo/Ps3vYSOiOY6esn1xeoWmAlmLwmaOoQ9T7lnCb6TRVZ67prVtxc96eRcwG0EieQAHA==
mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU=
xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+nhmNgO2Yqkfbla+bgPzSp/ulULnjCxA+skRNBtWTg0BA==
+plE/1bhdo64kO07cLlUXzzWH25pNAS0eDxp8hnHILg=
1u+XjG/2+GSQRv6EzCaWRQ==
0jp9NuG0oWLkMfJ/MsYOmfu7WkSnZjHksXr6BBYLZSUOzHDXl9XjsxiUKNVbrHEG
Z8UsPk1Q7HtwjRd4g01ryw==
+kAMkUztiJJUlfW5H0qbp+x6TgEEQKPe4CNld5ATADH0jjvxLnFDBqMEQxxyT+xg
vPgTNKnHCM+ykPDJGfC5NIdfn4GuPGcMgxOHmcFE4sA=
Z8UsPk1Q7HtwjRd4g01ryw==
IhuisKim47k91RVt8z8qtK4Hi8QoBKGCIdLYuWbuvVULY/yxOc14YsJd+ymvu1M2ua6mSguBgb6gPVMjpVflaQ==
guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE=

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,393 @@
# coding: utf-8
# -------------------------------------------------------------------
# YakPanel
# -------------------------------------------------------------------
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: yakpanel
# -------------------------------------------------------------------
# ------------------------------
# server safe app
# ------------------------------
import json
import os
import re
from copy import deepcopy
from typing import Callable
import public
from public.exceptions import HintException
from public.validate import Param
public.sys_path_append("class_v2/")
from ssh_security_v2 import ssh_security
from config_v2 import config
class main:
def __init__(self):
# {name:安全项名称,desc:描述,
# suggest:修复建议,check:检查函数,repair:修复函数,value:获取当前值函数,status:状态}
self.config = [
{
"name": "Default SSH Port",
"desc": public.lang("Modify the default SSH port to improve server security"),
"suggest": public.lang("Use a high port other than 22"),
"check": self.check_ssh_port,
},
{
"name": "Password Complexity Policy",
"desc": public.lang("Enable password complexity check to ensure password security"),
"suggest": public.lang("Use a level greater than 3"),
"check": self.check_ssh_minclass,
"repair": self.repair_ssh_minclass,
},
{
"name": "Password Length Limit",
"desc": public.lang("Set minimum password length requirement"),
"suggest": public.lang("Use a password of 9-20 characters"),
"check": self.check_ssh_security,
"repair": self.repair_ssh_passwd_len,
},
{
"name": "SSH Login Alert",
"desc": public.lang("Send alert notification upon SSH login"),
"suggest": public.lang("Enable SSH login alert"),
"check": self.check_ssh_login_sender,
},
{
"name": "Root Login Settings",
"desc": public.lang("It is recommended to allow key-based login only"),
"suggest": public.lang("Allow only SSH key-based login"),
"check": self.check_ssh_login_root_with_key,
},
{
"name": "SSH Brute-force",
"desc": public.lang("Prevent SSH brute-force attacks"),
"suggest": public.lang("Enable SSH brute-force protection"),
"check": self.check_ssh_fail2ban_brute,
},
{
"name": "Panel Login Alert",
"desc": public.lang("Send alert notification upon panel login"),
"suggest": public.lang("Enable panel login alert"),
"check": self.check_panel_swing,
},
{
"name": "Panel Google Authenticator login",
"desc": public.lang("Enable TOTP for enhanced security"),
"suggest": public.lang("Enable OTP authentication"),
"check": self.check_panel_login_2fa,
},
{
"name": "UnAuth Response Status Code",
"desc": public.lang("Set the HTTP response status code for unauthenticated access"),
"suggest": public.lang("Set 404 as the response code"),
"check": self.check_panel_not_auth_code,
},
{
"name": "Panel SSL",
"desc": public.lang("Enable HTTPS encrypted transmission (after setting will restart the panel)"),
"suggest": public.lang("Enable panel HTTPS"),
"check": self.check_panel_ssl,
}
]
self.ssh_security_obj = ssh_security()
self.config_obj = config()
def get_security_info(self, get=None):
"""
获取安全评分
"""
new_list = deepcopy(self.config)
for idx, module in enumerate(new_list):
if isinstance(module.get("check"), Callable):
try:
module["id"] = int(idx) + 1
check_status = module["check"]()
module["status"] = check_status.get("status", False)
module["value"] = check_status.get("value")
except:
module["status"] = False
module["value"] = None
if "check" in module and isinstance(module["check"], Callable):
del module["check"]
if "repair" in module and isinstance(module["repair"], Callable):
del module["repair"]
if "value" not in module:
module["value"] = None
total_score = 100 # 总分
score = total_score / len(new_list) # 每条的分数
missing_count = 0 # 缺少的条数
for module in new_list:
if module["status"] is False:
missing_count += 1
# 计算总分
security_score = total_score - (missing_count * score)
security_score = round(security_score, 2)
# 计算得分文本
if security_score >= 90:
score_text = public.lang("Secure")
elif security_score >= 70:
score_text = public.lang("Relatively Secure")
elif security_score >= 50:
score_text = public.lang("Average Security")
else:
score_text = public.lang("Insecure")
public.set_module_logs("server_secury", "get_security_info", 1)
return public.success_v2({
"security_data": new_list,
"total_score": total_score,
"score_text": score_text,
"score": int(security_score)
})
def install_fail2ban(self, get):
from panel_plugin_v2 import panelPlugin
public.set_module_logs("server_secury", "install_fail2ban", 1)
return panelPlugin().install_plugin(get)
def repair_security(self, get):
"""
@name 修复安全项
@parma {"name":"","args":{}}
"""
try:
get.validate([
Param("name").String().Require(),
Param("args").Dict().Require(),
], [public.validate.trim_filter()])
except Exception as ex:
public.print_log("error info: {}".format(ex))
return public.fail_v2(str(ex))
for security in self.config:
if security.get("name") == get.name and isinstance(security.get("repair"), Callable):
return security["repair"](public.to_dict_obj(get.args))
raise HintException(public.lang(f"Security Repair Item [{get.name}] Not Found!"))
@staticmethod
def _find_pwquality_conf_with_keyword(re_search: str) -> str:
"""
读取ssh密码复杂度配置
@param re_search: 正则表达式
"""
try:
if not re_search:
raise HintException("required parameter re_search")
p_file = '/etc/security/pwquality.conf'
p_body = public.readFile(p_file)
if not p_body:
return "" # 无配置文件时
tmp = re.findall(re_search, p_body, re.M)
if not tmp:
return "" # 未设置minclass
find = tmp[0].strip()
return find
except:
return "" # 异常时认为无
# =================== 检查函数 ===================
def check_ssh_port(self) -> dict:
"""
@name 检查SSH端口是否为默认端口22
"""
current_port = public.get_ssh_port()
return {"status": current_port != 22, "value": current_port}
def check_ssh_minclass(self) -> dict:
"""
@name 检查SSH密码复杂度策略
"""
re_pattern = r"\n\s*minclass\s+=\s+(.+)"
find = self._find_pwquality_conf_with_keyword(re_pattern)
if not find:
return {"status": False, "value": None} # 未设置minclass
minclass_value = int(find)
return {"status": minclass_value >= 3, "value": minclass_value}
def check_ssh_security(self) -> dict:
"""
@name 检查SSH密码长度限制
"""
re_pattern = r"\s*minlen\s+=\s+(.+)"
find = self._find_pwquality_conf_with_keyword(re_pattern)
if not find:
return {"status": True, "value": None} # 未设置minlen时认为无风险
minlen_value = int(find)
return {"status": minlen_value >= 9, "value": minlen_value}
def check_panel_swing(self) -> dict:
"""
@name 检查面板登录告警是否开启
"""
tip_files = [
"panel_login_send.pl", "login_send_type.pl", "login_send_mail.pl", "login_send_dingding.pl"
]
enabled_files = []
for fname in tip_files:
filename = "data/" + fname
if os.path.exists(filename):
enabled_files.append(fname)
break
is_enabled = len(enabled_files) > 0
value = None
if not is_enabled:
return {"status": False, "value": value}
task_file_path = "/www/server/panel/data/mod_push_data/task.json"
sender_file_path = "/www/server/panel/data/mod_push_data/sender.json"
task_data = {}
try:
with open(task_file_path, "r") as file:
tasks = json.load(file)
# 读取发送者配置文件
with open(sender_file_path, "r") as file:
senders = json.load(file)
sender_dict = {
sender["id"]: sender for sender in senders
}
# 查找特定的告警任务
for task in tasks:
if task.get("keyword") == "panel_login":
task_data = task
sender_types = set() # 使用集合来保证类型的唯一性
# 对应sender的ID获取sender_type并保证唯一性
for sender_id in task.get("sender", []):
if sender_id in sender_dict:
sender_types.add(sender_dict[sender_id]["sender_type"])
# 将唯一的通道类型列表转回列表格式,添加到告警数据中
task_data["channels"] = list(sender_types)
break
except:
pass
value = task_data
return {"status": value.get("status", False), "value": value}
def check_ssh_login_sender(self) -> dict:
"""
@name 检查SSH登录告警是否启用
"""
result = self.ssh_security_obj.get_login_send(None)
res = public.find_value_by_key(
result, "result", "error"
)
return {"status": res != "error", "value": res}
def check_ssh_login_root_with_key(self) -> dict:
"""
@name 检查SSH是否仅允许密钥登录root
"""
parsed = self.ssh_security_obj.paser_root_login()
current_policy = None
try:
current_policy = parsed[1]
except Exception as e:
import traceback
public.print_log("error info: {}".format(traceback.format_exc()))
return {"status": current_policy == "without-password", "value": current_policy}
def check_ssh_fail2ban_brute(self) -> dict:
"""
@name 检查SSH防爆破是否启用
"""
from safeModelV2.sshModel import main as sshmod
cfg = sshmod._get_ssh_fail2ban() or {}
current_value = cfg.get("status", 0)
return {"status": current_value == 1, "value": current_value}
def check_panel_login_2fa(self) -> dict:
"""
@name 检查面板登录动态口令认证是否启用
"""
current_value = self.config_obj.check_two_step(None)
res = public.find_value_by_key(
current_value, "result", False
)
return {"status": bool(res), "value": res}
def check_panel_not_auth_code(self) -> dict:
"""
@name 检查面板未登录响应状态码是否设置为 400+
"""
current_code = self.config_obj.get_not_auth_status()
return {"status": current_code != 0, "value": current_code}
def check_panel_ssl(self):
"""
@name 检查面板是否开启SSL
"""
enabled = os.path.exists("data/ssl.pl")
return {"status": bool(enabled), "value": enabled}
# =================== 修复函数 ===================
def repair_ssh_minclass(self, get):
"""
@name 修复SSH密码复杂度
@param {"minclass":9}
"""
try:
get.validate([
Param("minclass").Integer(">", 0).Require(),
], [public.validate.trim_filter()])
except Exception as ex:
public.print_log("error info: {}".format(ex))
return public.fail_v2(str(ex))
minclass = int(get.minclass)
file = "/etc/security/pwquality.conf"
result = {
"status": False, "msg": public.lang("Failed to set SSH password complexity, "
"please disable system hardening or set it manually")
}
if not os.path.exists(file):
public.ExecShell("apt install libpam-pwquality -y")
if os.path.exists(file):
f_data = public.readFile(file)
if re.findall("\n\s*minclass\s*=\s*\d*", f_data):
file_result = re.sub("\n\s*minclass\s*=\s*\d*", "\nminclass = {}".format(minclass), f_data)
else:
file_result = f_data + "\nminclass = {}".format(minclass)
public.writeFile(file, file_result)
f_data = public.readFile(file)
if f_data.find("minclass = {}".format(minclass)) != -1:
result["status"] = True
result["msg"] = public.lang("SSH minimum password complexity has been set")
return public.return_message(0 if result["status"] else 1, 0, result["msg"])
def repair_ssh_passwd_len(self, get):
"""
@name SSH密码最小长度设置
@param {"len":9}
"""
try:
get.validate([
Param("len").Integer(">", 0).Require(),
], [public.validate.trim_filter()])
except Exception as ex:
public.print_log("error info: {}".format(ex))
return public.fail_v2(str(ex))
pwd_len = int(get.len)
file = "/etc/security/pwquality.conf"
result = {
"status": False, "msg": public.lang("Failed to set SSH minimum password length, please set it manually")
}
if not os.path.exists(file):
public.ExecShell("apt install libpam-pwquality -y")
if os.path.exists(file):
f_data = public.readFile(file)
ssh_minlen = "\n#?\s*minlen\s*=\s*\d*"
file_result = re.sub(ssh_minlen, "\nminlen = {}".format(pwd_len), f_data)
public.writeFile(file, file_result)
f_data = public.readFile(file)
if f_data.find("minlen = {}".format(pwd_len)) != -1:
result["status"] = True
result["msg"] = "SSH minimum password length has been set to {}".format(pwd_len)
return public.return_message(0 if result["status"] else 1, 0, result["msg"])

View File

@@ -0,0 +1,380 @@
#coding: utf-8
#-------------------------------------------------------------------
# YakPanel
#-------------------------------------------------------------------
# Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
#-------------------------------------------------------------------
# Author: hwliang <hwl@yakpanel.com>
#-------------------------------------------------------------------
# ssh信息
#------------------------------
import json
import os
import re
import time
import public
from safeModelV2.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.return_message(0, 0, {
"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():
# count = 0
# if sfile in data[stype] and not sfile in ['/var/log/auth.log','/var/log/secure']:
# count += data[stype][sfile]
# else:
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
# return public.return_message(0, 0, 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
from ssh_security_v2 import ssh_security
data = {}
data['port'] = port
data['status'] = status
data['ping'] = isPing
data['config'] = ssh_security().get_config(None).get("message", {})
data['firewall_status'] = self.CheckFirewallStatus()
# data['error'] = self.get_ssh_intrusion(get)
data['fail2ban'] = self._get_ssh_fail2ban()
return public.return_message(0, 0, data)
def get_ssh_login_info(self, get):
"""
@获取SSH登录信息
"""
# return self.get_ssh_intrusion(get)
return public.return_message(0, 0, 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,'Port range must be between 22 and 65535!')
ports = ['21','25','80','443','8080','888','8888']
if port in ports: return public.returnMsg(False,'Please dont use default ports for common programs!')
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")
self.FirewallReload()
public.M('firewall').where("ps=? or ps=? or port=?",('SSH remote management service','SSH remote service',port)).delete()
public.M('firewall').add('port,ps,addtime',(port,'SSH remote service',time.strftime('%Y-%m-%d %X',time.localtime())))
public.WriteLog("TYPE_FIREWALL", "FIREWALL_SSH_PORT",(port,))
return public.return_message(0, 0,'Successfully modified')
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.ExecShell("/etc/init.d/sshd "+act)
public.ExecShell('service ssh ' + act)
public.ExecShell("systemctl "+act+" sshd")
public.ExecShell("systemctl "+act+" ssh")
public.WriteLog("TYPE_FIREWALL", msg)
return public.return_message(0, 0,'SUCCESS')

File diff suppressed because it is too large Load Diff