1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 22:18:07 +08:00

feat: ssh 登录日志增加概览显示 (#2347)

This commit is contained in:
ssongliu 2023-09-20 14:18:20 +08:00 committed by GitHub
parent a520bdbe56
commit 017eb3814b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 756 additions and 105 deletions

View File

@ -9,7 +9,7 @@ import (
) )
// @Tags SSH // @Tags SSH
// @Summary Load host ssh setting info // @Summary Load host SSH setting info
// @Description 加载 SSH 配置信息 // @Description 加载 SSH 配置信息
// @Success 200 {object} dto.SSHInfo // @Success 200 {object} dto.SSHInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
@ -24,7 +24,7 @@ func (b *BaseApi) GetSSHInfo(c *gin.Context) {
} }
// @Tags SSH // @Tags SSH
// @Summary Operate ssh // @Summary Operate SSH
// @Description 修改 SSH 服务状态 // @Description 修改 SSH 服务状态
// @Accept json // @Accept json
// @Param request body dto.Operate true "request" // @Param request body dto.Operate true "request"
@ -50,7 +50,7 @@ func (b *BaseApi) OperateSSH(c *gin.Context) {
} }
// @Tags SSH // @Tags SSH
// @Summary Update host ssh setting // @Summary Update host SSH setting
// @Description 更新 SSH 配置 // @Description 更新 SSH 配置
// @Accept json // @Accept json
// @Param request body dto.SettingUpdate true "request" // @Param request body dto.SettingUpdate true "request"
@ -77,7 +77,7 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
} }
// @Tags SSH // @Tags SSH
// @Summary Update host ssh setting by file // @Summary Update host SSH setting by file
// @Description 上传文件更新 SSH 配置 // @Description 上传文件更新 SSH 配置
// @Accept json // @Accept json
// @Param request body dto.SSHConf true "request" // @Param request body dto.SSHConf true "request"
@ -104,8 +104,8 @@ func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
} }
// @Tags SSH // @Tags SSH
// @Summary Generate host ssh secret // @Summary Generate host SSH secret
// @Description 生成 ssh 密钥 // @Description 生成 SSH 密钥
// @Accept json // @Accept json
// @Param request body dto.GenerateSSH true "request" // @Param request body dto.GenerateSSH true "request"
// @Success 200 // @Success 200
@ -131,8 +131,8 @@ func (b *BaseApi) GenerateSSH(c *gin.Context) {
} }
// @Tags SSH // @Tags SSH
// @Summary Load host ssh secret // @Summary Load host SSH secret
// @Description 获取 ssh 密钥 // @Description 获取 SSH 密钥
// @Accept json // @Accept json
// @Param request body dto.GenerateLoad true "request" // @Param request body dto.GenerateLoad true "request"
// @Success 200 // @Success 200
@ -158,13 +158,40 @@ func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
} }
// @Tags SSH // @Tags SSH
// @Summary Load host ssh logs // @Summary Analysis host SSH logs
// @Description 获取 ssh 登录日志 // @Description 分析 SSH 登录日志
// @Accept json
// @Param request body dto.SearchForAnalysis true "request"
// @Success 200 {array} dto.SSHLogAnalysis
// @Security ApiKeyAuth
// @Router /host/ssh/log/analysis [post]
func (b *BaseApi) AnalysisLog(c *gin.Context) {
var req dto.SearchForAnalysis
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
data, err := sshService.AnalysisLog(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags SSH
// @Summary Load host SSH logs
// @Description 获取 SSH 登录日志
// @Accept json // @Accept json
// @Param request body dto.SearchSSHLog true "request" // @Param request body dto.SearchSSHLog true "request"
// @Success 200 {object} dto.SSHLog // @Success 200 {object} dto.SSHLog
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /host/ssh/logs [post] // @Router /host/ssh/log [post]
func (b *BaseApi) LoadSSHLogs(c *gin.Context) { func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
var req dto.SearchSSHLog var req dto.SearchSSHLog
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@ -185,8 +212,8 @@ func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
} }
// @Tags SSH // @Tags SSH
// @Summary Load host ssh conf // @Summary Load host SSH conf
// @Description 获取 ssh 配置文件 // @Description 获取 SSH 配置文件
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /host/ssh/conf [get] // @Router /host/ssh/conf [get]

View File

@ -36,6 +36,19 @@ type SSHLog struct {
SuccessfulCount int `json:"successfulCount"` SuccessfulCount int `json:"successfulCount"`
FailedCount int `json:"failedCount"` FailedCount int `json:"failedCount"`
} }
type SearchForAnalysis struct {
OrderBy string `json:"orderBy" validate:"required,oneof=Success Failed"`
}
type SSHLogAnalysis struct {
Address string `json:"address"`
Area string `json:"area"`
SuccessfulCount int `json:"successfulCount"`
FailedCount int `json:"failedCount"`
Status string `json:"status"`
}
type SSHHistory struct { type SSHHistory struct {
Date time.Time `json:"date"` Date time.Time `json:"date"`
DateStr string `json:"dateStr"` DateStr string `json:"dateStr"`

View File

@ -608,3 +608,21 @@ func (u *FirewallService) addAddressRecord(req dto.AddrRuleOperate) error {
Description: req.Description, Description: req.Description,
}) })
} }
func listIpRules(strategy string) ([]string, error) {
client, err := firewall.NewFirewallClient()
if err != nil {
return nil, err
}
addrs, err := client.ListAddress()
if err != nil {
return nil, err
}
var rules []string
for _, addr := range addrs {
if addr.Strategy == strategy {
rules = append(rules, addr.Address)
}
}
return rules, nil
}

