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

View File

@ -36,6 +36,19 @@ type SSHLog struct {
SuccessfulCount int `json:"successfulCount"`
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 {
Date time.Time `json:"date"`
DateStr string `json:"dateStr"`

View File

@ -608,3 +608,21 @@ func (u *FirewallService) addAddressRecord(req dto.AddrRuleOperate) error {
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
Update(key, value string) error
GenerateSSH(req dto.GenerateSSH) error
AnalysisLog(req dto.SearchForAnalysis) ([]dto.SSHLogAnalysis, error)
LoadSSHSecret(mode string) (string, 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
}
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) {
if _, err := os.Stat("/etc/ssh/sshd_config"); err != nil {
return "", buserr.New("ErrHttpReqNotFound")
@ -412,6 +477,47 @@ func loadSSHData(command string, showCountFrom, showCountTo, currentYear int, qq
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 {
var data dto.SSHHistory
parts := strings.Fields(line)

View File

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

View File

@ -111,4 +111,7 @@ ErrConfigParse: "Configuration file format error"
ErrConfigIsNull: "The configuration file is not allowed to be empty"
ErrConfigDirNotFound: "The running directory does not exist"
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: "運行目錄不存在"
ErrConfigAlreadyExist: "已存在同名配置文件"
ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}"
#ssh
ErrFirewall: "當前未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!"

View File

@ -112,3 +112,6 @@ ErrConfigIsNull: "配置文件不允许为空"
ErrConfigDirNotFound: "运行目录不存在"
ErrConfigAlreadyExist: "已存在同名配置文件"
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/secret", baseApi.LoadSSHSecret)
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
hostRouter.POST("/ssh/log/analysis", baseApi.AnalysisLog)
hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile)
hostRouter.POST("/ssh/operate", baseApi.OperateSSH)

View File

@ -1,9 +1,10 @@
package firewall
import (
"errors"
"os"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"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 {
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": [
"SSH"
],
"summary": "Update host ssh setting by file",
"summary": "Update host SSH setting by file",
"parameters": [
{
"description": "request",
@ -5940,11 +5940,11 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "获取 ssh 配置文件",
"description": "获取 SSH 配置文件",
"tags": [
"SSH"
],
"summary": "Load host ssh conf",
"summary": "Load host SSH conf",
"responses": {
"200": {
"description": "OK"
@ -5959,14 +5959,14 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "生成 ssh 密钥",
"description": "生成 SSH 密钥",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Generate host ssh secret",
"summary": "Generate host SSH secret",
"parameters": [
{
"description": "request",
@ -5992,21 +5992,21 @@ const docTemplate = `{
}
}
},
"/host/ssh/logs": {
"/host/ssh/log": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 ssh 登录日志",
"description": "获取 SSH 登录日志",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Load host ssh logs",
"summary": "Load host SSH logs",
"parameters": [
{
"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": {
"post": {
"security": [
@ -6042,7 +6081,7 @@ const docTemplate = `{
"tags": [
"SSH"
],
"summary": "Operate ssh",
"summary": "Operate SSH",
"parameters": [
{
"description": "request",
@ -6077,7 +6116,7 @@ const docTemplate = `{
"tags": [
"SSH"
],
"summary": "Load host ssh setting info",
"summary": "Load host SSH setting info",
"responses": {
"200": {
"description": "OK",
@ -6095,14 +6134,14 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "获取 ssh 密钥",
"description": "获取 SSH 密钥",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Load host ssh secret",
"summary": "Load host SSH secret",
"parameters": [
{
"description": "request",
@ -6135,7 +6174,7 @@ const docTemplate = `{
"tags": [
"SSH"
],
"summary": "Update host ssh setting",
"summary": "Update host SSH setting",
"parameters": [
{
"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": {
"type": "object",
"required": [
@ -14486,6 +14545,21 @@ const docTemplate = `{
}
}
},
"dto.SearchForAnalysis": {
"type": "object",
"required": [
"orderBy"
],
"properties": {
"orderBy": {
"type": "string",
"enum": [
"Success",
"Failed"
]
}
}
},
"dto.SearchForTree": {
"type": "object",
"properties": {
@ -14656,6 +14730,9 @@ const docTemplate = `{
"complexityVerification": {
"type": "string"
},
"defaultNetwork": {
"type": "string"
},
"dingVars": {
"type": "string"
},
@ -16305,6 +16382,9 @@ const docTemplate = `{
"resource": {
"type": "string"
},
"source": {
"type": "string"
},
"type": {
"type": "string"
},
@ -16364,6 +16444,9 @@ const docTemplate = `{
"rebuild": {
"type": "boolean"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
}

View File

@ -5900,7 +5900,7 @@
"tags": [
"SSH"
],
"summary": "Update host ssh setting by file",
"summary": "Update host SSH setting by file",
"parameters": [
{
"description": "request",
@ -5933,11 +5933,11 @@
"ApiKeyAuth": []
}
],
"description": "获取 ssh 配置文件",
"description": "获取 SSH 配置文件",
"tags": [
"SSH"
],
"summary": "Load host ssh conf",
"summary": "Load host SSH conf",
"responses": {
"200": {
"description": "OK"
@ -5952,14 +5952,14 @@
"ApiKeyAuth": []
}
],
"description": "生成 ssh 密钥",
"description": "生成 SSH 密钥",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Generate host ssh secret",
"summary": "Generate host SSH secret",
"parameters": [
{
"description": "request",
@ -5985,21 +5985,21 @@
}
}
},
"/host/ssh/logs": {
"/host/ssh/log": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 ssh 登录日志",
"description": "获取 SSH 登录日志",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Load host ssh logs",
"summary": "Load host SSH logs",
"parameters": [
{
"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": {
"post": {
"security": [
@ -6035,7 +6074,7 @@
"tags": [
"SSH"
],
"summary": "Operate ssh",
"summary": "Operate SSH",
"parameters": [
{
"description": "request",
@ -6070,7 +6109,7 @@
"tags": [
"SSH"
],
"summary": "Load host ssh setting info",
"summary": "Load host SSH setting info",
"responses": {
"200": {
"description": "OK",
@ -6088,14 +6127,14 @@
"ApiKeyAuth": []
}
],
"description": "获取 ssh 密钥",
"description": "获取 SSH 密钥",
"consumes": [
"application/json"
],
"tags": [
"SSH"
],
"summary": "Load host ssh secret",
"summary": "Load host SSH secret",
"parameters": [
{
"description": "request",
@ -6128,7 +6167,7 @@
"tags": [
"SSH"
],
"summary": "Update host ssh setting",
"summary": "Update host SSH setting",
"parameters": [
{
"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": {
"type": "object",
"required": [
@ -14479,6 +14538,21 @@
}
}
},
"dto.SearchForAnalysis": {
"type": "object",
"required": [
"orderBy"
],
"properties": {
"orderBy": {
"type": "string",
"enum": [
"Success",
"Failed"
]
}
}
},
"dto.SearchForTree": {
"type": "object",
"properties": {
@ -14649,6 +14723,9 @@
"complexityVerification": {
"type": "string"
},
"defaultNetwork": {
"type": "string"
},
"dingVars": {
"type": "string"
},
@ -16298,6 +16375,9 @@
"resource": {
"type": "string"
},
"source": {
"type": "string"
},
"type": {
"type": "string"
},
@ -16357,6 +16437,9 @@
"rebuild": {
"type": "boolean"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
}

View File

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

View File

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

View File

@ -123,3 +123,6 @@ export const loadSecret = (mode: string) => {
export const loadSSHLogs = (params: Host.searchSSHLog) => {
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',
upgradeerr: 'Upgrade Error',
pullerr: 'Pull Image Error',
rebuilding: '重建中',
rebuilding: 'ReBuilding',
deny: 'Denied',
accept: 'Accepted',
},
units: {
second: 'Second',
@ -950,6 +952,14 @@ const message = {
useDNS: 'useDNS',
dnsHelper:
'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',
loginMode: 'Login mode',
authenticating: 'Key',

View File

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

View File

@ -220,6 +220,8 @@ const message = {
upgradeerr: '升级失败',
pullerr: '镜像拉取失败',
rebuilding: '重建中',
deny: '已屏蔽',
accept: '已放行',
},
units: {
second: '秒',
@ -912,6 +914,12 @@ const message = {
keyAuthHelper: '是否启用密钥认证默认启用',
useDNS: '反向解析',
dnsHelper: '控制 SSH 服务器是否启用 DNS 解析功能从而验证连接方的身份',
analysis: '统计信息',
denyHelper: '将对下列地址进行屏蔽操作设置后该 IP 将禁止访问服务器是否继续',
acceptHelper: '将对下列地址进行放行操作设置后该 IP 将恢复正常访问是否继续',
noAddrWarning: '当前未选中任何可{0}地址请检查后重试',
successful: '成功',
failed: '失败',
loginLogs: '登录日志',
loginMode: '登录方式',
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>
<el-alert type="info" :title="$t('ssh.sshAlert')" :closable="false" />
</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>
<el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
<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>
<el-tag v-if="searchStatus === 'All'" type="success" size="large" style="margin-left: 15px">
{{ $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
type="primary"
@click="onLoadAnalysis"
:disabled="data?.length === 0"
style="margin-left: 5px"
>
{{ $t('ssh.analysis') }}
</el-button>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
@ -41,13 +42,7 @@
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@search="search"
>
<el-table-column type="selection" :selectable="selectable" fix />
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
<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('commons.table.port')" prop="port" />
@ -78,6 +73,8 @@
</ComplexTable>
</template>
</LayoutContent>
<Analysis ref="analysisRef" />
</div>
</template>
@ -86,9 +83,7 @@ import TableSetting from '@/components/table-setting/index.vue';
import { dateFormat } from '@/utils/util';
import { onMounted, reactive, ref } from '@vue/runtime-core';
import { loadSSHLogs } from '@/api/modules/host';
import { operateIPRule } from '@/api/modules/host';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import Analysis from '@/views/host/ssh/log/analysis/index.vue';
const loading = ref();
const data = ref();
@ -100,29 +95,10 @@ const paginationConfig = reactive({
});
const searchInfo = ref();
const searchStatus = ref('All');
const successfulCount = ref(0);
const faliedCount = ref(0);
const selects = ref<any>([]);
const analysisRef = ref();
function selectable(row: any): boolean {
return row.address !== '127.0.0.1' && row.address !== '::1';
}
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 onLoadAnalysis = () => {
analysisRef.value.acceptParams();
};
const search = async () => {
@ -137,8 +113,6 @@ const search = async () => {
.then((res) => {
loading.value = false;
data.value = res.data?.logs || [];
faliedCount.value = res.data.failedCount;
successfulCount.value = res.data.successfulCount;
if (searchStatus.value === 'Success') {
paginationConfig.total = res.data.successfulCount;
}

View File

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