feat: ClamAV 增加病毒文件处理策略 (#5635)

pull/5644/head
ssongliu 5 months ago committed by GitHub
parent d71514885d
commit f131aae344
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -210,18 +210,18 @@ func (b *BaseApi) UpdateFile(c *gin.Context) {
// @Summary Delete clam // @Summary Delete clam
// @Description 删除扫描规则 // @Description 删除扫描规则
// @Accept json // @Accept json
// @Param request body dto.BatchDeleteReq true "request" // @Param request body dto.ClamDelete true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /toolbox/clam/del [post] // @Router /toolbox/clam/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"clams","output_column":"name","output_value":"names"}],"formatZH":"删除扫描规则 [names]","formatEN":"delete clam [names]"} // @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"clams","output_column":"name","output_value":"names"}],"formatZH":"删除扫描规则 [names]","formatEN":"delete clam [names]"}
func (b *BaseApi) DeleteClam(c *gin.Context) { func (b *BaseApi) DeleteClam(c *gin.Context) {
var req dto.BatchDeleteReq var req dto.ClamDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := clamService.Delete(req.Ids); err != nil { if err := clamService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }

@ -16,6 +16,8 @@ type ClamInfo struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
LastHandleDate string `json:"lastHandleDate"` LastHandleDate string `json:"lastHandleDate"`
Description string `json:"description"` Description string `json:"description"`
} }
@ -40,6 +42,8 @@ type ClamLog struct {
type ClamCreate struct { type ClamCreate struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
Description string `json:"description"` Description string `json:"description"`
} }
@ -48,5 +52,13 @@ type ClamUpdate struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"`
Description string `json:"description"` Description string `json:"description"`
} }
type ClamDelete struct {
RemoveResult bool `json:"removeResult"`
RemoveInfected bool `json:"removeInfected"`
Ids []uint `json:"ids" validate:"required"`
}

@ -5,5 +5,7 @@ type Clam struct {
Name string `gorm:"type:varchar(64);not null" json:"name"` Name string `gorm:"type:varchar(64);not null" json:"name"`
Path string `gorm:"type:varchar(64);not null" json:"path"` Path string `gorm:"type:varchar(64);not null" json:"path"`
Description string `gorm:"type:varchar(64);not null" json:"description"` InfectedStrategy string `gorm:"type:varchar(64)" json:"infectedStrategy"`
InfectedDir string `gorm:"type:varchar(64)" json:"infectedDir"`
Description string `gorm:"type:varchar(64)" json:"description"`
} }