View File

@ -32,6 +32,7 @@ type ISSHService interface {
UpdateByFile(value string) error UpdateByFile(value string) error
Update(key, value string) error Update(key, value string) error
GenerateSSH(req dto.GenerateSSH) error GenerateSSH(req dto.GenerateSSH) error
AnalysisLog(req dto.SearchForAnalysis) ([]dto.SSHLogAnalysis, error)
LoadSSHSecret(mode string) (string, error) LoadSSHSecret(mode string) (string, error)
LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error)
@ -303,6 +304,70 @@ func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) {
return &data, nil return &data, nil
} }
func (u *SSHService) AnalysisLog(req dto.SearchForAnalysis) ([]dto.SSHLogAnalysis, error) {
var fileList []string
baseDir := "/var/log"
if err := filepath.Walk(baseDir, func(pathItem string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") {
if !strings.HasSuffix(info.Name(), ".gz") {
fileList = append(fileList, pathItem)
return nil
}
itemFileName := strings.TrimSuffix(pathItem, ".gz")
if _, err := os.Stat(itemFileName); err != nil && os.IsNotExist(err) {
if err := handleGunzip(pathItem); err == nil {
fileList = append(fileList, itemFileName)
}
}
}
return nil
}); err != nil {
return nil, err
}
command := ""
sortMap := make(map[string]dto.SSHLogAnalysis)
for _, file := range fileList {
commandItem := ""
if strings.HasPrefix(path.Base(file), "secure") {
commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' | grep -v 'invalid' %s", file, command)
}
if strings.HasPrefix(path.Base(file), "auth.log") {
commandItem = fmt.Sprintf("cat %s | grep -aE \"(Connection closed by authenticating user|Accepted)\" | grep -v 'invalid' %s", file, command)
}
loadSSHDataForAnalysis(sortMap, commandItem)
}
var sortSlice []dto.SSHLogAnalysis
for key, value := range sortMap {
sortSlice = append(sortSlice, dto.SSHLogAnalysis{Address: key, SuccessfulCount: value.SuccessfulCount, FailedCount: value.FailedCount, Status: "accept"})
}
if req.OrderBy == constant.StatusSuccess {
sort.Slice(sortSlice, func(i, j int) bool {
return sortSlice[i].SuccessfulCount > sortSlice[j].SuccessfulCount
})
} else {
sort.Slice(sortSlice, func(i, j int) bool {
return sortSlice[i].FailedCount > sortSlice[j].FailedCount
})
}
qqWry, _ := qqwry.NewQQwry()
rules, _ := listIpRules("drop")
for i := 0; i < len(sortSlice); i++ {
sortSlice[i].Area = qqWry.Find(sortSlice[i].Address).Area
for _, rule := range rules {
if sortSlice[i].Address == rule {
sortSlice[i].Status = "drop"
break
}
}
}
return sortSlice, nil
}
func (u *SSHService) LoadSSHConf() (string, error) { func (u *SSHService) LoadSSHConf() (string, error) {
if _, err := os.Stat("/etc/ssh/sshd_config"); err != nil { if _, err := os.Stat("/etc/ssh/sshd_config"); err != nil {
return "", buserr.New("ErrHttpReqNotFound") return "", buserr.New("ErrHttpReqNotFound")
@ -412,6 +477,47 @@ func loadSSHData(command string, showCountFrom, showCountTo, currentYear int, qq
return datas, successCount, failedCount return datas, successCount, failedCount
} }
func loadSSHDataForAnalysis(analysisMap map[string]dto.SSHLogAnalysis, commandItem string) {
stdout, err := cmd.Exec(commandItem)
if err != nil {
return
}
lines := strings.Split(string(stdout), "\n")
for i := len(lines) - 1; i >= 0; i-- {
var itemData dto.SSHHistory
switch {
case strings.Contains(lines[i], "Failed password for"):
itemData = loadFailedSecureDatas(lines[i])
case strings.Contains(lines[i], "Connection closed by authenticating user"):
itemData = loadFailedAuthDatas(lines[i])
case strings.Contains(lines[i], "Accepted "):
itemData = loadSuccessDatas(lines[i])
}
if len(itemData.Address) != 0 {
if val, ok := analysisMap[itemData.Address]; ok {
if itemData.Status == constant.StatusSuccess {
val.SuccessfulCount++
} else {
val.FailedCount++
}
analysisMap[itemData.Address] = val
} else {
item := dto.SSHLogAnalysis{
Address: itemData.Address,
SuccessfulCount: 0,
FailedCount: 0,
}
if itemData.Status == constant.StatusSuccess {
item.SuccessfulCount = 1
} else {
item.FailedCount = 1
}
analysisMap[itemData.Address] = item
}
}
}
}
func loadSuccessDatas(line string) dto.SSHHistory { func loadSuccessDatas(line string) dto.SSHHistory {
var data dto.SSHHistory var data dto.SSHHistory
parts := strings.Fields(line) parts := strings.Fields(line)

View File

@ -126,3 +126,7 @@ var (
ErrOSSConn = "ErrOSSConn" ErrOSSConn = "ErrOSSConn"
ErrEntrance = "ErrEntrance" ErrEntrance = "ErrEntrance"
) )
var (
ErrFirewall = "ErrFirewall"
)

View File

@ -112,3 +112,6 @@ ErrConfigIsNull: "The configuration file is not allowed to be empty"
ErrConfigDirNotFound: "The running directory does not exist" ErrConfigDirNotFound: "The running directory does not exist"
ErrConfigAlreadyExist: "A configuration file with the same name already exists" ErrConfigAlreadyExist: "A configuration file with the same name already exists"
ErrUserFindErr: "Failed to find user {{ .name }} {{ .err }}" ErrUserFindErr: "Failed to find user {{ .name }} {{ .err }}"
#ssh
ErrFirewall: "No firewalld or ufw service is detected. Please check and try again!"

View File

@ -112,3 +112,6 @@ ErrConfigIsNull: "配置文件不允許為空"
ErrConfigDirNotFound: "運行目錄不存在" ErrConfigDirNotFound: "運行目錄不存在"
ErrConfigAlreadyExist: "已存在同名配置文件" ErrConfigAlreadyExist: "已存在同名配置文件"
ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}" ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}"
#ssh
ErrFirewall: "當前未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!"

View File

@ -112,3 +112,6 @@ ErrConfigIsNull: "配置文件不允许为空"
ErrConfigDirNotFound: "运行目录不存在" ErrConfigDirNotFound: "运行目录不存在"
ErrConfigAlreadyExist: "已存在同名配置文件" ErrConfigAlreadyExist: "已存在同名配置文件"
ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}" ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}"
#ssh
ErrFirewall: "当前未检测到系统 firewalld 或 ufw 服务,请检查后重试!"

View File

@ -41,6 +41,7 @@ func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) {
hostRouter.POST("/ssh/generate", baseApi.GenerateSSH) hostRouter.POST("/ssh/generate", baseApi.GenerateSSH)
hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret) hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret)
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs) hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
hostRouter.POST("/ssh/log/analysis", baseApi.AnalysisLog)
hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile) hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile)
hostRouter.POST("/ssh/operate", baseApi.OperateSSH) hostRouter.POST("/ssh/operate", baseApi.OperateSSH)

View File

@ -1,9 +1,10 @@
package firewall package firewall
import ( import (
"errors"
"os" "os"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/firewall/client" "github.com/1Panel-dev/1Panel/backend/utils/firewall/client"
) )
@ -30,5 +31,5 @@ func NewFirewallClient() (FirewallClient, error) {
if _, err := os.Stat("/usr/sbin/ufw"); err == nil { if _, err := os.Stat("/usr/sbin/ufw"); err == nil {
return client.NewUfw() return client.NewUfw()
} }
return nil, errors.New("no such type") return nil, buserr.New(constant.ErrFirewall)
} }

View File

@ -5907,7 +5907,7 @@ const docTemplate = `{
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Update host ssh setting by file", "summary": "Update host SSH setting by file",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -5940,11 +5940,11 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 ssh 配置文件", "description": "获取 SSH 配置文件",
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh conf", "summary": "Load host SSH conf",
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
@ -5959,14 +5959,14 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "生成 ssh 密钥", "description": "生成 SSH 密钥",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Generate host ssh secret", "summary": "Generate host SSH secret",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -5992,21 +5992,21 @@ const docTemplate = `{
} }
} }
}, },
"/host/ssh/logs": { "/host/ssh/log": {
"post": { "post": {
"security": [ "security": [
{ {
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 ssh 登录日志", "description": "获取 SSH 登录日志",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh logs", "summary": "Load host SSH logs",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -6028,6 +6028,45 @@ const docTemplate = `{
} }
} }
}, },
"/host/ssh/log/analysis": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "分析 SSH 登录日志",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Analysis host SSH logs",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchForAnalysis"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SSHLogAnalysis"
}
}
}
}
}
},
"/host/ssh/operate": { "/host/ssh/operate": {
"post": { "post": {
"security": [ "security": [
@ -6042,7 +6081,7 @@ const docTemplate = `{
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Operate ssh", "summary": "Operate SSH",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -6077,7 +6116,7 @@ const docTemplate = `{
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh setting info", "summary": "Load host SSH setting info",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -6095,14 +6134,14 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 ssh 密钥", "description": "获取 SSH 密钥",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh secret", "summary": "Load host SSH secret",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -6135,7 +6174,7 @@ const docTemplate = `{
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Update host ssh setting", "summary": "Update host SSH setting",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -14456,6 +14495,26 @@ const docTemplate = `{
} }
} }
}, },
"dto.SSHLogAnalysis": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"area": {
"type": "string"
},
"failedCount": {
"type": "integer"
},
"status": {
"type": "string"
},
"successfulCount": {
"type": "integer"
}
}
},
"dto.SSLUpdate": { "dto.SSLUpdate": {
"type": "object", "type": "object",
"required": [ "required": [
@ -14486,6 +14545,21 @@ const docTemplate = `{
} }
} }
}, },
"dto.SearchForAnalysis": {
"type": "object",
"required": [
"orderBy"
],
"properties": {
"orderBy": {
"type": "string",
"enum": [
"Success",
"Failed"
]
}
}
},
"dto.SearchForTree": { "dto.SearchForTree": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -14656,6 +14730,9 @@ const docTemplate = `{
"complexityVerification": { "complexityVerification": {
"type": "string" "type": "string"
}, },
"defaultNetwork": {
"type": "string"
},
"dingVars": { "dingVars": {
"type": "string" "type": "string"
}, },
@ -16305,6 +16382,9 @@ const docTemplate = `{
"resource": { "resource": {
"type": "string" "type": "string"
}, },
"source": {
"type": "string"
},
"type": { "type": {
"type": "string" "type": "string"
}, },
@ -16364,6 +16444,9 @@ const docTemplate = `{
"rebuild": { "rebuild": {
"type": "boolean" "type": "boolean"
}, },
"source": {
"type": "string"
},
"version": { "version": {
"type": "string" "type": "string"
} }

View File

@ -5900,7 +5900,7 @@
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Update host ssh setting by file", "summary": "Update host SSH setting by file",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -5933,11 +5933,11 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 ssh 配置文件", "description": "获取 SSH 配置文件",
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh conf", "summary": "Load host SSH conf",
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
@ -5952,14 +5952,14 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "生成 ssh 密钥", "description": "生成 SSH 密钥",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Generate host ssh secret", "summary": "Generate host SSH secret",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -5985,21 +5985,21 @@
} }
} }
}, },
"/host/ssh/logs": { "/host/ssh/log": {
"post": { "post": {
"security": [ "security": [
{ {
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 ssh 登录日志", "description": "获取 SSH 登录日志",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh logs", "summary": "Load host SSH logs",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -6021,6 +6021,45 @@
} }
} }
}, },
"/host/ssh/log/analysis": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "分析 SSH 登录日志",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Analysis host SSH logs",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchForAnalysis"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SSHLogAnalysis"
}
}
}
}
}
},
"/host/ssh/operate": { "/host/ssh/operate": {
"post": { "post": {
"security": [ "security": [
@ -6035,7 +6074,7 @@
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Operate ssh", "summary": "Operate SSH",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -6070,7 +6109,7 @@
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh setting info", "summary": "Load host SSH setting info",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -6088,14 +6127,14 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 ssh 密钥", "description": "获取 SSH 密钥",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Load host ssh secret", "summary": "Load host SSH secret",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -6128,7 +6167,7 @@
"tags": [ "tags": [
"SSH" "SSH"
], ],
"summary": "Update host ssh setting", "summary": "Update host SSH setting",
"parameters": [ "parameters": [
{ {
"description": "request", "description": "request",
@ -14449,6 +14488,26 @@
} }
} }
}, },
"dto.SSHLogAnalysis": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"area": {
"type": "string"
},
"failedCount": {
"type": "integer"
},
"status": {
"type": "string"
},
"successfulCount": {
"type": "integer"
}
}
},
"dto.SSLUpdate": { "dto.SSLUpdate": {
"type": "object", "type": "object",
"required": [ "required": [
@ -14479,6 +14538,21 @@
} }
} }
}, },
"dto.SearchForAnalysis": {
"type": "object",
"required": [
"orderBy"
],
"properties": {
"orderBy": {
"type": "string",
"enum": [
"Success",
"Failed"
]
}
}
},
"dto.SearchForTree": { "dto.SearchForTree": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -14649,6 +14723,9 @@
"complexityVerification": { "complexityVerification": {
"type": "string" "type": "string"
}, },
"defaultNetwork": {
"type": "string"
},
"dingVars": { "dingVars": {
"type": "string" "type": "string"
}, },
@ -16298,6 +16375,9 @@
"resource": { "resource": {
"type": "string" "type": "string"
}, },
"source": {
"type": "string"
},
"type": { "type": {
"type": "string" "type": "string"
}, },
@ -16357,6 +16437,9 @@
"rebuild": { "rebuild": {
"type": "boolean" "type": "boolean"
}, },
"source": {
"type": "string"
},
"version": { "version": {
"type": "string" "type": "string"
} }

View File

@ -1855,6 +1855,19 @@ definitions:
totalCount: totalCount:
type: integer type: integer
type: object type: object
dto.SSHLogAnalysis:
properties:
address:
type: string
area:
type: string
failedCount:
type: integer
status:
type: string
successfulCount:
type: integer
type: object
dto.SSLUpdate: dto.SSLUpdate:
properties: properties:
cert: cert:
@ -1875,6 +1888,16 @@ definitions:
required: required:
- ssl - ssl
type: object type: object
dto.SearchForAnalysis:
properties:
orderBy:
enum:
- Success
- Failed
type: string
required:
- orderBy
type: object
dto.SearchForTree: dto.SearchForTree:
properties: properties:
info: info:
@ -1989,6 +2012,8 @@ definitions:
type: string type: string
complexityVerification: complexityVerification:
type: string type: string
defaultNetwork:
type: string
dingVars: dingVars:
type: string type: string
email: email:
@ -3089,6 +3114,8 @@ definitions:
type: object type: object
resource: resource:
type: string type: string
source:
type: string
type: type:
type: string type: string
version: version:
@ -3128,6 +3155,8 @@ definitions:
type: object type: object
rebuild: rebuild:
type: boolean type: boolean
source:
type: string
version: version:
type: string type: string
type: object type: object
@ -7804,7 +7833,7 @@ paths:
description: OK description: OK
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Update host ssh setting by file summary: Update host SSH setting by file
tags: tags:
- SSH - SSH
x-panel-log: x-panel-log:
@ -7815,20 +7844,20 @@ paths:
paramKeys: [] paramKeys: []
/host/ssh/conf: /host/ssh/conf:
get: get:
description: 获取 ssh 配置文件 description: 获取 SSH 配置文件
responses: responses:
"200": "200":
description: OK description: OK
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Load host ssh conf summary: Load host SSH conf
tags: tags:
- SSH - SSH
/host/ssh/generate: /host/ssh/generate:
post: post:
consumes: consumes:
- application/json - application/json
description: 生成 ssh 密钥 description: 生成 SSH 密钥
parameters: parameters:
- description: request - description: request
in: body in: body
@ -7841,7 +7870,7 @@ paths:
description: OK description: OK
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Generate host ssh secret summary: Generate host SSH secret
tags: tags:
- SSH - SSH
x-panel-log: x-panel-log:
@ -7850,11 +7879,11 @@ paths:
formatEN: generate SSH secret formatEN: generate SSH secret
formatZH: '生成 SSH 密钥 ' formatZH: '生成 SSH 密钥 '
paramKeys: [] paramKeys: []
/host/ssh/logs: /host/ssh/log:
post: post:
consumes: consumes:
- application/json - application/json
description: 获取 ssh 登录日志 description: 获取 SSH 登录日志
parameters: parameters:
- description: request - description: request
in: body in: body
@ -7869,7 +7898,31 @@ paths:
$ref: '#/definitions/dto.SSHLog' $ref: '#/definitions/dto.SSHLog'
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Load host ssh logs summary: Load host SSH logs
tags:
- SSH
/host/ssh/log/analysis:
post:
consumes:
- application/json
description: 分析 SSH 登录日志
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchForAnalysis'
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.SSHLogAnalysis'
type: array
security:
- ApiKeyAuth: []
summary: Analysis host SSH logs
tags: tags:
- SSH - SSH
/host/ssh/operate: /host/ssh/operate:
@ -7887,7 +7940,7 @@ paths:
responses: {} responses: {}
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Operate ssh summary: Operate SSH
tags: tags:
- SSH - SSH
x-panel-log: x-panel-log:
@ -7907,14 +7960,14 @@ paths:
$ref: '#/definitions/dto.SSHInfo' $ref: '#/definitions/dto.SSHInfo'
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Load host ssh setting info summary: Load host SSH setting info
tags: tags:
- SSH - SSH
/host/ssh/secret: /host/ssh/secret:
post: post:
consumes: consumes:
- application/json - application/json
description: 获取 ssh 密钥 description: 获取 SSH 密钥
parameters: parameters:
- description: request - description: request
in: body in: body
@ -7927,7 +7980,7 @@ paths:
description: OK description: OK
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Load host ssh secret summary: Load host SSH secret
tags: tags:
- SSH - SSH
/host/ssh/update: /host/ssh/update:
@ -7947,7 +8000,7 @@ paths:
description: OK description: OK
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Update host ssh setting summary: Update host SSH setting
tags: tags:
- SSH - SSH
x-panel-log: x-panel-log:

View File

@ -141,6 +141,13 @@ export namespace Host {
successfulCount: number; successfulCount: number;
failedCount: number; failedCount: number;
} }
export interface logAnalysis {
address: string;
area: string;
successfulCount: number;
failedCount: number;
status: string;
}
export interface sshHistory { export interface sshHistory {
date: Date; date: Date;
area: string; area: string;

View File

@ -123,3 +123,6 @@ export const loadSecret = (mode: string) => {
export const loadSSHLogs = (params: Host.searchSSHLog) => { export const loadSSHLogs = (params: Host.searchSSHLog) => {
return http.post<Host.sshLog>(`/hosts/ssh/log`, params); return http.post<Host.sshLog>(`/hosts/ssh/log`, params);
}; };
export const loadAnalysis = (orderBy: string) => {
return http.post<Array<Host.logAnalysis>>(`/hosts/ssh/log/analysis`, { orderBy: orderBy }, TimeoutEnum.T_40S);
};

View File

@ -221,7 +221,9 @@ const message = {
upgrading: 'Upgrading', upgrading: 'Upgrading',
upgradeerr: 'Upgrade Error', upgradeerr: 'Upgrade Error',
pullerr: 'Pull Image Error', pullerr: 'Pull Image Error',
rebuilding: '重建中', rebuilding: 'ReBuilding',
deny: 'Denied',
accept: 'Accepted',
}, },
units: { units: {
second: 'Second', second: 'Second',
@ -950,6 +952,14 @@ const message = {
useDNS: 'useDNS', useDNS: 'useDNS',
dnsHelper: dnsHelper:
'Controls whether the DNS resolution function is enabled on the SSH server to verify the identity of the connection.', 'Controls whether the DNS resolution function is enabled on the SSH server to verify the identity of the connection.',
analysis: 'Statistical information',
denyHelper:
"Performing a 'deny' operation on the following addresses. After setting, the IP will be prohibited from accessing the server. Do you want to continue?",
acceptHelper:
"Performing an 'accept' operation on the following addresses. After setting, the IP will regain normal access. Do you want to continue?",
noAddrWarning: 'No [{0}] addresses are currently selected. Please check and try again!',
successful: 'Success',
failed: 'Failed',
loginLogs: 'SSH login log', loginLogs: 'SSH login log',
loginMode: 'Login mode', loginMode: 'Login mode',
authenticating: 'Key', authenticating: 'Key',

View File

@ -220,6 +220,8 @@ const message = {
upgradeerr: '升級失敗', upgradeerr: '升級失敗',
pullerr: '鏡像拉取失敗', pullerr: '鏡像拉取失敗',
rebuilding: '重建中', rebuilding: '重建中',
deny: '已屏蔽',
accept: '已放行',
}, },
units: { units: {
second: '秒', second: '秒',
@ -912,6 +914,12 @@ const message = {
keyAuthHelper: '是否啟用密鑰認證默認啟用', keyAuthHelper: '是否啟用密鑰認證默認啟用',
useDNS: '反向解析', useDNS: '反向解析',
dnsHelper: '控製 SSH 服務器是否啟用 DNS 解析功能從而驗證連接方的身份', dnsHelper: '控製 SSH 服務器是否啟用 DNS 解析功能從而驗證連接方的身份',
analysis: '統計信息',
denyHelper: '將對下列地址進行屏蔽操作設置後該 IP 將禁止訪問服務器是否繼續',
acceptHelper: '將對下列地址進行放行操作設置後該 IP 將恢復正常訪問是否繼續',
noAddrWarning: '當前未選中任何可{0}地址請檢查後重試',
successful: '成功',
failed: '失敗',
loginLogs: 'SSH 登錄日誌', loginLogs: 'SSH 登錄日誌',
loginMode: '登錄方式', loginMode: '登錄方式',
authenticating: '密鑰', authenticating: '密鑰',

View File

@ -220,6 +220,8 @@ const message = {
upgradeerr: '升级失败', upgradeerr: '升级失败',
pullerr: '镜像拉取失败', pullerr: '镜像拉取失败',
rebuilding: '重建中', rebuilding: '重建中',
deny: '已屏蔽',
accept: '已放行',
}, },
units: { units: {
second: '秒', second: '秒',
@ -912,6 +914,12 @@ const message = {
keyAuthHelper: '是否启用密钥认证默认启用', keyAuthHelper: '是否启用密钥认证默认启用',
useDNS: '反向解析', useDNS: '反向解析',
dnsHelper: '控制 SSH 服务器是否启用 DNS 解析功能从而验证连接方的身份', dnsHelper: '控制 SSH 服务器是否启用 DNS 解析功能从而验证连接方的身份',
analysis: '统计信息',
denyHelper: '将对下列地址进行屏蔽操作设置后该 IP 将禁止访问服务器是否继续',
acceptHelper: '将对下列地址进行放行操作设置后该 IP 将恢复正常访问是否继续',
noAddrWarning: '当前未选中任何可{0}地址请检查后重试',
successful: '成功',
failed: '失败',
loginLogs: '登录日志', loginLogs: '登录日志',
loginMode: '登录方式', loginMode: '登录方式',
authenticating: '密钥', authenticating: '密钥',

View File

@ -0,0 +1,244 @@
<template>
<div>
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('menu.home')" :back="handleClose" />
</template>
<div v-loading="loading">
<el-form label-position="top" class="ml-5">
<el-row type="flex" justify="center" :gutter="20">
<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item>
<template #label>
<span class="status-label">{{ $t('ssh.successful') }}</span>
</template>
<span class="status-count">{{ successfulTotalCount }}</span>
</el-form-item>
</el-col>
<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item>
<template #label>
<span class="status-label">{{ $t('ssh.failed') }}</span>
</template>
<span class="status-count">{{ failedTotalCount }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-button type="primary" @click="onChangeStatus('accept', null)" :disabled="selects.length === 0">
{{ $t('firewall.allow') }}
</el-button>
<el-button type="primary" @click="onChangeStatus('drop', null)" :disabled="selects.length === 0">
{{ $t('firewall.deny') }}
</el-button>
<ComplexTable v-model:selects="selects" class="mt-5" :data="data" @header-click="changeSort">
<el-table-column type="selection" fix :selectable="selectable" />
<el-table-column :label="$t('logs.loginIP')" prop="address" min-width="40" />
<el-table-column :label="$t('ssh.belong')" prop="area" min-width="40" />
<el-table-column prop="successfulCount" min-width="20">
<template #header>
{{ $t('ssh.successful') }}
<el-icon style="cursor: pointer" @click="search('Success')"><CaretBottom /></el-icon>
</template>
<template #default="{ row }">
<el-button type="primary" link>{{ row.successfulCount }}</el-button>
</template>
</el-table-column>
<el-table-column prop="failedCount" min-width="20">
<template #header>
{{ $t('ssh.failed') }}
<el-icon style="cursor: pointer" @click="search('Failed')"><CaretBottom /></el-icon>
</template>
<template #default="{ row }">
<el-button type="danger" link>{{ row.failedCount }}</el-button>
</template>
</el-table-column>
<el-table-column :min-width="30" :label="$t('commons.table.status')" prop="strategy">
<template #default="{ row }">
<el-button
v-if="row.status === 'accept'"
:disabled="!selectable(row)"
@click="onChangeStatus('drop', row)"
link
type="success"
>
{{ $t('commons.status.accept') }}
</el-button>
<el-button
v-else
link
:disabled="!selectable(row)"
type="danger"
@click="onChangeStatus('accept', row)"
>
{{ $t('commons.status.deny') }}
</el-button>
</template>
</el-table-column>
</ComplexTable>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-drawer>
<el-dialog
v-model="dialogVisible"
:title="$t('firewall.' + (operation === 'drop' ? 'deny' : 'allow'))"
width="30%"
:close-on-click-modal="false"
>
<el-row>
<el-col :span="20" :offset="2">
<el-alert :title="msg" show-icon type="error" :closable="false"></el-alert>
<div class="resource">
<table>
<tr v-for="(row, index) in operationList" :key="index">
<td>
<span>{{ row }}</span>
</td>
</tr>
</table>
</div>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="submitOperation" v-loading="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { loadAnalysis, operateIPRule } from '@/api/modules/host';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import { Host } from '@/api/interface/host';
const drawerVisiable = ref();
const loading = ref();
const data = ref();
const successfulTotalCount = ref();
const failedTotalCount = ref();
const selects = ref<any>([]);
const dialogVisible = ref();
const msg = ref();
const operation = ref();
const operationList = ref();
const acceptParams = (): void => {
search('Failed');
drawerVisiable.value = true;
};
const search = async (status: string) => {
loading.value = true;
loadAnalysis(status)
.then((res) => {
loading.value = false;
data.value = res.data || [];
successfulTotalCount.value = 0;
failedTotalCount.value = 0;
for (const item of data.value) {
successfulTotalCount.value += item.successfulCount;
failedTotalCount.value += item.failedCount;
}
})
.catch(() => {
loading.value = false;
});
};
function selectable(row: any): boolean {
return row.address !== '127.0.0.1' && row.address !== '::1';
}
const changeSort = (column: any) => {
switch (column.property) {
case 'successfulCount':
search('Success');
return;
case 'failedCount':
search('Failed');
return;
}
};
const onChangeStatus = async (status: string, row: Host.logAnalysis | null) => {
operationList.value = [];
if (row) {
if (row.status !== status) {
operationList.value.push(row.address);
}
} else {
for (const item of selects.value) {
if (item.status !== status) {
operationList.value.push(item.address);
}
}
}
if (operationList.value.length === 0) {
MsgError(
i18n.global.t('ssh.noAddrWarning', [i18n.global.t('firewall.' + (status === 'drop' ? 'deny' : 'allow'))]),
);
return;
}
operation.value = status;
msg.value = status === 'drop' ? i18n.global.t('ssh.denyHelper') : i18n.global.t('ssh.acceptHelper');
dialogVisible.value = true;
};
const submitOperation = async () => {
const pros = [];
for (const item of operationList.value) {
pros.push(
operateIPRule({
operation: operation.value === 'drop' ? 'add' : 'remove',
address: item,
strategy: 'drop',
description: '',
}),
);
}
loading.value = true;
Promise.all(pros)
.then(() => {
loading.value = false;
dialogVisible.value = false;
search('Failed');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.finally(() => {
loading.value = false;
});
};
const handleClose = () => {
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.resource {
margin-top: 10px;
max-height: 400px;
overflow: auto;
}
</style>

View File

@ -4,23 +4,24 @@
<template #prompt> <template #prompt>
<el-alert type="info" :title="$t('ssh.sshAlert')" :closable="false" /> <el-alert type="info" :title="$t('ssh.sshAlert')" :closable="false" />
</template> </template>
<template #search>
<el-select v-model="searchStatus" @change="search()">
<template #prefix>{{ $t('commons.table.status') }}</template>
<el-option :label="$t('commons.table.all')" value="All"></el-option>
<el-option :label="$t('commons.status.success')" value="Success"></el-option>
<el-option :label="$t('commons.status.failed')" value="Failed"></el-option>
</el-select>
</template>
<template #toolbar> <template #toolbar>
<el-row> <el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16"> <el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
<el-select v-model="searchStatus" @change="search()"> <el-button
<template #prefix>{{ $t('commons.table.status') }}</template> type="primary"
<el-option :label="$t('commons.table.all')" value="All"></el-option> @click="onLoadAnalysis"
<el-option :label="$t('commons.status.success')" value="Success"></el-option> :disabled="data?.length === 0"
<el-option :label="$t('commons.status.failed')" value="Failed"></el-option> style="margin-left: 5px"
</el-select> >
<el-tag v-if="searchStatus === 'All'" type="success" size="large" style="margin-left: 15px"> {{ $t('ssh.analysis') }}
{{ $t('commons.status.success') }} {{ successfulCount }}
</el-tag>
<el-tag v-if="searchStatus === 'All'" type="danger" size="large" style="margin-left: 5px">
{{ $t('commons.status.failed') }} {{ faliedCount }}
</el-tag>
<el-button plain @click="onDeny" :disabled="selects.length === 0" style="margin-left: 5px">
{{ $t('firewall.deny') }}
</el-button> </el-button>
</el-col> </el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8"> <el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
@ -41,13 +42,7 @@
</template> </template>
<template #main> <template #main>
<ComplexTable <ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@search="search"
>
<el-table-column type="selection" :selectable="selectable" fix />
<el-table-column min-width="80" :label="$t('logs.loginIP')" prop="address" /> <el-table-column min-width="80" :label="$t('logs.loginIP')" prop="address" />
<el-table-column min-width="60" :label="$t('ssh.belong')" prop="area" /> <el-table-column min-width="60" :label="$t('ssh.belong')" prop="area" />
<el-table-column min-width="60" :label="$t('commons.table.port')" prop="port" /> <el-table-column min-width="60" :label="$t('commons.table.port')" prop="port" />
@ -78,6 +73,8 @@
</ComplexTable> </ComplexTable>
</template> </template>
</LayoutContent> </LayoutContent>
<Analysis ref="analysisRef" />
</div> </div>
</template> </template>
@ -86,9 +83,7 @@ import TableSetting from '@/components/table-setting/index.vue';
import { dateFormat } from '@/utils/util'; import { dateFormat } from '@/utils/util';
import { onMounted, reactive, ref } from '@vue/runtime-core'; import { onMounted, reactive, ref } from '@vue/runtime-core';
import { loadSSHLogs } from '@/api/modules/host'; import { loadSSHLogs } from '@/api/modules/host';
import { operateIPRule } from '@/api/modules/host'; import Analysis from '@/views/host/ssh/log/analysis/index.vue';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const loading = ref(); const loading = ref();
const data = ref(); const data = ref();
@ -100,29 +95,10 @@ const paginationConfig = reactive({
}); });
const searchInfo = ref(); const searchInfo = ref();
const searchStatus = ref('All'); const searchStatus = ref('All');
const successfulCount = ref(0); const analysisRef = ref();
const faliedCount = ref(0);
const selects = ref<any>([]);
function selectable(row: any): boolean { const onLoadAnalysis = () => {
return row.address !== '127.0.0.1' && row.address !== '::1'; analysisRef.value.acceptParams();
}
function select2address(): string {
let res = [];
selects.value.forEach((item: any) => {
if (!res.includes(item.address)) res.push(item.address);
});
return res.join(',');
}
const onDeny = async () => {
let address = select2address();
if (!address) return;
await operateIPRule({ operation: 'add', address: address, strategy: 'drop', description: '' }).then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});
}; };
const search = async () => { const search = async () => {
@ -137,8 +113,6 @@ const search = async () => {
.then((res) => { .then((res) => {
loading.value = false; loading.value = false;
data.value = res.data?.logs || []; data.value = res.data?.logs || [];
faliedCount.value = res.data.failedCount;
successfulCount.value = res.data.successfulCount;
if (searchStatus.value === 'Success') { if (searchStatus.value === 'Success') {
paginationConfig.total = res.data.successfulCount; paginationConfig.total = res.data.successfulCount;
} }

View File

@ -64,7 +64,6 @@ const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
form.defaultNetwork = params.defaultNetwork; form.defaultNetwork = params.defaultNetwork;
console.log(form.defaultNetwork);
loadNetworkOptions(); loadNetworkOptions();
drawerVisiable.value = true; drawerVisiable.value = true;
}; };