@ -26,7 +26,7 @@ import (
const ( const (
clamServiceNameCentOs = "clamd@scan.service" clamServiceNameCentOs = "clamd@scan.service"
clamServiceNameUbuntu = "clamav-daemon.service" clamServiceNameUbuntu = "clamav-daemon.service"
scanDir = "scan-result" resultDir = "clamav"
) )
type ClamService struct { type ClamService struct {
@ -39,7 +39,7 @@ type IClamService interface {
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate) error Create(req dto.ClamCreate) error
Update(req dto.ClamUpdate) error Update(req dto.ClamUpdate) error
Delete(ids []uint) error Delete(req dto.ClamDelete) error
HandleOnce(req dto.OperateByID) error HandleOnce(req dto.OperateByID) error
LoadFile(req dto.OperationWithName) (string, error) LoadFile(req dto.OperationWithName) (string, error)
UpdateFile(req dto.UpdateByNameAndFile) error UpdateFile(req dto.UpdateByNameAndFile) error
@ -154,15 +154,21 @@ func (f *ClamService) Update(req dto.ClamUpdate) error {
return nil return nil
} }
func (u *ClamService) Delete(ids []uint) error { func (u *ClamService) Delete(req dto.ClamDelete) error {
if len(ids) == 1 { for _, id := range req.Ids {
clam, _ := clamRepo.Get(commonRepo.WithByID(ids[0])) clam, _ := clamRepo.Get(commonRepo.WithByID(id))
if clam.ID == 0 { if clam.ID == 0 {
return constant.ErrRecordNotFound continue
}
if req.RemoveResult {
_ = os.RemoveAll(path.Join(global.CONF.System.DataDir, resultDir, clam.Name))
} }
return clamRepo.Delete(commonRepo.WithByID(ids[0])) if req.RemoveInfected {
_ = os.RemoveAll(path.Join(clam.InfectedDir, "1panel-infected", clam.Name))
} }
return clamRepo.Delete(commonRepo.WithIdsIn(ids)) return clamRepo.Delete(commonRepo.WithByID(id))
}
return nil
} }
func (u *ClamService) HandleOnce(req dto.OperateByID) error { func (u *ClamService) HandleOnce(req dto.OperateByID) error {
@ -173,12 +179,30 @@ func (u *ClamService) HandleOnce(req dto.OperateByID) error {
if cmd.CheckIllegal(clam.Path) { if cmd.CheckIllegal(clam.Path) {
return buserr.New(constant.ErrCmdIllegal) return buserr.New(constant.ErrCmdIllegal)
} }
logFile := path.Join(global.CONF.System.DataDir, scanDir, clam.Name, time.Now().Format(constant.DateTimeSlimLayout)) timeNow := time.Now().Format(constant.DateTimeSlimLayout)
logFile := path.Join(global.CONF.System.DataDir, resultDir, clam.Name, timeNow)
if _, err := os.Stat(path.Dir(logFile)); err != nil { if _, err := os.Stat(path.Dir(logFile)); err != nil {
_ = os.MkdirAll(path.Dir(logFile), os.ModePerm) _ = os.MkdirAll(path.Dir(logFile), os.ModePerm)
} }
go func() { go func() {
cmd := exec.Command("clamdscan", "--fdpass", clam.Path, "-l", logFile) strategy := ""
switch clam.InfectedStrategy {
case "remove":
strategy = "--remove"
case "move":
dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow)
strategy = "--move=" + dir
if _, err := os.Stat(dir); err != nil {
_ = os.MkdirAll(dir, os.ModePerm)
}
case "copy":
dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow)
strategy = "--copy=" + dir
if _, err := os.Stat(dir); err != nil {
_ = os.MkdirAll(dir, os.ModePerm)
}
}
cmd := exec.Command("clamdscan", "--fdpass", strategy, clam.Path, "-l", logFile)
_, _ = cmd.CombinedOutput() _, _ = cmd.CombinedOutput()
}() }()
return nil return nil
@ -226,7 +250,7 @@ func (u *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, er
var datas []dto.ClamLog var datas []dto.ClamLog
for i := 0; i < len(records); i++ { for i := 0; i < len(records); i++ {
item := loadResultFromLog(path.Join(global.CONF.System.DataDir, scanDir, clam.Name, records[i])) item := loadResultFromLog(path.Join(global.CONF.System.DataDir, resultDir, clam.Name, records[i]))
datas = append(datas, item) datas = append(datas, item)
} }
return int64(total), datas, nil return int64(total), datas, nil
@ -237,7 +261,7 @@ func (u *ClamService) CleanRecord(req dto.OperateByID) error {
if clam.ID == 0 { if clam.ID == 0 {
return constant.ErrRecordNotFound return constant.ErrRecordNotFound
} }
pathItem := path.Join(global.CONF.System.DataDir, scanDir, clam.Name) pathItem := path.Join(global.CONF.System.DataDir, resultDir, clam.Name)
_ = os.RemoveAll(pathItem) _ = os.RemoveAll(pathItem)
return nil return nil
} }
@ -319,7 +343,7 @@ func (u *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
func loadFileByName(name string) []string { func loadFileByName(name string) []string {
var logPaths []string var logPaths []string
pathItem := path.Join(global.CONF.System.DataDir, scanDir, name) pathItem := path.Join(global.CONF.System.DataDir, resultDir, name)
_ = filepath.Walk(pathItem, func(path string, info os.FileInfo, err error) error { _ = filepath.Walk(pathItem, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return nil return nil

@ -270,7 +270,7 @@ var AddShellColumn = &gormigrate.Migration{
} }
var AddClam = &gormigrate.Migration{ var AddClam = &gormigrate.Migration{
ID: "20240624-add-clam", ID: "20240701-add-clam",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Clam{}); err != nil { if err := tx.AutoMigrate(&model.Clam{}); err != nil {
return err return err

@ -72,7 +72,7 @@ func (sws *LocalWsSession) masterWrite(data []byte) error {
func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) { func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
global.LOG.Errorf("[xpack] A panic occurred during receive ws message, error message: %v", r) global.LOG.Errorf("A panic occurred during receive ws message, error message: %v", r)
} }
}() }()
wsConn := sws.wsConn wsConn := sws.wsConn

@ -118,7 +118,7 @@ func (sws *LogicSshWsSession) Start(quitChan chan bool) {
func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) { func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
global.LOG.Errorf("[xpack] A panic occurred during receive ws message, error message: %v", r) global.LOG.Errorf("[A panic occurred during receive ws message, error message: %v", r)
} }
}() }()
wsConn := sws.wsConn wsConn := sws.wsConn

@ -1585,6 +1585,12 @@ const docTemplate = `{
} }
} }
}, },
"/containers/download/log": {
"post": {
"description": "下载容器日志",
"responses": {}
}
},
"/containers/image": { "/containers/image": {
"get": { "get": {
"security": [ "security": [
@ -11155,7 +11161,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/dto.BatchDeleteReq" "$ref": "#/definitions/dto.ClamDelete"
} }
} }
], ],
@ -15460,6 +15466,12 @@ const docTemplate = `{
"description": { "description": {
"type": "string" "type": "string"
}, },
"infectedDir": {
"type": "string"
},
"infectedStrategy": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -15468,6 +15480,26 @@ const docTemplate = `{
} }
} }
}, },
"dto.ClamDelete": {
"type": "object",
"required": [
"ids"
],
"properties": {
"ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"removeInfected": {
"type": "boolean"
},
"removeResult": {
"type": "boolean"
}
}
},
"dto.ClamLogSearch": { "dto.ClamLogSearch": {
"type": "object", "type": "object",
"required": [ "required": [
@ -15501,6 +15533,12 @@ const docTemplate = `{
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"infectedDir": {
"type": "string"
},
"infectedStrategy": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -20116,6 +20154,9 @@ const docTemplate = `{
"domains": { "domains": {
"type": "string" "type": "string"
}, },
"execShell": {
"type": "boolean"
},
"expireDate": { "expireDate": {
"type": "string" "type": "string"
}, },
@ -20152,6 +20193,9 @@ const docTemplate = `{
"pushDir": { "pushDir": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"skipDNS": { "skipDNS": {
"type": "boolean" "type": "boolean"
}, },
@ -21663,6 +21707,9 @@ const docTemplate = `{
"domains": { "domains": {
"type": "string" "type": "string"
}, },
"execShell": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -21683,6 +21730,9 @@ const docTemplate = `{
"renew": { "renew": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"sslID": { "sslID": {
"type": "integer" "type": "integer"
}, },
@ -22242,6 +22292,9 @@ const docTemplate = `{
"dnsAccountId": { "dnsAccountId": {
"type": "integer" "type": "integer"
}, },
"execShell": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -22266,6 +22319,9 @@ const docTemplate = `{
"pushDir": { "pushDir": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"skipDNS": { "skipDNS": {
"type": "boolean" "type": "boolean"
} }
@ -22318,6 +22374,9 @@ const docTemplate = `{
"dnsAccountId": { "dnsAccountId": {
"type": "integer" "type": "integer"
}, },
"execShell": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -22342,6 +22401,9 @@ const docTemplate = `{
"pushDir": { "pushDir": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"skipDNS": { "skipDNS": {
"type": "boolean" "type": "boolean"
} }
@ -22786,9 +22848,15 @@ const docTemplate = `{
"$ref": "#/definitions/response.FileTree" "$ref": "#/definitions/response.FileTree"
} }
}, },
"extension": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
"isDir": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },

@ -1578,6 +1578,12 @@
} }
} }
}, },
"/containers/download/log": {
"post": {
"description": "下载容器日志",
"responses": {}
}
},
"/containers/image": { "/containers/image": {
"get": { "get": {
"security": [ "security": [
@ -11148,7 +11154,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/dto.BatchDeleteReq" "$ref": "#/definitions/dto.ClamDelete"
} }
} }
], ],
@ -15453,6 +15459,12 @@
"description": { "description": {
"type": "string" "type": "string"
}, },
"infectedDir": {
"type": "string"
},
"infectedStrategy": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -15461,6 +15473,26 @@
} }
} }
}, },
"dto.ClamDelete": {
"type": "object",
"required": [
"ids"
],
"properties": {
"ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"removeInfected": {
"type": "boolean"
},
"removeResult": {
"type": "boolean"
}
}
},
"dto.ClamLogSearch": { "dto.ClamLogSearch": {
"type": "object", "type": "object",
"required": [ "required": [
@ -15494,6 +15526,12 @@
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"infectedDir": {
"type": "string"
},
"infectedStrategy": {
"type": "string"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -20109,6 +20147,9 @@
"domains": { "domains": {
"type": "string" "type": "string"
}, },
"execShell": {
"type": "boolean"
},
"expireDate": { "expireDate": {
"type": "string" "type": "string"
}, },
@ -20145,6 +20186,9 @@
"pushDir": { "pushDir": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"skipDNS": { "skipDNS": {
"type": "boolean" "type": "boolean"
}, },
@ -21656,6 +21700,9 @@
"domains": { "domains": {
"type": "string" "type": "string"
}, },
"execShell": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -21676,6 +21723,9 @@
"renew": { "renew": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"sslID": { "sslID": {
"type": "integer" "type": "integer"
}, },
@ -22235,6 +22285,9 @@
"dnsAccountId": { "dnsAccountId": {
"type": "integer" "type": "integer"
}, },
"execShell": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -22259,6 +22312,9 @@
"pushDir": { "pushDir": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"skipDNS": { "skipDNS": {
"type": "boolean" "type": "boolean"
} }
@ -22311,6 +22367,9 @@
"dnsAccountId": { "dnsAccountId": {
"type": "integer" "type": "integer"
}, },
"execShell": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -22335,6 +22394,9 @@
"pushDir": { "pushDir": {
"type": "boolean" "type": "boolean"
}, },
"shell": {
"type": "string"
},
"skipDNS": { "skipDNS": {
"type": "boolean" "type": "boolean"
} }
@ -22779,9 +22841,15 @@
"$ref": "#/definitions/response.FileTree" "$ref": "#/definitions/response.FileTree"
} }
}, },
"extension": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
"isDir": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },

@ -229,11 +229,28 @@ definitions:
properties: properties:
description: description:
type: string type: string
infectedDir:
type: string
infectedStrategy:
type: string
name: name:
type: string type: string
path: path:
type: string type: string
type: object type: object
dto.ClamDelete:
properties:
ids:
items:
type: integer
type: array
removeInfected:
type: boolean
removeResult:
type: boolean
required:
- ids
type: object
dto.ClamLogSearch: dto.ClamLogSearch:
properties: properties:
clamID: clamID:
@ -256,6 +273,10 @@ definitions:
type: string type: string
id: id:
type: integer type: integer
infectedDir:
type: string
infectedStrategy:
type: string
name: name:
type: string type: string
path: path:
@ -3368,6 +3389,8 @@ definitions:
type: integer type: integer
domains: domains:
type: string type: string
execShell:
type: boolean
expireDate: expireDate:
type: string type: string
id: id:
@ -3392,6 +3415,8 @@ definitions:
type: string type: string
pushDir: pushDir:
type: boolean type: boolean
shell:
type: string
skipDNS: skipDNS:
type: boolean type: boolean
startDate: startDate:
@ -4401,6 +4426,8 @@ definitions:
type: string type: string
domains: domains:
type: string type: string
execShell:
type: boolean
id: id:
type: integer type: integer
keyType: keyType:
@ -4416,6 +4443,8 @@ definitions:
type: boolean type: boolean
renew: renew:
type: boolean type: boolean
shell:
type: string
sslID: sslID:
type: integer type: integer
time: time:
@ -4793,6 +4822,8 @@ definitions:
type: boolean type: boolean
dnsAccountId: dnsAccountId:
type: integer type: integer
execShell:
type: boolean
id: id:
type: integer type: integer
keyType: keyType:
@ -4809,6 +4840,8 @@ definitions:
type: string type: string
pushDir: pushDir:
type: boolean type: boolean
shell:
type: string
skipDNS: skipDNS:
type: boolean type: boolean
required: required:
@ -4844,6 +4877,8 @@ definitions:
type: boolean type: boolean
dnsAccountId: dnsAccountId:
type: integer type: integer
execShell:
type: boolean
id: id:
type: integer type: integer
keyType: keyType:
@ -4860,6 +4895,8 @@ definitions:
type: string type: string
pushDir: pushDir:
type: boolean type: boolean
shell:
type: string
skipDNS: skipDNS:
type: boolean type: boolean
required: required:
@ -5162,8 +5199,12 @@ definitions:
items: items:
$ref: '#/definitions/response.FileTree' $ref: '#/definitions/response.FileTree'
type: array type: array
extension:
type: string
id: id:
type: string type: string
isDir:
type: boolean
name: name:
type: string type: string
path: path:
@ -6414,6 +6455,10 @@ paths:
summary: Load docker status summary: Load docker status
tags: tags:
- Container Docker - Container Docker
/containers/download/log:
post:
description: 下载容器日志
responses: {}
/containers/image: /containers/image:
get: get:
description: 获取镜像名称列表 description: 获取镜像名称列表
@ -12471,7 +12516,7 @@ paths:
name: request name: request
required: true required: true
schema: schema:
$ref: '#/definitions/dto.BatchDeleteReq' $ref: '#/definitions/dto.ClamDelete'
responses: responses:
"200": "200":
description: OK description: OK

@ -126,18 +126,24 @@ export namespace Toolbox {
id: number; id: number;
name: string; name: string;
path: string; path: string;
infectedStrategy: string;
infectedDir: string;
lastHandleDate: string; lastHandleDate: string;
description: string; description: string;
} }
export interface ClamCreate { export interface ClamCreate {
name: string; name: string;
path: string; path: string;
infectedStrategy: string;
infectedDir: string;
description: string; description: string;
} }
export interface ClamUpdate { export interface ClamUpdate {
id: number; id: number;
name: string; name: string;
path: string; path: string;
infectedStrategy: string;
infectedDir: string;
description: string; description: string;
} }
export interface ClamSearchLog extends ReqPage { export interface ClamSearchLog extends ReqPage {

@ -135,7 +135,7 @@ export const createClam = (params: Toolbox.ClamCreate) => {
export const updateClam = (params: Toolbox.ClamUpdate) => { export const updateClam = (params: Toolbox.ClamUpdate) => {
return http.post(`/toolbox/clam/update`, params); return http.post(`/toolbox/clam/update`, params);
}; };
export const deleteClam = (params: { ids: number[] }) => { export const deleteClam = (params: { ids: number[]; removeResult: boolean; removeInfected: boolean }) => {
return http.post(`/toolbox/clam/del`, params); return http.post(`/toolbox/clam/del`, params);
}; };
export const handleClamScan = (id: number) => { export const handleClamScan = (id: number) => {

@ -1066,18 +1066,36 @@ const message = {
}, },
clam: { clam: {
clam: 'Virus Scan', clam: 'Virus Scan',
clamHelper:
'The minimum recommended configuration for ClamAV is: 3 GiB of RAM or more, single-core CPU with 2.0 GHz or higher, and at least 5 GiB of available hard disk space.',
noClam: 'ClamAV service not detected, please refer to the official documentation for installation!', noClam: 'ClamAV service not detected, please refer to the official documentation for installation!',
notStart: 'ClamAV service is currently not running, please start it first!', notStart: 'ClamAV service is currently not running, please start it first!',
clamCreate: 'Create Scan Rule', removeResult: 'Delete Report Files',
removeResultHelper: 'Delete report files generated during task execution to free up storage space.',
removeInfected: 'Delete Virus Files',
removeInfectedHelper:
'Delete virus files detected during the task to ensure server security and normal operation.',
clamCreate: 'Create Scan Rules',
infectedStrategy: 'Virus Strategy',
remove: 'Delete',
removeHelper: 'Delete virus files, choose carefully!',
move: 'Move',
moveHelper: 'Move virus files to specified directory',
copy: 'Copy',
copyHelper: 'Copy virus files to specified directory',
none: 'Do Nothing',
noneHelper: 'Take no action on virus files',
scanDir: 'Scan Directory',
infectedDir: 'Infected Directory',
scanDate: 'Scan Date', scanDate: 'Scan Date',
scanTime: 'Elapsed Time', scanTime: 'Time Taken',
scannedFiles: 'Number of Scanned Files', scannedFiles: 'Scanned Files',
infectedFiles: 'Number of Infected Files', infectedFiles: 'Infected Files',
log: 'Details', log: 'Details',
clamConf: 'Scan Configuration', clamConf: 'Scan Configuration',
clamLog: 'Scan Log', clamLog: 'Scan Logs',
freshClam: 'Virus Database Refresh Configuration', freshClam: 'Update Virus Definitions',
freshClamLog: 'Virus Database Refresh Log', freshClamLog: 'Update Virus Definitions Logs',
}, },
}, },
logs: { logs: {

@ -1008,9 +1008,26 @@ const message = {
}, },
clam: { clam: {
clam: '', clam: '',
clamHelper:
'ClamAV 3 GiB RAM2.0 GHz CPU 5 GiB ',
noClam: ' ClamAV ', noClam: ' ClamAV ',
notStart: ' ClamAV ', notStart: ' ClamAV ',
removeResult: '',
removeResultHelper: '',
removeInfected: '',
removeInfectedHelper: '',
clamCreate: '', clamCreate: '',
infectedStrategy: '',
remove: '',
removeHelper: '',
move: '',
moveHelper: '',
copy: '',
copyHelper: '',
none: '',
noneHelper: '',
scanDir: '',
infectedDir: '',
scanDate: '', scanDate: '',
scanTime: '', scanTime: '',
scannedFiles: '', scannedFiles: '',

@ -1009,9 +1009,26 @@ const message = {
}, },
clam: { clam: {
clam: '', clam: '',
clamHelper:
'ClamAV 3 GiB RAM2.0 GHz CPU 5 GiB ',
noClam: ' ClamAV ', noClam: ' ClamAV ',
notStart: ' ClamAV ', notStart: ' ClamAV ',
removeResult: '',
removeResultHelper: '',
removeInfected: '',
removeInfectedHelper: '',
clamCreate: '', clamCreate: '',
infectedStrategy: '',
remove: '',
removeHelper: '',
move: '',
moveHelper: '',
copy: '',
copyHelper: '',
none: '',
noneHelper: '',
scanDir: '',
infectedDir: '',
scanDate: '', scanDate: '',
scanTime: '', scanTime: '',
scannedFiles: '', scannedFiles: '',

@ -1,6 +1,21 @@
<template> <template>
<div> <div>
<LayoutContent v-loading="loading" v-if="!isRecordShow && !isSettingShow" :title="$t('toolbox.clam.clam')"> <LayoutContent v-loading="loading" v-if="!isRecordShow && !isSettingShow" :title="$t('toolbox.clam.clam')">
<template #prompt>
<el-alert type="info" :closable="false">
<template #title>
{{ $t('toolbox.clam.clamHelper') }}
<el-link
style="font-size: 12px; margin-left: 5px"
icon="Position"
@click="toDoc()"
type="primary"
>
{{ $t('firewall.quickJump') }}
</el-link>
</template>
</el-alert>
</template>
<template #app> <template #app>
<ClamStatus <ClamStatus
@setting="setting" @setting="setting"
@ -28,7 +43,7 @@
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
<el-card v-if="!clamStatus.isRunning && maskShow" class="mask-prompt"> <el-card v-if="clamStatus.isExist && !clamStatus.isRunning && maskShow" class="mask-prompt">
<span>{{ $t('toolbox.clam.notStart') }}</span> <span>{{ $t('toolbox.clam.notStart') }}</span>
</el-card> </el-card>
<template #main v-if="clamStatus.isExist"> <template #main v-if="clamStatus.isExist">
@ -48,9 +63,30 @@
prop="name" prop="name"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column :label="$t('file.path')" :min-width="120" prop="path" show-overflow-tooltip> <el-table-column
:label="$t('toolbox.clam.scanDir')"
:min-width="120"
prop="path"
show-overflow-tooltip
>
<template #default="{ row }"> <template #default="{ row }">
<el-button text type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button> <el-button link type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button>
</template>
</el-table-column>
<el-table-column
:label="$t('toolbox.clam.infectedDir')"
:min-width="120"
prop="path"
show-overflow-tooltip
>
<template #default="{ row }">
<el-button
link
type="primary"
@click="toFolder(row.infectedDir + '/1panel-infected/' + row.name)"
>
{{ row.infectedDir + '/1panel-infected/' + row.name }}
</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@ -75,7 +111,20 @@
</template> </template>
</LayoutContent> </LayoutContent>
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()" /> <OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
<template #content>
<el-form class="mt-4 mb-1" ref="deleteForm" label-position="left">
<el-form-item>
<el-checkbox v-model="removeResult" :label="$t('toolbox.clam.removeResult')" />
<span class="input-help">{{ $t('toolbox.clam.removeResultHelper') }}</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="removeInfected" :label="$t('toolbox.clam.removeInfected')" />
<span class="input-help">{{ $t('toolbox.clam.removeInfectedHelper') }}</span>
</el-form-item>
</el-form>
</template>
</OpDialog>
<OperateDialog @search="search" ref="dialogRef" /> <OperateDialog @search="search" ref="dialogRef" />
<LogDialog ref="dialogLogRef" /> <LogDialog ref="dialogLogRef" />
<SettingDialog v-if="isSettingShow" /> <SettingDialog v-if="isSettingShow" />
@ -114,6 +163,9 @@ const operateIDs = ref();
const dialogLogRef = ref(); const dialogLogRef = ref();
const isRecordShow = ref(); const isRecordShow = ref();
const removeResult = ref();
const removeInfected = ref();
const isSettingShow = ref(); const isSettingShow = ref();
const maskShow = ref(true); const maskShow = ref(true);
const clamStatus = ref({ const clamStatus = ref({
@ -144,20 +196,27 @@ const setting = () => {
}; };
const getStatus = (status: any) => { const getStatus = (status: any) => {
clamStatus.value = status; clamStatus.value = status;
console.log(clamStatus.value);
search(); search();
}; };
const toFolder = (folder: string) => { const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } }); router.push({ path: '/hosts/files', query: { path: folder } });
}; };
const toDoc = async () => {
window.open('https://1panel.cn/docs/user_manual/toolbox/clam/', '_blank', 'noopener,noreferrer');
};
const onChange = async (row: any) => { const onChange = async (row: any) => {
await await updateClam(row); await await updateClam(row);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
}; };
const onOpenDialog = async (title: string, rowData: Partial<Toolbox.ClamInfo> = {}) => { const onOpenDialog = async (
title: string,
rowData: Partial<Toolbox.ClamInfo> = {
infectedStrategy: 'none',
},
) => {
let params = { let params = {
title, title,
rowData: { ...rowData }, rowData: { ...rowData },
@ -182,7 +241,7 @@ const onDelete = async (row: Toolbox.ClamInfo | null) => {
title: i18n.global.t('commons.button.delete'), title: i18n.global.t('commons.button.delete'),
names: names, names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [ msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('cronjob.cronTask'), i18n.global.t('toolbox.clam.clam'),
i18n.global.t('commons.button.delete'), i18n.global.t('commons.button.delete'),
]), ]),
api: null, api: null,
@ -192,7 +251,7 @@ const onDelete = async (row: Toolbox.ClamInfo | null) => {
const onSubmitDelete = async () => { const onSubmitDelete = async () => {
loading.value = true; loading.value = true;
await deleteClam({ ids: operateIDs.value }) await deleteClam({ ids: operateIDs.value, removeResult: removeResult.value, removeInfected: removeInfected.value })
.then(() => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess')); MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));

@ -24,13 +24,32 @@
v-model.trim="dialogData.rowData!.name" v-model.trim="dialogData.rowData!.name"
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('file.root')" prop="path"> <el-form-item :label="$t('toolbox.clam.scanDir')" prop="path">
<el-input v-model="dialogData.rowData!.path"> <el-input v-model="dialogData.rowData!.path">
<template #prepend> <template #prepend>
<FileList @choose="loadDir" :dir="true"></FileList> <FileList @choose="loadDir" :dir="true"></FileList>
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('toolbox.clam.infectedStrategy')" prop="infectedStrategy">
<el-radio-group v-model="dialogData.rowData!.infectedStrategy">
<el-radio value="none">{{ $t('toolbox.clam.none') }}</el-radio>
<el-radio value="remove">{{ $t('toolbox.clam.remove') }}</el-radio>
<el-radio value="move">{{ $t('toolbox.clam.move') }}</el-radio>
<el-radio value="copy">{{ $t('toolbox.clam.copy') }}</el-radio>
</el-radio-group>
<span class="input-help">
{{ $t('toolbox.clam.' + dialogData.rowData!.infectedStrategy + 'Helper') }}
</span>
</el-form-item>
<el-form-item v-if="hasInfectedDir()" :label="$t('toolbox.clam.infectedDir')" prop="infectedDir">
<el-input v-model="dialogData.rowData!.infectedDir">
<template #prepend>
<FileList @choose="loadInfectedDir" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description"> <el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" /> <el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" />
</el-form-item> </el-form-item>
@ -90,9 +109,17 @@ const rules = reactive({
type FormInstance = InstanceType<typeof ElForm>; type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const hasInfectedDir = () => {
return (
dialogData.value.rowData!.infectedStrategy === 'move' || dialogData.value.rowData!.infectedStrategy === 'copy'
);
};
const loadDir = async (path: string) => { const loadDir = async (path: string) => {
dialogData.value.rowData!.path = path; dialogData.value.rowData!.path = path;
}; };
const loadInfectedDir = async (path: string) => {
dialogData.value.rowData!.infectedDir = path;
};
const onSubmit = async (formEl: FormInstance | undefined) => { const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;

@ -4,7 +4,7 @@
<el-card> <el-card>
<div> <div>
<el-tag class="float-left" effect="dark" type="success"> <el-tag class="float-left" effect="dark" type="success">
{{ dialogData.rowData.name }} {{ $t('commons.table.name') }}: {{ dialogData.rowData.name }}
</el-tag> </el-tag>
<el-popover <el-popover
v-if="dialogData.rowData.path.length >= 35" v-if="dialogData.rowData.path.length >= 35"
@ -15,7 +15,7 @@
> >
<template #reference> <template #reference>
<el-tag style="float: left" effect="dark" type="success"> <el-tag style="float: left" effect="dark" type="success">
{{ dialogData.rowData.path.substring(0, 20) }}... {{ $t('file.path') }}: {{ dialogData.rowData.path.substring(0, 20) }}...
</el-tag> </el-tag>
</template> </template>
</el-popover> </el-popover>
@ -25,7 +25,7 @@
effect="dark" effect="dark"
type="success" type="success"
> >
{{ dialogData.rowData.path }} {{ $t('file.path') }}: {{ dialogData.rowData.path }}
</el-tag> </el-tag>
<span class="buttons"> <span class="buttons">
@ -115,9 +115,14 @@
<template #label> <template #label>
<span class="status-label">{{ $t('toolbox.clam.infectedFiles') }}</span> <span class="status-label">{{ $t('toolbox.clam.infectedFiles') }}</span>
</template> </template>
<span class="status-count"> <span class="status-count" v-if="!hasInfectedDir()">
{{ currentRecord?.infectedFiles }}
</span>
<div class="count" v-else>
<span @click="toFolder(currentRecord?.name)">
{{ currentRecord?.infectedFiles }} {{ currentRecord?.infectedFiles }}
</span> </span>
</div>
</el-form-item> </el-form-item>
</el-row> </el-row>
<el-row v-if="currentRecord?.log"> <el-row v-if="currentRecord?.log">
@ -167,6 +172,8 @@ import { MsgSuccess } from '@/utils/message';
import { shortcuts } from '@/utils/shortcuts'; import { shortcuts } from '@/utils/shortcuts';
import { Toolbox } from '@/api/interface/toolbox'; import { Toolbox } from '@/api/interface/toolbox';
import { cleanClamRecord, handleClamScan, searchClamRecord } from '@/api/modules/toolbox'; import { cleanClamRecord, handleClamScan, searchClamRecord } from '@/api/modules/toolbox';
import { useRouter } from 'vue-router';
const router = useRouter();
const loading = ref(); const loading = ref();
const refresh = ref(false); const refresh = ref(false);
@ -212,6 +219,11 @@ const handleCurrentChange = (val: number) => {
searchInfo.page = val; searchInfo.page = val;
search(); search();
}; };
const hasInfectedDir = () => {
return (
dialogData.value.rowData!.infectedStrategy === 'move' || dialogData.value.rowData!.infectedStrategy === 'copy'
);
};
const timeRangeLoad = ref<[Date, Date]>([ const timeRangeLoad = ref<[Date, Date]>([
new Date(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7).setHours(0, 0, 0, 0)), new Date(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7).setHours(0, 0, 0, 0)),
@ -239,6 +251,10 @@ const onHandle = async (row: Toolbox.ClamInfo) => {
loading.value = false; loading.value = false;
}); });
}; };
const toFolder = async (path: string) => {
let folder = dialogData.value.rowData!.infectedDir + '/1panel-infected/' + path;
router.push({ path: '/hosts/files', query: { path: folder } });
};
const search = async () => { const search = async () => {
if (timeRangeLoad.value && timeRangeLoad.value.length === 2) { if (timeRangeLoad.value && timeRangeLoad.value.length === 2) {
@ -278,7 +294,6 @@ const onClean = async () => {
type: 'warning', type: 'warning',
}).then(async () => { }).then(async () => {
loading.value = true; loading.value = true;
console.log(dialogData.value.id);
cleanClamRecord(dialogData.value.rowData.id) cleanClamRecord(dialogData.value.rowData.id)
.then(() => { .then(() => {
loading.value = false; loading.value = false;
@ -334,6 +349,16 @@ defineExpose({
float: right; float: right;
} }
.count {
span {
font-size: 25px;
color: $primary-color;
font-weight: 500;
line-height: 32px;
cursor: pointer;
}
}
@media only screen and (max-width: 1400px) { @media only screen and (max-width: 1400px) {
.mainClass { .mainClass {
overflow: auto; overflow: auto;

@ -5,7 +5,7 @@
<ClamStatus v-model:loading="loading" /> <ClamStatus v-model:loading="loading" />
</template> </template>
<template #title> <template #title>
<back-button name="Clam" header="Clamav"> <back-button name="Clam" header="ClamAV">
<template #buttons> <template #buttons>
<el-button type="primary" :plain="activeName !== 'clamd'" @click="search('clamd')"> <el-button type="primary" :plain="activeName !== 'clamd'" @click="search('clamd')">
{{ $t('toolbox.clam.clamConf') }} {{ $t('toolbox.clam.clamConf') }}

@ -3,7 +3,7 @@
<div class="app-status tool-status" v-if="data.isExist"> <div class="app-status tool-status" v-if="data.isExist">
<el-card> <el-card>
<div> <div>
<el-tag effect="dark" type="success">Clamav</el-tag> <el-tag effect="dark" type="success">ClamAV</el-tag>
<el-tag round class="status-content" v-if="data.isActive" type="success"> <el-tag round class="status-content" v-if="data.isActive" type="success">
{{ $t('commons.status.running') }} {{ $t('commons.status.running') }}
</el-tag> </el-tag>
@ -75,7 +75,7 @@ const toDoc = async () => {
const onOperate = async (operation: string) => { const onOperate = async (operation: string) => {
em('update:maskShow', false); em('update:maskShow', false);
ElMessageBox.confirm( ElMessageBox.confirm(
i18n.global.t('commons.msg.operatorHelper', [' Clamav ', i18n.global.t('app.' + operation)]), i18n.global.t('commons.msg.operatorHelper', [' ClamAV ', i18n.global.t('app.' + operation)]),
i18n.global.t('app.' + operation), i18n.global.t('app.' + operation),
{ {
confirmButtonText: i18n.global.t('commons.button.confirm'), confirmButtonText: i18n.global.t('commons.button.confirm'),

Loading…
Cancel
Save